package com.atlassian.stash.internal.hook.repository;

import com.atlassian.bitbucket.hook.repository.*;
import com.atlassian.event.api.EventPublisher;
import com.atlassian.plugin.ModuleDescriptor;
import com.atlassian.plugin.Plugin;
import com.atlassian.plugin.PluginAccessor;
import com.atlassian.plugin.servlet.download.plugin.DownloadStrategyModuleDescriptor;
import com.atlassian.plugin.web.descriptors.WebFragmentModuleDescriptor;
import com.atlassian.bitbucket.AbstractServiceTest;
import com.atlassian.bitbucket.avatar.CacheableAvatarSupplier;
import com.atlassian.bitbucket.event.hook.RepositoryHookDisabledEvent;
import com.atlassian.bitbucket.event.hook.RepositoryHookEnabledEvent;
import com.atlassian.bitbucket.event.hook.RepositoryHookEvent;
import com.atlassian.bitbucket.event.hook.RepositoryHookSettingsChangedEvent;
import com.atlassian.bitbucket.validation.FormValidationException;
import com.atlassian.bitbucket.IllegalEntityStateException;
import com.atlassian.bitbucket.NoSuchEntityException;
import com.atlassian.bitbucket.i18n.I18nService;
import com.atlassian.bitbucket.i18n.SimpleI18nService;
import com.atlassian.stash.internal.plugin.RepositoryHookModuleDescriptor;
import com.atlassian.stash.internal.repository.InternalRepository;
import com.atlassian.stash.internal.repository.InternalRepositoryHook;
import com.atlassian.stash.internal.repository.RepositoryHookDao;
import com.atlassian.stash.internal.setting.InternalSharedLob;
import com.atlassian.stash.internal.setting.SettingsHelper;
import com.atlassian.bitbucket.repository.Repository;
import com.atlassian.bitbucket.setting.RepositorySettingsValidator;
import com.atlassian.bitbucket.setting.Settings;
import com.atlassian.bitbucket.setting.SettingsBuilder;
import com.atlassian.bitbucket.setting.SettingsValidationErrors;
import com.atlassian.bitbucket.util.Page;
import com.atlassian.bitbucket.util.PageUtils;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import org.hamcrest.Description;
import org.hamcrest.Matcher;
import org.hamcrest.TypeSafeMatcher;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Answers;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.runners.MockitoJUnitRunner;
import org.mockito.stubbing.Answer;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.TransactionStatus;

import javax.annotation.Nonnull;
import java.io.IOException;
import java.net.FileNameMap;
import java.util.*;

import static com.atlassian.stash.internal.hook.repository.DefaultRepositoryHookService.PAGE_SETTINGS_LIMIT;
import static org.mockito.Matchers.eq;
import static org.mockito.Mockito.*;

@RunWith(MockitoJUnitRunner.class)
public class DefaultRepositoryHookServiceTest extends AbstractServiceTest {

    private static final String HOOK_KEY = "repository-hook";
    private static final String HOOK_NAME = "repository-hook-name";
    private static final String HOOK_DESCRIPTION = "repository-hook-description";
    private static final String HOOK_CONFIG_FORM_KEY = "repository-hook-config-form";
    private static final String HOOK_ICON_FILE = "hook_icon.png";
    private static final String SERIALIZED_SETTINGS = "SERIALIZED SETTINGS DATA";
    private static final long SETTINGS_ID = 123456789L;
    private static final InternalSharedLob SETTINGS_LOB = new InternalSharedLob.Builder()
            .id(SETTINGS_ID)
            .data(SERIALIZED_SETTINGS)
            .build();

    private final List<RepositoryHookModuleDescriptor> descriptors = Lists.newArrayList();
    private final I18nService i18nService = new SimpleI18nService();
    private List<InternalRepositoryHook> enabledHooks;

    @Mock
    private EventPublisher eventPublisher;
    @Mock
    private RepositoryHookDao hookDao;
    @Mock(answer = Answers.RETURNS_DEEP_STUBS)
    private Plugin plugin;
    @Mock
    private PluginAccessor pluginAccessor;
    @Mock
    private PreReceiveRepositoryHook preReceiveHook;
    @Mock
    private InternalRepository repository;
    @Mock
    private InternalRepositoryHook repositoryHook;
    private DefaultRepositoryHookService service;
    @Mock
    private Settings settings;
    @Mock
    private SettingsHelper settingsHelper;
    @Mock
    private PlatformTransactionManager transactionManager;
    @Mock
    private TransactionStatus transactionStatus;

    public DefaultRepositoryHookServiceTest() {
        super(DefaultRepositoryHookService.class, RepositoryHookService.class);
    }

    @Before
    public void setUp() throws Throwable {
        when(pluginAccessor.getEnabledModuleDescriptorsByClass(eq(RepositoryHookModuleDescriptor.class))).thenReturn(descriptors);
        when(repository.getId()).thenReturn(1);

        service = new DefaultRepositoryHookService(eventPublisher, new FileNameMap() {
            @Override
            public String getContentTypeFor(String fileName) {
                return "image/" + fileName;
            }
        }, hookDao, i18nService, pluginAccessor, settingsHelper, transactionManager);

        enabledHooks = Lists.newArrayList();
    }

    @Test
    public void testCreateSettingsBuilder() throws Exception {
        SettingsBuilder builder = service.createSettingsBuilder();

        assertTrue(builder.build().asMap().keySet().isEmpty());
    }

    @Test
    public void testDisableAlreadyDisabledHook() throws Exception {
        InternalRepositoryHook repositoryHook = createHook(HOOK_KEY, SETTINGS_LOB, false);
        when(hookDao.findByKey(repository, HOOK_KEY)).thenReturn(repositoryHook);

        createConfigurableModule(HOOK_KEY, RepositoryHookType.PRE_RECEIVE, HOOK_CONFIG_FORM_KEY);

        service.disable(repository, HOOK_KEY);

        verify(hookDao, never()).create(any(InternalRepositoryHook.class));
        verify(hookDao, never()).update(any(InternalRepositoryHook.class));

        verify(eventPublisher).publish(argThat(repositoryHookDisabledMatcher()));
    }

    @Test
    public void testDisableHook() throws Exception {
        InternalRepositoryHook repositoryHook = createHook(HOOK_KEY, SETTINGS_LOB, true);
        when(hookDao.findByKey(repository, HOOK_KEY)).thenReturn(repositoryHook);

        createConfigurableModule(HOOK_KEY, RepositoryHookType.PRE_RECEIVE, HOOK_CONFIG_FORM_KEY);

        service.disable(repository, HOOK_KEY);

        ArgumentCaptor<InternalRepositoryHook> hookCaptor = ArgumentCaptor.forClass(InternalRepositoryHook.class);
        verify(hookDao).update(hookCaptor.capture());

        InternalRepositoryHook capturedHook = hookCaptor.getValue();
        assertEquals(HOOK_KEY, capturedHook.getHookKey());
        assertEquals(repository, capturedHook.getRepository());
        assertEquals(SETTINGS_LOB.getData(), capturedHook.getSettings().getData());
        assertEquals(SETTINGS_LOB.getId(), capturedHook.getSettings().getId());
        assertEquals(false, capturedHook.isEnabled());

        verify(eventPublisher).publish(argThat(repositoryHookDisabledMatcher()));
    }

    @Test(expected = NoSuchEntityException.class)
    public void testDisableKeyForNonHookPlugin() throws Exception {
        InternalRepositoryHook repositoryHook = createHook(HOOK_KEY, SETTINGS_LOB, false);
        when(hookDao.findByKey(repository, HOOK_KEY)).thenReturn(repositoryHook);

        ModuleDescriptor nonHookDescriptor = mock(DownloadStrategyModuleDescriptor.class);
        when(pluginAccessor.getEnabledPluginModule(HOOK_KEY))
                .thenReturn(nonHookDescriptor);

        try {
            service.disable(repository, HOOK_KEY);
        } finally {
            verify(hookDao, never()).create(any(InternalRepositoryHook.class));
            verify(hookDao, never()).update(any(InternalRepositoryHook.class));
            verifyZeroInteractions(eventPublisher);
        }
    }

    @Test
    public void testDisableNonExistingHook() throws Exception {
        when(hookDao.findByKey(repository, HOOK_KEY)).thenReturn(null);

        createConfigurableModule(HOOK_KEY, RepositoryHookType.PRE_RECEIVE, HOOK_CONFIG_FORM_KEY);

        service.disable(repository, HOOK_KEY);

        ArgumentCaptor<InternalRepositoryHook> hookCaptor = ArgumentCaptor.forClass(InternalRepositoryHook.class);
        verify(hookDao).create(hookCaptor.capture());

        InternalRepositoryHook capturedHook = hookCaptor.getValue();
        assertEquals(HOOK_KEY, capturedHook.getHookKey());
        assertEquals(repository, capturedHook.getRepository());
        assertEquals(null, capturedHook.getSettings());
        assertEquals(false, capturedHook.isEnabled());

        verify(eventPublisher).publish(argThat(repositoryHookDisabledMatcher()));
    }

    @Test(expected = NoSuchEntityException.class)
    public void testDisableNonExistingHookKey() throws Exception {
        InternalRepositoryHook repositoryHook = createHook(HOOK_KEY, SETTINGS_LOB, false);
        when(hookDao.findByKey(repository, HOOK_KEY)).thenReturn(repositoryHook);

        try {
            service.disable(repository, HOOK_KEY);
        } finally {
            verify(hookDao, never()).create(any(InternalRepositoryHook.class));
            verify(hookDao, never()).update(any(InternalRepositoryHook.class));
            verifyZeroInteractions(eventPublisher);
        }
    }

    @Test
    public void testEnableAlreadyEnabledHook() throws Exception {
        InternalRepositoryHook repositoryHook = createHook(HOOK_KEY, SETTINGS_LOB, true);
        when(hookDao.findByKey(repository, HOOK_KEY)).thenReturn(repositoryHook);

        createConfigurableModule(HOOK_KEY, RepositoryHookType.PRE_RECEIVE, HOOK_CONFIG_FORM_KEY);

        service.enable(repository, HOOK_KEY);

        verify(hookDao, never()).update(any(InternalRepositoryHook.class));
        verify(hookDao, never()).create(any(InternalRepositoryHook.class));

        verify(eventPublisher).publish(argThat(repositoryHookEnabledMatcher()));
    }

    @Test
    public void testEnableAlreadyEnabledHookWithSettings() throws Exception {
        InternalRepositoryHook repositoryHook = createHook(HOOK_KEY, SETTINGS_LOB, true);
        when(hookDao.findByKey(repository, HOOK_KEY)).thenReturn(repositoryHook);

        createConfigurableModule(HOOK_KEY, RepositoryHookType.PRE_RECEIVE, HOOK_CONFIG_FORM_KEY);

        when(settingsHelper.serialize(settings)).thenReturn(SERIALIZED_SETTINGS);

        service.enable(repository, HOOK_KEY, settings);

        // Apparently 'enable' is being used to update the repository hook settings.
        // So even if the status doesn't change if settings are provided the hook should be updated.
        ArgumentCaptor<InternalRepositoryHook> hookCaptor = ArgumentCaptor.forClass(InternalRepositoryHook.class);
        verify(hookDao).update(hookCaptor.capture());

        InternalRepositoryHook capturedHook = hookCaptor.getValue();
        assertEquals(HOOK_KEY, capturedHook.getHookKey());
        assertEquals(repository, capturedHook.getRepository());
        assertEquals(SERIALIZED_SETTINGS, capturedHook.getSettings().getData());
        assertEquals(true, capturedHook.isEnabled());

        verify(eventPublisher).publish(argThat(repositoryHookEnabledMatcher()));
        verify(eventPublisher).publish(argThat(repositoryHookSettingsChangedMatcher()));
    }

    @Test
    public void testEnableConfiguredHookWhenSettingsAreRequired() throws Exception {
        InternalRepositoryHook repositoryHook = createHook(HOOK_KEY, SETTINGS_LOB, false);
        when(hookDao.findByKey(repository, HOOK_KEY)).thenReturn(repositoryHook);

        createConfigurableModule(HOOK_KEY, RepositoryHookType.PRE_RECEIVE, HOOK_CONFIG_FORM_KEY);

        service.enable(repository, HOOK_KEY);

        ArgumentCaptor<InternalRepositoryHook> hookCaptor = ArgumentCaptor.forClass(InternalRepositoryHook.class);
        verify(hookDao).update(hookCaptor.capture());

        InternalRepositoryHook capturedHook = hookCaptor.getValue();
        assertEquals(HOOK_KEY, capturedHook.getHookKey());
        assertEquals(repository, capturedHook.getRepository());
        assertEquals(SETTINGS_LOB.getData(), capturedHook.getSettings().getData());
        assertEquals(SETTINGS_LOB.getId(), capturedHook.getSettings().getId());
        assertEquals(true, capturedHook.isEnabled());

        verify(eventPublisher).publish(argThat(repositoryHookEnabledMatcher()));
    }

    @Test
    public void testEnableHookWhenSettingsAreNotRequired() throws Exception {
        InternalRepositoryHook repositoryHook = createHook(HOOK_KEY, null, false);

        when(hookDao.findByKey(repository, HOOK_KEY)).thenReturn(repositoryHook);

        createNonConfigurableModule(HOOK_KEY, RepositoryHookType.PRE_RECEIVE);

        service.enable(repository, HOOK_KEY);

        ArgumentCaptor<InternalRepositoryHook> hookCaptor = ArgumentCaptor.forClass(InternalRepositoryHook.class);
        verify(hookDao).update(hookCaptor.capture());

        InternalRepositoryHook capturedHook = hookCaptor.getValue();
        assertEquals(HOOK_KEY, capturedHook.getHookKey());
        assertEquals(repository, capturedHook.getRepository());
        assertEquals(null, capturedHook.getSettings());
        assertEquals(true, capturedHook.isEnabled());

        verify(eventPublisher).publish(argThat(repositoryHookEnabledMatcher()));
    }

    @Test
    public void testEnableHookWithSettings() throws Exception {
        InternalRepositoryHook repositoryHook = createHook(HOOK_KEY, SETTINGS_LOB, false);
        when(hookDao.findByKey(repository, HOOK_KEY)).thenReturn(repositoryHook);

        createConfigurableModule(HOOK_KEY, RepositoryHookType.PRE_RECEIVE, HOOK_CONFIG_FORM_KEY);

        when(settingsHelper.serialize(settings)).thenReturn(SERIALIZED_SETTINGS);

        service.enable(repository, HOOK_KEY, settings);

        ArgumentCaptor<InternalRepositoryHook> hookCaptor = ArgumentCaptor.forClass(InternalRepositoryHook.class);
        verify(hookDao).update(hookCaptor.capture());

        InternalRepositoryHook capturedHook = hookCaptor.getValue();
        assertEquals(HOOK_KEY, capturedHook.getHookKey());
        assertEquals(repository, capturedHook.getRepository());
        assertEquals(SERIALIZED_SETTINGS, capturedHook.getSettings().getData());
        assertEquals(true, capturedHook.isEnabled());

        verify(eventPublisher).publish(argThat(repositoryHookEnabledMatcher()));
        verify(eventPublisher).publish(argThat(repositoryHookSettingsChangedMatcher()));
    }

    @Test(expected = NoSuchEntityException.class)
    public void testEnableKeyForNonHookPlugin() throws Exception {
        InternalRepositoryHook repositoryHook = createHook(HOOK_KEY, SETTINGS_LOB, false);
        when(hookDao.findByKey(repository, HOOK_KEY)).thenReturn(repositoryHook);

        ModuleDescriptor nonHookDescriptor = mock(DownloadStrategyModuleDescriptor.class);
        when(pluginAccessor.getEnabledPluginModule(HOOK_KEY))
                .thenReturn(nonHookDescriptor);

        try {
            service.enable(repository, HOOK_KEY);
        } finally {
            verify(hookDao, never()).create(any(InternalRepositoryHook.class));
            verify(hookDao, never()).update(any(InternalRepositoryHook.class));
            verifyZeroInteractions(eventPublisher);
        }
    }

    @Test(expected = IllegalArgumentException.class)
    public void testEnableKeyForNonHookPluginWithSettings() throws Exception {
        createConfigurableModule(HOOK_KEY, RepositoryHookType.PRE_RECEIVE, HOOK_CONFIG_FORM_KEY);
        InternalRepositoryHook repositoryHook = createHook(HOOK_KEY, SETTINGS_LOB, false);
        when(hookDao.findByKey(repository, HOOK_KEY)).thenReturn(repositoryHook);

        ModuleDescriptor nonHookDescriptor = mock(DownloadStrategyModuleDescriptor.class);
        when(pluginAccessor.getEnabledPluginModule(HOOK_KEY))
                .thenReturn(nonHookDescriptor);

        when(settingsHelper.serialize(settings)).thenReturn(SERIALIZED_SETTINGS);

        try {
            service.enable(repository, HOOK_KEY, settings);
        } finally {
            verify(hookDao, never()).create(any(InternalRepositoryHook.class));
            verify(hookDao, never()).update(any(InternalRepositoryHook.class));
            verifyZeroInteractions(eventPublisher);
        }
    }

    @Test(expected = IllegalEntityStateException.class)
    public void testEnableNonConfiguredHookWhenSettingsAreRequired() throws Exception {
        InternalRepositoryHook repositoryHook = createHook(HOOK_KEY, null, false);
        when(hookDao.findByKey(repository, HOOK_KEY)).thenReturn(repositoryHook);

        createConfigurableModule(HOOK_KEY, RepositoryHookType.PRE_RECEIVE, HOOK_CONFIG_FORM_KEY);

        try {
            service.enable(repository, HOOK_KEY);
        } finally {
            verify(hookDao, never()).create(any(InternalRepositoryHook.class));
            verify(hookDao, never()).update(any(InternalRepositoryHook.class));
            verifyZeroInteractions(eventPublisher);
        }
    }

    @Test
    public void testEnableNonConfiguredHookWithSettings() throws Exception {
        InternalRepositoryHook repositoryHook = createHook(HOOK_KEY, null, false);
        when(hookDao.findByKey(repository, HOOK_KEY)).thenReturn(repositoryHook);

        createConfigurableModule(HOOK_KEY, RepositoryHookType.PRE_RECEIVE, HOOK_CONFIG_FORM_KEY);

        when(settingsHelper.serialize(settings)).thenReturn(SERIALIZED_SETTINGS);

        service.enable(repository, HOOK_KEY, settings);

        ArgumentCaptor<InternalRepositoryHook> hookCaptor = ArgumentCaptor.forClass(InternalRepositoryHook.class);
        verify(hookDao).update(hookCaptor.capture());

        InternalRepositoryHook capturedHook = hookCaptor.getValue();
        assertEquals(HOOK_KEY, capturedHook.getHookKey());
        assertEquals(repository, capturedHook.getRepository());
        assertEquals(SERIALIZED_SETTINGS, capturedHook.getSettings().getData());
        assertEquals(true, capturedHook.isEnabled());

        verify(eventPublisher).publish(argThat(repositoryHookEnabledMatcher()));
        verify(eventPublisher).publish(argThat(repositoryHookSettingsChangedMatcher()));
    }

    @Test(expected = NoSuchEntityException.class)
    public void testEnableNonExistingHookKey() throws Exception {
        InternalRepositoryHook repositoryHook = createHook(HOOK_KEY, SETTINGS_LOB, false);
        when(hookDao.findByKey(repository, HOOK_KEY)).thenReturn(repositoryHook);

        try {
            service.enable(repository, HOOK_KEY);
        } finally {
            verify(hookDao, never()).create(any(InternalRepositoryHook.class));
            verify(hookDao, never()).update(any(InternalRepositoryHook.class));
            verifyZeroInteractions(eventPublisher);
        }
    }

    @Test(expected = IllegalArgumentException.class)
    public void testEnableNonExistingHookKeyWithSettings() throws Exception {
        InternalRepositoryHook repositoryHook = createHook(HOOK_KEY, SETTINGS_LOB, false);
        when(hookDao.findByKey(repository, HOOK_KEY)).thenReturn(repositoryHook);

        when(settingsHelper.serialize(settings)).thenReturn(SERIALIZED_SETTINGS);

        try {
            service.enable(repository, HOOK_KEY, settings);
        } finally {
            verify(hookDao, never()).create(any(InternalRepositoryHook.class));
            verify(hookDao, never()).update(any(InternalRepositoryHook.class));
            verifyZeroInteractions(eventPublisher);
        }
    }

    @Test
    public void testEnableNonExistingHookWhenSettingsAreNotRequired() throws Exception {
        when(hookDao.findByKey(repository, HOOK_KEY)).thenReturn(null);

        createNonConfigurableModule(HOOK_KEY, RepositoryHookType.PRE_RECEIVE);

        service.enable(repository, HOOK_KEY);

        ArgumentCaptor<InternalRepositoryHook> hookCaptor = ArgumentCaptor.forClass(InternalRepositoryHook.class);
        verify(hookDao).create(hookCaptor.capture());

        InternalRepositoryHook capturedHook = hookCaptor.getValue();
        assertEquals(HOOK_KEY, capturedHook.getHookKey());
        assertEquals(repository, capturedHook.getRepository());
        assertEquals(null, capturedHook.getSettings());
        assertEquals(true, capturedHook.isEnabled());

        verify(eventPublisher).publish(argThat(repositoryHookEnabledMatcher()));
    }

    @Test(expected = IllegalEntityStateException.class)
    public void testEnableNonExistingHookWhenSettingsAreRequired() throws Exception {
        when(hookDao.findByKey(repository, HOOK_KEY)).thenReturn(null);

        createConfigurableModule(HOOK_KEY, RepositoryHookType.PRE_RECEIVE, HOOK_CONFIG_FORM_KEY);

        try {
            service.enable(repository, HOOK_KEY);
        } finally {
            verify(hookDao, never()).create(any(InternalRepositoryHook.class));
            verify(hookDao, never()).update(any(InternalRepositoryHook.class));
            verifyZeroInteractions(eventPublisher);
        }
    }

    @Test
    public void testEnableNonExistingHookWithSettings() throws Exception {
        when(hookDao.findByKey(repository, HOOK_KEY)).thenReturn(null);

        createConfigurableModule(HOOK_KEY, RepositoryHookType.PRE_RECEIVE, HOOK_CONFIG_FORM_KEY);

        when(settingsHelper.serialize(settings)).thenReturn(SERIALIZED_SETTINGS);

        service.enable(repository, HOOK_KEY, settings);

        ArgumentCaptor<InternalRepositoryHook> hookCaptor = ArgumentCaptor.forClass(InternalRepositoryHook.class);
        verify(hookDao).create(hookCaptor.capture());

        InternalRepositoryHook capturedHook = hookCaptor.getValue();
        assertEquals(HOOK_KEY, capturedHook.getHookKey());
        assertEquals(repository, capturedHook.getRepository());
        assertEquals(SERIALIZED_SETTINGS, capturedHook.getSettings().getData());
        assertEquals(true, capturedHook.isEnabled());

        verify(eventPublisher).publish(argThat(repositoryHookEnabledMatcher()));
        verify(eventPublisher).publish(argThat(repositoryHookSettingsChangedMatcher()));
    }

    @Test(expected = FormValidationException.class)
    public void testEnableWithSettingsAndValidationErrors() throws Exception {
        RepositoryHookModuleDescriptor moduleDescriptor =
                createConfigurableModule(HOOK_KEY, RepositoryHookType.PRE_RECEIVE, HOOK_CONFIG_FORM_KEY);
        when(moduleDescriptor.getValidator()).thenReturn(new RepositorySettingsValidator() {
            @Override
            public void validate(@Nonnull Settings settings, @Nonnull SettingsValidationErrors errors, @Nonnull Repository repository) {
                errors.addFieldError("some field", "some error");
            }
        });

        try {
            service.enable(repository, HOOK_KEY, settings);
        } finally {
            verifyZeroInteractions(hookDao, eventPublisher);
        }
    }

    @Test
    public void testFindAll() throws Exception {
        descriptors.add(createConfigurableModule(HOOK_KEY, RepositoryHookType.PRE_RECEIVE, HOOK_CONFIG_FORM_KEY));
        when(pluginAccessor.getEnabledModuleDescriptorsByClass(RepositoryHookModuleDescriptor.class))
                .thenReturn(descriptors);

        InternalRepositoryHook repositoryHook = createHook(HOOK_KEY, SETTINGS_LOB, false);
        when(hookDao.findEnabledByKey(repository, Sets.newHashSet(HOOK_KEY)))
                .thenReturn(Lists.newArrayList(repositoryHook));

        Page<RepositoryHook> pagesByType =
                service.findAll(repository, PageUtils.newRequest(0, 2));

        assertEquals(2, pagesByType.getLimit());
        assertEquals(1, pagesByType.getSize());
        assertTrue(pagesByType.getIsLastPage());

        Iterator<RepositoryHook> hooksIterator = pagesByType.getValues().iterator();
        assertTrue(hooksIterator.hasNext());

        RepositoryHook actualRepositoryHook = hooksIterator.next();
        RepositoryHookDetails actualDetails = actualRepositoryHook.getDetails();
        assertEquals(HOOK_CONFIG_FORM_KEY, actualDetails.getConfigFormKey());
        assertEquals(HOOK_KEY, actualDetails.getKey());
        assertEquals(HOOK_NAME, actualDetails.getName());
        assertEquals(HOOK_DESCRIPTION, actualDetails.getDescription());
        assertEquals(RepositoryHookType.PRE_RECEIVE, actualDetails.getType());
        assertTrue(actualRepositoryHook.isConfigured());
        assertFalse(actualRepositoryHook.isEnabled());

        assertFalse(hooksIterator.hasNext());
    }

    @Test
    public void testFindAllOverPageSize() throws Exception {
        createConfigurableModule(HOOK_KEY, RepositoryHookType.PRE_RECEIVE, HOOK_CONFIG_FORM_KEY);

        InternalRepositoryHook repositoryHook = createHook(HOOK_KEY, SETTINGS_LOB, false);
        when(hookDao.findEnabledByKey(same(repository), eq(Sets.newHashSet(HOOK_KEY))))
                .thenReturn(Lists.newArrayList(repositoryHook));

        Page<RepositoryHook> pagesByType =
                service.findAll(repository, PageUtils.newRequest(10, 2));

        verifyZeroInteractions(hookDao);

        assertEquals(0, pagesByType.getSize());
    }

    @Test
    public void testFindAllWithHooksWithoutSettings() throws Exception {
        descriptors.add(createConfigurableModule(HOOK_KEY, RepositoryHookType.PRE_RECEIVE, HOOK_CONFIG_FORM_KEY));
        when(pluginAccessor.getEnabledModuleDescriptorsByClass(RepositoryHookModuleDescriptor.class))
                .thenReturn(descriptors);

        when(hookDao.findEnabledByKey(repository, Sets.newHashSet(HOOK_KEY)))
                .thenReturn(Lists.newArrayList(createHook(HOOK_KEY, null, false)));

        Page<RepositoryHook> page = service.findAll(repository, PageUtils.newRequest(0, 2));

        assertEquals(2, page.getLimit());
        assertEquals(1, page.getSize());
        assertTrue(page.getIsLastPage());

        Iterator<RepositoryHook> hooksIterator = page.getValues().iterator();
        assertTrue(hooksIterator.hasNext());

        RepositoryHook actualRepositoryHook = hooksIterator.next();
        RepositoryHookDetails actualDetails = actualRepositoryHook.getDetails();
        assertEquals(HOOK_CONFIG_FORM_KEY, actualDetails.getConfigFormKey());
        assertEquals(HOOK_KEY, actualDetails.getKey());
        assertEquals(HOOK_NAME, actualDetails.getName());
        assertEquals(HOOK_DESCRIPTION, actualDetails.getDescription());
        assertEquals(RepositoryHookType.PRE_RECEIVE, actualDetails.getType());
        assertFalse(actualRepositoryHook.isConfigured());
        assertFalse(actualRepositoryHook.isEnabled());

        assertFalse(hooksIterator.hasNext());
    }

    @Test
    public void testFindByType() throws Exception {
        descriptors.add(createConfigurableModule(HOOK_KEY, RepositoryHookType.PRE_RECEIVE, HOOK_CONFIG_FORM_KEY));
        when(pluginAccessor.getEnabledModuleDescriptorsByClass(RepositoryHookModuleDescriptor.class))
                .thenReturn(descriptors);

        InternalRepositoryHook repositoryHook = createHook(HOOK_KEY, SETTINGS_LOB, false);
        when(hookDao.findEnabledByKey(repository, Sets.newHashSet(HOOK_KEY)))
                .thenReturn(Lists.newArrayList(repositoryHook));

        Page<RepositoryHook> pagesByType =
                service.findByType(repository, RepositoryHookType.PRE_RECEIVE, PageUtils.newRequest(0, 2));

        assertEquals(2, pagesByType.getLimit());
        assertEquals(1, pagesByType.getSize());
        assertTrue(pagesByType.getIsLastPage());

        Iterator<RepositoryHook> hooksIterator = pagesByType.getValues().iterator();
        assertTrue(hooksIterator.hasNext());

        RepositoryHook actualRepositoryHook = hooksIterator.next();
        RepositoryHookDetails actualDetails = actualRepositoryHook.getDetails();
        assertEquals(HOOK_CONFIG_FORM_KEY, actualDetails.getConfigFormKey());
        assertEquals(HOOK_KEY, actualDetails.getKey());
        assertEquals(HOOK_NAME, actualDetails.getName());
        assertEquals(HOOK_DESCRIPTION, actualDetails.getDescription());
        assertEquals(RepositoryHookType.PRE_RECEIVE, actualDetails.getType());
        assertTrue(actualRepositoryHook.isConfigured());
        assertFalse(actualRepositoryHook.isEnabled());

        assertFalse(hooksIterator.hasNext());
    }

    @Test
    public void testFindByTypeOrder() throws Exception {
        List<RepositoryHookModuleDescriptor> moduleDescriptors = createConfigurableModules(2);
        when(moduleDescriptors.get(0).getI18nName()).thenReturn("a - first module descriptor");
        when(moduleDescriptors.get(1).getI18nName()).thenReturn("b - second module descriptor");

        when(pluginAccessor.getEnabledModuleDescriptorsByClass(RepositoryHookModuleDescriptor.class))
                .thenReturn(moduleDescriptors);

        List<InternalRepositoryHook> repositoryHooks = createHooks(moduleDescriptors.get(0).getKey(),
                moduleDescriptors.get(1).getKey());

        when(hookDao.findEnabledByKey(same(repository), any(Set.class)))
                .thenAnswer(new AnswerFindHooksByKey(repositoryHooks));

        Page<RepositoryHook> pagesByType =
                service.findByType(repository, RepositoryHookType.POST_RECEIVE, PageUtils.newRequest(0, 2));

        assertEquals(2, pagesByType.getLimit());
        assertEquals(2, pagesByType.getSize());
        assertTrue(pagesByType.getIsLastPage());

        Iterator<RepositoryHook> hooksIterator = pagesByType.getValues().iterator();
        assertEquals("a - first module descriptor", hooksIterator.next().getDetails().getName());
        assertEquals("b - second module descriptor", hooksIterator.next().getDetails().getName());
    }

    private List<InternalRepositoryHook> createHooks(String... hookKeys) {
        List<InternalRepositoryHook> repositoryHooks = Lists.newArrayList();
        for (String hookKey : hookKeys) {
            repositoryHooks.add(createHook(hookKey, SETTINGS_LOB, true));
        }
        return repositoryHooks;
    }

    @Test
    public void testFindByTypeOverPageSize() throws Exception {
        createConfigurableModule(HOOK_KEY, RepositoryHookType.PRE_RECEIVE, HOOK_CONFIG_FORM_KEY);

        InternalRepositoryHook repositoryHook = createHook(HOOK_KEY, SETTINGS_LOB, false);
        when(hookDao.findEnabledByKey(same(repository), eq(Sets.newHashSet(HOOK_KEY))))
                .thenReturn(Lists.newArrayList(repositoryHook));

        Page<RepositoryHook> pagesByType =
                service.findByType(repository, RepositoryHookType.PRE_RECEIVE, PageUtils.newRequest(10, 2));

        verifyZeroInteractions(hookDao);

        assertEquals(0, pagesByType.getSize());
    }

    @Test
    public void testFindByTypeWithHooksWithoutSettings() throws Exception {
        List<RepositoryHookModuleDescriptor> moduleDescriptors = createConfigurableModules(2);
        when(moduleDescriptors.get(0).getType()).thenReturn(RepositoryHookType.POST_RECEIVE);
        when(moduleDescriptors.get(1).getType()).thenReturn(RepositoryHookType.PRE_RECEIVE);

        when(pluginAccessor.getEnabledModuleDescriptorsByClass(RepositoryHookModuleDescriptor.class))
                .thenReturn(moduleDescriptors);

        String[] hookKeys = {moduleDescriptors.get(0).getKey(), moduleDescriptors.get(1).getKey()};
        final List<InternalRepositoryHook> repositoryHooks = createHooks(hookKeys);

        when(hookDao.findEnabledByKey(same(repository), any(Set.class)))
                .thenAnswer(new AnswerFindHooksByKey(repositoryHooks));

        Page<RepositoryHook> pagesByType =
                service.findByType(repository, RepositoryHookType.POST_RECEIVE, PageUtils.newRequest(0, 2));

        assertEquals(2, pagesByType.getLimit());
        assertEquals(1, pagesByType.getSize());
        assertTrue(pagesByType.getIsLastPage());

        RepositoryHook actualRepositoryHook = pagesByType.getValues().iterator().next();
        RepositoryHookDetails actualDetails = actualRepositoryHook.getDetails();
        assertEquals(hookKeys[0], actualDetails.getKey());
    }

    @Test
    public void testFindByTypeWithoutHooksOfSameType() throws Exception {
        createConfigurableModule(HOOK_KEY, RepositoryHookType.PRE_RECEIVE, HOOK_CONFIG_FORM_KEY);

        InternalRepositoryHook repositoryHook = createHook(HOOK_KEY, SETTINGS_LOB, false);
        when(hookDao.findEnabledByKey(same(repository), eq(Sets.newHashSet(HOOK_KEY))))
                .thenReturn(Lists.newArrayList(repositoryHook));

        Page<RepositoryHook> pagesByType =
                service.findByType(repository, RepositoryHookType.POST_RECEIVE, PageUtils.newRequest(0, 2));

        verifyZeroInteractions(hookDao);

        assertEquals(0, pagesByType.getSize());
    }

    @Test
    public void testGetByKey() throws Exception {
        createConfigurableModule(HOOK_KEY, RepositoryHookType.PRE_RECEIVE, HOOK_CONFIG_FORM_KEY);

        InternalRepositoryHook repositoryHook = createHook(HOOK_KEY, SETTINGS_LOB, false);
        when(hookDao.findByKey(repository, HOOK_KEY)).thenReturn(repositoryHook);

        RepositoryHook actualRepositoryHook = service.getByKey(repository, HOOK_KEY);

        assertNotNull(actualRepositoryHook);
        RepositoryHookDetails actualDetails = actualRepositoryHook.getDetails();
        assertEquals(HOOK_CONFIG_FORM_KEY, actualDetails.getConfigFormKey());
        assertEquals(HOOK_KEY, actualDetails.getKey());
        assertEquals(HOOK_NAME, actualDetails.getName());
        assertEquals(HOOK_DESCRIPTION, actualDetails.getDescription());
        assertEquals(RepositoryHookType.PRE_RECEIVE, actualDetails.getType());
        assertTrue(actualRepositoryHook.isConfigured());
        assertFalse(actualRepositoryHook.isEnabled());
    }

    @Test
    public void testGetByKeyWithNothingInDb() throws Exception {
        createConfigurableModule(HOOK_KEY, RepositoryHookType.PRE_RECEIVE, HOOK_CONFIG_FORM_KEY);

        when(hookDao.findByKey(repository, HOOK_KEY)).thenReturn(null);

        RepositoryHook actualRepositoryHook = service.getByKey(repository, HOOK_KEY);

        assertNotNull(actualRepositoryHook);
        RepositoryHookDetails actualDetails = actualRepositoryHook.getDetails();
        assertEquals(HOOK_CONFIG_FORM_KEY, actualDetails.getConfigFormKey());
        assertEquals(HOOK_KEY, actualDetails.getKey());
        assertEquals(HOOK_NAME, actualDetails.getName());
        assertEquals(HOOK_DESCRIPTION, actualDetails.getDescription());
        assertEquals(RepositoryHookType.PRE_RECEIVE, actualDetails.getType());
        assertFalse(actualRepositoryHook.isConfigured());
        assertFalse(actualRepositoryHook.isEnabled());
    }

    @Test
    public void testGetByKeyForNonRepositoryHookModule() throws Exception {
        ModuleDescriptor nonHookDescriptor = mock(DownloadStrategyModuleDescriptor.class);
        when(pluginAccessor.getEnabledPluginModule(HOOK_KEY)).thenReturn(nonHookDescriptor);

        assertNull(service.getByKey(repository, HOOK_KEY));

        verifyZeroInteractions(hookDao);
    }

    @Test
    public void testGetIcon() throws Exception {
        checkGetIcon(HOOK_ICON_FILE, "image/" + getClass().getSimpleName() + ".class");
    }

    @Test
    public void testGetIconDefault() throws Exception {
        checkGetIcon(null, "image/icon-default-hook.png");
    }

    @Test
    public void testGetIconDefaultNotFound() throws Exception {
        checkGetIcon("bad.png", "image/icon-default-hook.png");
    }

    @Test
    public void testGetSettings() throws Exception {
        InternalRepositoryHook repositoryHook = createHook(HOOK_KEY, SETTINGS_LOB, false);
        when(hookDao.findByKey(repository, HOOK_KEY)).thenReturn(repositoryHook);
        when(settingsHelper.deserialize(SERIALIZED_SETTINGS)).thenReturn(settings);

        assertSame(settings, service.getSettings(repository, HOOK_KEY));
    }

    @Test
    public void testGetSettingsWhenHookDoesNotExist() throws Exception {
        assertNull(service.getSettings(repository, HOOK_KEY));
    }

    @Test
    public void testGetSettingsWhenNoSettingsInDb() throws Exception {
        InternalRepositoryHook repositoryHook = createHook(HOOK_KEY, null, false);
        when(hookDao.findByKey(repository, HOOK_KEY)).thenReturn(repositoryHook);
        when(settingsHelper.deserialize(SERIALIZED_SETTINGS)).thenReturn(settings);

        assertNull(service.getSettings(repository, HOOK_KEY));
    }

    @Test
    public void testSetSettings() throws Exception {
        createConfigurableModule(HOOK_KEY, RepositoryHookType.PRE_RECEIVE, HOOK_CONFIG_FORM_KEY);
        InternalRepositoryHook repositoryHook = createHook(HOOK_KEY, SETTINGS_LOB, false);
        when(hookDao.findByKey(repository, HOOK_KEY)).thenReturn(repositoryHook);

        when(settingsHelper.serialize(settings)).thenReturn(SERIALIZED_SETTINGS);

        assertSame(settings, service.setSettings(repository, HOOK_KEY, settings));

        ArgumentCaptor<InternalRepositoryHook> hookCaptor = ArgumentCaptor.forClass(InternalRepositoryHook.class);

        verify(hookDao).update(hookCaptor.capture());

        InternalRepositoryHook actualHook = hookCaptor.getValue();
        assertEquals(HOOK_KEY, actualHook.getHookKey());
        assertEquals(repository, actualHook.getRepository());
        assertEquals(SETTINGS_ID, actualHook.getSettings().getId());
        assertEquals(SERIALIZED_SETTINGS, actualHook.getSettings().getData());
        assertEquals(false, actualHook.isEnabled());
    }

    @Test
    public void testSetSettingsForNonExistingHook() throws Exception {
        createConfigurableModule(HOOK_KEY, RepositoryHookType.PRE_RECEIVE, HOOK_CONFIG_FORM_KEY);

        when(hookDao.findByKey(repository, HOOK_KEY)).thenReturn(null);

        when(settingsHelper.serialize(settings)).thenReturn(SERIALIZED_SETTINGS);

        assertSame(settings, service.setSettings(repository, HOOK_KEY, settings));

        ArgumentCaptor<InternalRepositoryHook> hookCaptor = ArgumentCaptor.forClass(InternalRepositoryHook.class);

        verify(hookDao).create(hookCaptor.capture());

        InternalRepositoryHook actualHook = hookCaptor.getValue();
        assertEquals(HOOK_KEY, actualHook.getHookKey());
        assertEquals(repository, actualHook.getRepository());
        assertEquals(SERIALIZED_SETTINGS, actualHook.getSettings().getData());
        assertEquals(false, actualHook.isEnabled());
    }

    @Test(expected = IllegalArgumentException.class)
    public void testSetSettingsForNonValidatorModuleDescriptor() {
        when(pluginAccessor.getEnabledPluginModule(HOOK_KEY)).thenReturn(mock(WebFragmentModuleDescriptor.class));

        try {
            service.setSettings(repository, HOOK_KEY, settings);
        } finally {
            verifyZeroInteractions(hookDao);
        }
    }

    @Test(expected = FormValidationException.class)
    public void testSetSettingsWithValidationErrors() throws Exception {
        RepositoryHookModuleDescriptor moduleDescriptor =
                createConfigurableModule(HOOK_KEY, RepositoryHookType.PRE_RECEIVE, HOOK_CONFIG_FORM_KEY);

        when(moduleDescriptor.getValidator()).thenReturn(new RepositorySettingsValidator() {
            @Override
            public void validate(@Nonnull Settings settings, @Nonnull SettingsValidationErrors errors, @Nonnull Repository repository) {
                errors.addFieldError("some field", "some error");
            }
        });

        try {
            service.setSettings(repository, HOOK_KEY, settings);
        } finally {
            verifyZeroInteractions(hookDao, eventPublisher);
        }
    }

    @Test
    public void testVisitEnabledHooks() {
        final String notConfiguredKey = "hook key 002";
        descriptors.add(createConfigurableModule(HOOK_KEY, RepositoryHookType.PRE_RECEIVE, HOOK_CONFIG_FORM_KEY));
        descriptors.add(createConfigurableModule(notConfiguredKey, RepositoryHookType.PRE_RECEIVE, HOOK_CONFIG_FORM_KEY));
        when(pluginAccessor.getEnabledModuleDescriptorsByClass(RepositoryHookModuleDescriptor.class))
                .thenReturn(descriptors);

        final List<String> hookKeys = Lists.newArrayList(HOOK_KEY, notConfiguredKey);

        List<InternalRepositoryHook> configuredHooks = Lists.newArrayList(createHook(HOOK_KEY, SETTINGS_LOB, true));
        when(settingsHelper.deserialize(SERIALIZED_SETTINGS)).thenReturn(settings);

        List<InternalRepositoryHook> repositoryHooks = Lists.newArrayList(configuredHooks);
        repositoryHooks.add(createHook(notConfiguredKey, null, true));

        when(hookDao.findEnabledByKey(same(repository), eq(hookKeys))).thenReturn(repositoryHooks);

        final Map<String, Settings> visitedHooksByKey = Maps.newHashMap();
        assertTrue(service.visitEnabledHooks(repository, PreReceiveRepositoryHook.class, new RepositoryHookSettingsCallback<PreReceiveRepositoryHook>() {
            @Override
            public boolean visit(@Nonnull String key, @Nonnull PreReceiveRepositoryHook hook, @Nonnull Settings settings) {
                visitedHooksByKey.put(key, settings);
                return hookKeys.contains(key);
            }
        }));

        assertEquals(2, visitedHooksByKey.size());
        assertEquals(settings, visitedHooksByKey.get(HOOK_KEY));
        assertNotNull(visitedHooksByKey.get(notConfiguredKey));
    }

    @Test
    public void testVisitEnabledHooksOnPageEdge() {
        List<RepositoryHookModuleDescriptor> moduleDescriptors = createConfigurableModules(PAGE_SETTINGS_LIMIT + 1);

        final Map<String, InternalRepositoryHook> repositoryHooksByKey = Maps.newHashMap();

        for (RepositoryHookModuleDescriptor descriptor : moduleDescriptors) {
            String hookKey = descriptor.getCompleteKey();
            repositoryHooksByKey.put(hookKey, createHook(hookKey, SETTINGS_LOB, true));
        }

        when(hookDao.findEnabledByKey(same(repository), any(List.class)))
                .thenAnswer(new AnswerFindByKey(repositoryHooksByKey));

        when(settingsHelper.deserialize(SERIALIZED_SETTINGS)).thenReturn(settings);

        visitEnabledHooks(moduleDescriptors.subList(0, PAGE_SETTINGS_LIMIT - 1), repositoryHooksByKey.keySet());
        visitEnabledHooks(moduleDescriptors.subList(0, PAGE_SETTINGS_LIMIT), repositoryHooksByKey.keySet());
        visitEnabledHooks(moduleDescriptors.subList(0, PAGE_SETTINGS_LIMIT + 1), repositoryHooksByKey.keySet());
    }

    @Test
    public void testVisitEnabledHooksWithExit() {
        final String notConfiguredKey = "hook key 002";
        descriptors.add(createConfigurableModule(HOOK_KEY, RepositoryHookType.PRE_RECEIVE, HOOK_CONFIG_FORM_KEY));
        descriptors.add(createConfigurableModule(notConfiguredKey, RepositoryHookType.PRE_RECEIVE, HOOK_CONFIG_FORM_KEY));
        when(pluginAccessor.getEnabledModuleDescriptorsByClass(RepositoryHookModuleDescriptor.class))
                .thenReturn(descriptors);

        final List<String> hookKeys = Lists.newArrayList(HOOK_KEY, notConfiguredKey);

        List<InternalRepositoryHook> configuredHooks = Lists.newArrayList(createHook(HOOK_KEY, SETTINGS_LOB, true));
        when(settingsHelper.deserialize(SERIALIZED_SETTINGS)).thenReturn(settings);

        List<InternalRepositoryHook> repositoryHooks = Lists.newArrayList(configuredHooks);
        repositoryHooks.add(createHook(notConfiguredKey, null, true));

        when(hookDao.findEnabledByKey(same(repository), eq(hookKeys))).thenReturn(repositoryHooks);

        final Map<String, Settings> visitedHooksByKey = Maps.newHashMap();
        assertFalse(service.visitEnabledHooks(repository, PreReceiveRepositoryHook.class, new RepositoryHookSettingsCallback<PreReceiveRepositoryHook>() {
            @Override
            public boolean visit(@Nonnull String key, @Nonnull PreReceiveRepositoryHook hook, @Nonnull Settings settings) {
                visitedHooksByKey.put(key, settings);
                return false;
            }
        }));

        assertEquals(1, visitedHooksByKey.size());
        assertEquals(settings, visitedHooksByKey.get(HOOK_KEY));
    }

    private void checkGetIcon(String icon, String expected) throws IOException {
        RepositoryHookModuleDescriptor module =
                createConfigurableModule(HOOK_KEY, RepositoryHookType.PRE_RECEIVE, HOOK_CONFIG_FORM_KEY);

        when(module.getIconPath()).thenReturn(icon);
        when(pluginAccessor.getEnabledPluginModule(module.getCompleteKey())).thenReturn((ModuleDescriptor) module);

        CacheableAvatarSupplier avatar = service.getAvatar(module.getCompleteKey());

        assertNotNull(avatar.open());
        assertTrue(avatar.getTimestamp() > 0);
        assertEquals(expected, avatar.getContentType());
    }

    @SuppressWarnings("unchecked")
    private RepositoryHookModuleDescriptor createConfigurableModule(String hookKey,
                                                                    RepositoryHookType type,
                                                                    String configFormKey) {
        RepositoryHookModuleDescriptor moduleDescriptor = mock(RepositoryHookModuleDescriptor.class);
        when(moduleDescriptor.getKey()).thenReturn(hookKey);
        when(moduleDescriptor.getCompleteKey()).thenReturn(hookKey);
        when(moduleDescriptor.getI18nName()).thenReturn(HOOK_NAME);
        when(moduleDescriptor.getType()).thenReturn(type);
        when(moduleDescriptor.getI18nDescription()).thenReturn(HOOK_DESCRIPTION);
        when(moduleDescriptor.getPlugin()).thenReturn(plugin);
        when(moduleDescriptor.getConfigFormKey()).thenReturn(configFormKey);
        when(moduleDescriptor.getModuleClass()).thenReturn((Class) PreReceiveRepositoryHook.class);

        // We just need a file that exists in the test jar - pick one any one
        when(plugin.getResource(HOOK_ICON_FILE)).thenReturn(getClass().getResource(getClass().getSimpleName() + ".class"));

        when(pluginAccessor.getEnabledPluginModule(hookKey)).thenReturn((ModuleDescriptor) moduleDescriptor);

        return moduleDescriptor;
    }

    private List<RepositoryHookModuleDescriptor> createConfigurableModules(int hooksCount) {
        List<RepositoryHookModuleDescriptor> moduleDescriptors = Lists.newArrayList();
        for (int i = 0; i < hooksCount; i++) {
            moduleDescriptors.add(createConfigurableModule(String.format("hook key %03d", i),
                    RepositoryHookType.POST_RECEIVE, HOOK_CONFIG_FORM_KEY));
        }
        return moduleDescriptors;
    }

    private InternalRepositoryHook createHook(String hookKey, InternalSharedLob settings, boolean enabled) {
        InternalRepositoryHook repositoryHook = new InternalRepositoryHook.Builder()
                .repository(repository)
                .hookKey(hookKey)
                .settings(settings)
                .enabled(enabled)
                .build();

        if (enabled) {
            enabledHooks.add(repositoryHook);
        }

        return repositoryHook;
    }

    private void createNonConfigurableModule(String hookKey, RepositoryHookType type) {
        createConfigurableModule(hookKey, type, null);
    }

    private Matcher<RepositoryHookDisabledEvent> repositoryHookDisabledMatcher() {
        return new TypeSafeMatcher<RepositoryHookDisabledEvent>() {
            @Override
            protected boolean matchesSafely(RepositoryHookDisabledEvent item) {
                return repositoryHookEventMatcher().matches(item);
            }

            @Override
            public void describeTo(Description description) {
                // NO OP
            }
        };
    }

    private Matcher<RepositoryHookEnabledEvent> repositoryHookEnabledMatcher() {
        return new TypeSafeMatcher<RepositoryHookEnabledEvent>() {
            @Override
            protected boolean matchesSafely(RepositoryHookEnabledEvent item) {
                return repositoryHookEventMatcher().matches(item);
            }

            @Override
            public void describeTo(Description description) {
                // NO OP
            }
        };
    }

    private Matcher<RepositoryHookEvent> repositoryHookEventMatcher() {
        return new TypeSafeMatcher<RepositoryHookEvent>() {
            @Override
            protected boolean matchesSafely(RepositoryHookEvent item) {
                return item.getRepository() == repository && item.getSource() == service
                        && HOOK_KEY.equals(item.getRepositoryHookKey());
            }

            @Override
            public void describeTo(Description description) {
                // NO OP
            }
        };
    }

    private Matcher<RepositoryHookSettingsChangedEvent> repositoryHookSettingsChangedMatcher() {
        return new TypeSafeMatcher<RepositoryHookSettingsChangedEvent>() {
            @Override
            protected boolean matchesSafely(RepositoryHookSettingsChangedEvent item) {
                return repositoryHookEventMatcher().matches(item) && HOOK_KEY.equals(item.getRepositoryHookKey());
            }

            @Override
            public void describeTo(Description description) {
                // NO OP
            }
        };
    }

    private void visitEnabledHooks(List<RepositoryHookModuleDescriptor> moduleDescriptors, final Set<String> hookKeys) {
        when(pluginAccessor.getEnabledModuleDescriptorsByClass(RepositoryHookModuleDescriptor.class))
                .thenReturn(moduleDescriptors);

        final Map<String, Settings> visitedHooksByKey = Maps.newHashMap();
        assertTrue(service.visitEnabledHooks(repository, PreReceiveRepositoryHook.class, new RepositoryHookSettingsCallback<PreReceiveRepositoryHook>() {
            @Override
            public boolean visit(@Nonnull String key, @Nonnull PreReceiveRepositoryHook hook, @Nonnull Settings settings) {
                visitedHooksByKey.put(key, settings);
                return hookKeys.contains(key);
            }
        }));

        assertEquals(moduleDescriptors.size(), visitedHooksByKey.size());
    }

    private static final class AnswerFindHooksByKey  implements Answer<List<InternalRepositoryHook>> {

        private List<InternalRepositoryHook> repositoryHooks;

        private AnswerFindHooksByKey(List<InternalRepositoryHook> repositoryHooks) {
            this.repositoryHooks = repositoryHooks;
        }

        @Override
        public List<InternalRepositoryHook> answer(InvocationOnMock invocation) throws Throwable {
            Collection<String> hookKeys = (Collection<String>) invocation.getArguments()[1];
            List<InternalRepositoryHook> matchingHooks = Lists.newArrayList();
            for (InternalRepositoryHook hook : repositoryHooks) {
                if (hookKeys.contains(hook.getHookKey())) {
                    matchingHooks.add(hook);
                }
            }
            return matchingHooks;
        }
    }

    private static final class AnswerFindByKey implements Answer<List<InternalRepositoryHook>> {

        private Map<String, InternalRepositoryHook> hooksByKey;

        private AnswerFindByKey(Map<String, InternalRepositoryHook> hooksByKey) {
            this.hooksByKey = hooksByKey;
        }

        @Override
        public List<InternalRepositoryHook> answer(InvocationOnMock invocation) throws Throwable {
            Collection<String> hookKeys = (Collection<String>) invocation.getArguments()[1];
            List<InternalRepositoryHook> repositoryHooks = Lists.newArrayList();
            for (String hookKey : hookKeys) {
                if (hooksByKey.containsKey(hookKey)) {
                    repositoryHooks.add(hooksByKey.get(hookKey));
                }
            }
            return repositoryHooks;
        }
    }
}
