package com.atlassian.stash.internal.merge;

import com.atlassian.bitbucket.i18n.I18nService;
import com.atlassian.bitbucket.i18n.SimpleI18nService;
import com.atlassian.bitbucket.scm.FeatureUnsupportedScmException;
import com.atlassian.bitbucket.scm.PluginMergeStrategies;
import com.atlassian.bitbucket.scm.PluginMergeStrategy;
import com.atlassian.bitbucket.validation.ArgumentValidationException;
import com.atlassian.stash.internal.project.InternalNormalProject;
import com.atlassian.stash.internal.repository.InternalRepository;
import com.atlassian.stash.internal.scm.InternalScmService;
import com.google.common.collect.ImmutableSet;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.Spy;
import org.mockito.runners.MockitoJUnitRunner;

import static java.util.stream.Collectors.toSet;
import static org.junit.Assert.*;
import static org.mockito.Matchers.eq;
import static org.mockito.Matchers.same;
import static org.mockito.Mockito.*;

@RunWith(MockitoJUnitRunner.class)
@SuppressWarnings("ConstantConditions")
public class DefaultMergeConfigHelperTest {

    private static final InternalNormalProject PROJECT = new InternalNormalProject.Builder()
            .id(1)
            .name("Key")
            .build();
    private static final InternalRepository REPOSITORY = new InternalRepository.Builder()
            .id(1)
            .name("slug")
            .project(PROJECT)
            .scmId("scm")
            .build();

    @InjectMocks
    private DefaultMergeConfigHelper helper;
    @Spy
    private I18nService i18nService = new SimpleI18nService();
    @Mock
    private ProjectMergeConfigDao projectConfigDao;
    @Mock
    private RepositoryMergeConfigDao repositoryConfigDao;
    @Mock
    private ScmMergeConfigDao scmConfigDao;
    @Mock
    private InternalScmService scmService;

    @Before
    public void setup() {
        PluginMergeStrategy first = mockStrategy("first");

        PluginMergeStrategy second = mockStrategy("second");
        when(second.isDefault()).thenReturn(true);

        PluginMergeStrategies strategies = mock(PluginMergeStrategies.class);
        when(strategies.getStrategies()).thenReturn(ImmutableSet.of(first, second));

        when(scmService.getMergeStrategies(eq("scm"))).thenReturn(strategies);
    }

    @Test
    public void testGetForProjectAndScm() {
        InternalProjectMergeConfig daoConfig = mock(InternalProjectMergeConfig.class);
        when(daoConfig.getDefaultStrategyId()).thenReturn("first");
        when(daoConfig.getStrategyIds()).thenReturn(ImmutableSet.of("first"));
        when(daoConfig.getType()).thenReturn(MergeConfigType.PROJECT);

        when(projectConfigDao.findByProjectAndScm(eq(1), eq("scm"))).thenReturn(daoConfig);

        MergeConfig config = helper.getForProjectAndScm(PROJECT, "scm");
        assertConfigured(config, MergeConfigType.PROJECT);
    }

    @Test
    public void testGetForProjectAndScmConfiguredByScm() {
        InternalScmMergeConfig daoConfig = mock(InternalScmMergeConfig.class);
        when(daoConfig.getDefaultStrategyId()).thenReturn("first");
        when(daoConfig.getStrategyIds()).thenReturn(ImmutableSet.of("first"));
        when(daoConfig.getType()).thenReturn(MergeConfigType.SCM);

        when(scmConfigDao.findByScm(eq("scm"))).thenReturn(daoConfig);

        MergeConfig config = helper.getForProjectAndScm(PROJECT, "scm");
        assertConfigured(config, MergeConfigType.SCM);
    }

    @Test
    public void testGetForProjectAndScmUnconfigured() {
        MergeConfig config = helper.getForProjectAndScm(PROJECT, "scm");
        assertUnconfigured(config);
    }

    @Test(expected = NullPointerException.class)
    public void testGetForProjectAndScmWithNullProject() {
        helper.getForProjectAndScm(null, "scm");
    }

    @Test(expected = NullPointerException.class)
    public void testGetForProjectAndScmWithNullScm() {
        helper.getForProjectAndScm(PROJECT, null);
    }

    @Test
    public void testGetForRepository() {
        InternalRepositoryMergeConfig daoConfig = mock(InternalRepositoryMergeConfig.class);
        when(daoConfig.getDefaultStrategyId()).thenReturn("first");
        when(daoConfig.getStrategyIds()).thenReturn(ImmutableSet.of("first"));
        when(daoConfig.getType()).thenReturn(MergeConfigType.REPOSITORY);

        when(repositoryConfigDao.findByRepository(eq(1))).thenReturn(daoConfig);

        MergeConfig config = helper.getForRepository(REPOSITORY);
        assertConfigured(config, MergeConfigType.REPOSITORY);
    }

    @Test
    public void testGetForRepositoryConfiguredByProject() {
        InternalProjectMergeConfig daoConfig = mock(InternalProjectMergeConfig.class);
        when(daoConfig.getDefaultStrategyId()).thenReturn("first");
        when(daoConfig.getStrategyIds()).thenReturn(ImmutableSet.of("first"));
        when(daoConfig.getType()).thenReturn(MergeConfigType.PROJECT);

        when(projectConfigDao.findByProjectAndScm(eq(1), eq("scm"))).thenReturn(daoConfig);

        MergeConfig config = helper.getForRepository(REPOSITORY);
        assertConfigured(config, MergeConfigType.PROJECT);
    }

    @Test
    public void testGetForRepositoryConfiguredByScm() {
        InternalScmMergeConfig daoConfig = mock(InternalScmMergeConfig.class);
        when(daoConfig.getDefaultStrategyId()).thenReturn("first");
        when(daoConfig.getStrategyIds()).thenReturn(ImmutableSet.of("first"));
        when(daoConfig.getType()).thenReturn(MergeConfigType.SCM);

        when(scmConfigDao.findByScm(eq("scm"))).thenReturn(daoConfig);

        MergeConfig config = helper.getForRepository(REPOSITORY);
        assertConfigured(config, MergeConfigType.SCM);
    }

    @Test
    public void testGetForRepositoryFiltersStrategiesByAvailability() {
        InternalRepositoryMergeConfig daoConfig = mock(InternalRepositoryMergeConfig.class);
        when(daoConfig.getDefaultStrategyId()).thenReturn("third");
        when(daoConfig.getStrategyIds()).thenReturn(ImmutableSet.of("first", "second", "third"));
        when(daoConfig.getType()).thenReturn(MergeConfigType.REPOSITORY);

        when(repositoryConfigDao.findByRepository(eq(1))).thenReturn(daoConfig);

        MergeConfig config = helper.getForRepository(REPOSITORY);
        assertNotNull(config);
        assertEquals(ImmutableSet.of("first", "second"),
                config.getAvailableStrategies().stream().map(PluginMergeStrategy::getId).collect(toSet()));
        //The unavailable default should be replaced by the first enabled strategy still available
        assertEquals("first", config.getDefaultStrategyId());
        //The unavailable enabled strategy should be removed
        assertEquals(ImmutableSet.of("first", "second"), config.getEnabledStrategyIds());
        assertSame(MergeConfigType.REPOSITORY, config.getType());
        assertTrue(config.isAvailable("first"));
        assertTrue(config.isAvailable("second"));
        assertFalse(config.isAvailable("third"));
        assertTrue(config.isEnabled("first"));
        assertTrue(config.isEnabled("second"));
        assertFalse(config.isEnabled("third"));
    }

    @Test
    public void testGetForRepositoryFiltersStrategiesByAvailabilityAndEnablesNewDefaults() {
        InternalRepositoryMergeConfig daoConfig = mock(InternalRepositoryMergeConfig.class);
        when(daoConfig.getDefaultStrategyId()).thenReturn("third");
        when(daoConfig.getStrategyIds()).thenReturn(ImmutableSet.of("third"));
        when(daoConfig.getType()).thenReturn(MergeConfigType.REPOSITORY);

        when(repositoryConfigDao.findByRepository(eq(1))).thenReturn(daoConfig);

        MergeConfig config = helper.getForRepository(REPOSITORY);
        assertNotNull(config);
        assertEquals(ImmutableSet.of("first", "second"),
                config.getAvailableStrategies().stream().map(PluginMergeStrategy::getId).collect(toSet()));
        //The unavailable default should be replaced by the SCM's default if no other enabled
        //strategy is still available
        assertEquals("second", config.getDefaultStrategyId());
        //The unavailable enabled strategy should be removed and the SCM's default should be enabled
        assertEquals(ImmutableSet.of("second"), config.getEnabledStrategyIds());
        assertSame(MergeConfigType.REPOSITORY, config.getType());
        assertTrue(config.isAvailable("first"));
        assertTrue(config.isAvailable("second"));
        assertFalse(config.isAvailable("third"));
        assertFalse(config.isEnabled("first"));
        assertTrue(config.isEnabled("second"));
    }

    @Test
    public void testGetForRepositoryUnconfigured() {
        MergeConfig config = helper.getForRepository(REPOSITORY);
        assertUnconfigured(config);
    }

    @Test(expected = NullPointerException.class)
    public void testGetForRepositoryWithNull() {
        helper.getForRepository(null);
    }

    @Test
    public void testGetForScm() {
        InternalScmMergeConfig daoConfig = mock(InternalScmMergeConfig.class);
        when(daoConfig.getDefaultStrategyId()).thenReturn("first");
        when(daoConfig.getStrategyIds()).thenReturn(ImmutableSet.of("first"));
        when(daoConfig.getType()).thenReturn(MergeConfigType.SCM);

        when(scmConfigDao.findByScm(eq("scm"))).thenReturn(daoConfig);

        MergeConfig config = helper.getForScm("scm");
        assertConfigured(config, MergeConfigType.SCM);
    }

    @Test(expected = NullPointerException.class)
    public void testGetForScmWithNull() {
        helper.getForScm(null);
    }

    @Test
    public void testGetForScmUnconfigured() {
        MergeConfig config = helper.getForScm("scm");
        assertUnconfigured(config);
    }

    @Test
    public void testSetForProjectAndScmCreatesNew() {
        helper.setForProjectAndScm(PROJECT, "scm", "first", ImmutableSet.of("first", "second"));

        ArgumentCaptor<InternalProjectMergeConfig> configCaptor = ArgumentCaptor.forClass(InternalProjectMergeConfig.class);

        verify(projectConfigDao).findByProjectAndScm(eq(1), eq("scm"));
        verify(projectConfigDao).create(configCaptor.capture());

        InternalProjectMergeConfig config = configCaptor.getValue();
        assertNotNull(config);
        assertEquals("first", config.getDefaultStrategyId());
        assertEquals("scm", config.getScmId());
        assertEquals(ImmutableSet.of("first", "second"), config.getStrategyIds());
        assertSame(PROJECT.getId(), config.getProjectId());
    }

    @Test
    public void testSetForProjectAndScmUpdatesExisting() {
        InternalProjectMergeConfig config = mock(InternalProjectMergeConfig.class);

        when(projectConfigDao.findByProjectAndScm(eq(1), eq("scm"))).thenReturn(config);

        helper.setForProjectAndScm(PROJECT, "scm", "first", ImmutableSet.of("first", "second"));

        verify(config).setDefaultStrategyId(eq("first"));
        verify(config).setStrategyIds(eq(ImmutableSet.of("first", "second")));
        verify(projectConfigDao).findByProjectAndScm(eq(1), eq("scm"));
        verify(projectConfigDao).update(same(config));
    }

    @Test(expected = NullPointerException.class)
    public void testSetForProjectAndScmWithNullProject() {
        helper.setForProjectAndScm(null, "scm", "first", ImmutableSet.of("first"));
    }

    @Test
    public void testSetForRepositoryCreatesNew() {
        helper.setForRepository(REPOSITORY, "second", ImmutableSet.of("second"));

        ArgumentCaptor<InternalRepositoryMergeConfig> configCaptor = ArgumentCaptor.forClass(InternalRepositoryMergeConfig.class);

        verify(repositoryConfigDao).findByRepository(eq(1));
        verify(repositoryConfigDao).create(configCaptor.capture());

        InternalRepositoryMergeConfig config = configCaptor.getValue();
        assertNotNull(config);
        assertEquals("second", config.getDefaultStrategyId());
        assertEquals(ImmutableSet.of("second"), config.getStrategyIds());
        assertSame(REPOSITORY.getId(), config.getRepositoryId());
    }

    @Test
    public void testSetForRepositoryUpdatesExisting() {
        InternalRepositoryMergeConfig config = mock(InternalRepositoryMergeConfig.class);

        when(repositoryConfigDao.findByRepository(eq(1))).thenReturn(config);

        helper.setForRepository(REPOSITORY, "second", ImmutableSet.of("second"));

        verify(config).setDefaultStrategyId(eq("second"));
        verify(config).setStrategyIds(eq(ImmutableSet.of("second")));
        verify(repositoryConfigDao).findByRepository(eq(1));
        verify(repositoryConfigDao).update(same(config));
    }

    @Test(expected = NullPointerException.class)
    public void testSetForRepositoryWithRepository() {
        helper.setForRepository(null, "first", ImmutableSet.of("first"));
    }

    @Test
    public void testSetForScmExistingCreatesNew() {
        helper.setForScm("scm", "first", ImmutableSet.of("first"));

        ArgumentCaptor<InternalScmMergeConfig> configCaptor = ArgumentCaptor.forClass(InternalScmMergeConfig.class);

        verify(scmConfigDao).findByScm(eq("scm"));
        verify(scmConfigDao).create(configCaptor.capture());

        InternalScmMergeConfig config = configCaptor.getValue();
        assertNotNull(config);
        assertEquals("first", config.getDefaultStrategyId());
        assertEquals("scm", config.getScmId());
        assertEquals(ImmutableSet.of("first"), config.getStrategyIds());
    }

    @Test(expected = FeatureUnsupportedScmException.class)
    public void testSetForScmPropagatesFeatureUnsupported() {
        doThrow(FeatureUnsupportedScmException.class).when(scmService).getMergeStrategies(eq("git"));

        try {
            helper.setForScm("git", "ff-only", ImmutableSet.of("no-ff", "ff-only"));
        } finally {
            verify(scmService).getMergeStrategies(eq("git"));
        }
    }

    @Test
    public void testSetForScmUpdatesExisting() {
        InternalScmMergeConfig config = mock(InternalScmMergeConfig.class);

        when(scmConfigDao.findByScm(eq("scm"))).thenReturn(config);

        helper.setForScm("scm", "first", ImmutableSet.of("first"));

        verify(config).setDefaultStrategyId(eq("first"));
        verify(config).setStrategyIds(eq(ImmutableSet.of("first")));
        verify(scmConfigDao).findByScm(eq("scm"));
        verify(scmConfigDao).update(same(config));
    }

    @Test(expected = ArgumentValidationException.class)
    public void testSetForScmWithDefaultStrategyIdNotEnabled() {
        helper.setForScm("scm", "second", ImmutableSet.of("first"));
    }

    @Test(expected = ArgumentValidationException.class)
    public void testSetForScmWithEnabledStrategyNotSupported() {
        helper.setForScm("scm", "first", ImmutableSet.of("first", "third"));
    }

    @Test(expected = NullPointerException.class)
    public void testSetForScmWithNullDefaultStrategyId() {
        helper.setForScm("scm", null, ImmutableSet.of("first"));
    }

    @Test(expected = NullPointerException.class)
    public void testSetForScmWithNullScmId() {
        helper.setForScm(null, "first", ImmutableSet.of("first"));
    }

    @Test(expected = NullPointerException.class)
    public void testSetForScmWithNullStrategies() {
        helper.setForScm("scm", "first", null);
    }

    private static void assertConfigured(MergeConfig config, MergeConfigType expectedType) {
        assertNotNull(config);
        assertEquals(ImmutableSet.of("first", "second"),
                config.getAvailableStrategies().stream().map(PluginMergeStrategy::getId).collect(toSet()));
        assertEquals("first", config.getDefaultStrategyId());
        assertEquals(ImmutableSet.of("first"), config.getEnabledStrategyIds());
        assertSame(expectedType, config.getType());
        assertTrue(config.isAvailable("first"));
        assertTrue(config.isAvailable("second"));
        assertFalse(config.isAvailable("third"));
        assertTrue(config.isEnabled("first"));
        assertFalse(config.isEnabled("second"));
    }

    private static void assertUnconfigured(MergeConfig config) {
        assertNotNull(config);
        assertEquals(ImmutableSet.of("first", "second"),
                config.getAvailableStrategies().stream().map(PluginMergeStrategy::getId).collect(toSet()));
        assertEquals("second", config.getDefaultStrategyId());
        assertEquals(ImmutableSet.of("second"), config.getEnabledStrategyIds());
        assertSame(MergeConfigType.DEFAULT, config.getType());
        assertTrue(config.isAvailable("first"));
        assertTrue(config.isAvailable("second"));
        assertFalse(config.isAvailable("third"));
        assertFalse(config.isEnabled("first"));
        assertTrue(config.isEnabled("second"));
    }

    private static PluginMergeStrategy mockStrategy(String id) {
        PluginMergeStrategy strategy = mock(PluginMergeStrategy.class);
        when(strategy.getId()).thenReturn(id);

        return strategy;
    }
}