package com.atlassian.stash.internal.pull;

import com.atlassian.plugin.PluginAccessor;
import com.atlassian.bitbucket.i18n.I18nService;
import com.atlassian.bitbucket.i18n.SimpleI18nService;
import com.atlassian.bitbucket.mockito.MockitoUtils;
import com.atlassian.bitbucket.pull.PullRequestMergeVeto;
import com.atlassian.bitbucket.pull.PullRequestMergeability;
import com.atlassian.bitbucket.scm.Command;
import com.atlassian.bitbucket.scm.ScmService;
import com.atlassian.bitbucket.scm.pull.MergeRequest;
import com.atlassian.bitbucket.scm.pull.MergeRequestCheck;
import com.atlassian.bitbucket.scm.pull.MergeRequestCheckModuleDescriptor;
import com.atlassian.bitbucket.scm.pull.ScmPullRequestCommandFactory;
import com.google.common.base.Function;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
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.invocation.InvocationOnMock;
import org.mockito.runners.MockitoJUnitRunner;
import org.mockito.stubbing.Answer;

import javax.annotation.Nonnull;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;

import static org.junit.Assert.*;
import static org.mockito.Mockito.*;

@RunWith(MockitoJUnitRunner.class)
public class DefaultMergeRequestCheckServiceTest {

    private static final String DESCRIPTION = "some description";
    private static final String SUMMARY = "some summary";
    private static final String EXCEPTION_SUMMARY = "bitbucket.service.pullrequest.merge.exception.summary";
    private static final String EXCEPTION_DESCRIPTION = "bitbucket.service.pullrequest.merge.exception.description";
    private static final MergeRequestCheck THROW = new MergeRequestCheck() {
        @Override
        public void check(@Nonnull MergeRequest request) {
            throw new RuntimeException();
        }
    };
    private static final MergeRequestCheck VETO = new MergeRequestCheck() {
        @Override
        public void check(@Nonnull MergeRequest request) {
            request.veto(SUMMARY, DESCRIPTION);
        }
    };

    @Mock
    private Command<Boolean> canMergeCommand;
    @Mock
    private InternalMergeRequest mergeRequest;
    @Mock
    private PluginAccessor pluginAccessor;
    @Mock
    private PullRequestEnricher pullRequestEnricher;
    @Spy
    private I18nService i18nService = new SimpleI18nService();
    @Mock
    private InternalPullRequest pullRequest;
    @Mock
    private ScmPullRequestCommandFactory commandFactory;
    @Mock
    private ScmService scmService;
    @InjectMocks
    private DefaultMergeRequestCheckService service;

    @Before
    public void setup() {
        final List<PullRequestMergeVeto> vetoes = new ArrayList<>();

        when(commandFactory.canMerge()).thenReturn(canMergeCommand);
        when(scmService.getPullRequestCommandFactory(same(pullRequest))).thenReturn(commandFactory);

        when(mergeRequest.getPullRequest()).thenReturn(pullRequest);
        when(mergeRequest.getVetoes()).thenReturn(vetoes);
        doAnswer(new Answer() {

            @Override
            public Object answer(InvocationOnMock invocation) throws Throwable {
                Object[] arguments = invocation.getArguments();

                vetoes.add(new SimplePullRequestMergeVeto((String) arguments[0], (String) arguments[1]));

                return null;
            }
        }).when(mergeRequest).veto(anyString(), anyString());

        when(pullRequestEnricher.enrich(any(InternalPullRequest.class), any(SimplePullRequestMergeability.class)))
                .thenAnswer(MockitoUtils.returnArg(1));
    }

    @Test
    public void testCheck() {
        MergeRequestCheck check = mock(MergeRequestCheck.class);
        mockMergeChecks(check);

        assertNotVetoed(service.check(pullRequest));

        ArgumentCaptor<MergeRequest> requestCaptor = ArgumentCaptor.forClass(MergeRequest.class);

        verify(check).check(requestCaptor.capture());

        MergeRequest request = requestCaptor.getValue();
        assertNotNull(request);
        assertNotNull(request.getContext());
        assertNull(request.getMessage());
        assertSame(pullRequest, request.getPullRequest());
        assertTrue(request.getContext().isEmpty());
        assertTrue(request.isDryRun());
    }

    @Test
    public void testCheckMergeability() {
        MergeRequestCheck check = mock(MergeRequestCheck.class);

        mockMergeChecks(check);

        PullRequestMergeability mergeability = service.checkMergeability(mergeRequest);
        assertNotNull(mergeability);
        assertFalse(mergeability.isConflicted());
        assertNotVetoed(mergeability.getVetoes());
        assertTrue(mergeability.canMerge());

        verify(canMergeCommand).call();
        verify(check).check(same(mergeRequest));
        verify(pullRequestEnricher).enrich(same(pullRequest), any(SimplePullRequestMergeability.class));
    }

    @Test
    public void testCheckMergeabilityWithConflicts() {
        when(canMergeCommand.call()).thenReturn(Boolean.FALSE);

        mockMergeChecks(mock(MergeRequestCheck.class));

        PullRequestMergeability mergeability = service.checkMergeability(mergeRequest);
        assertNotNull(mergeability);
        assertFalse(mergeability.canMerge());
        assertNotVetoed(mergeability.getVetoes());
        assertTrue(mergeability.isConflicted());
    }

    @Test
    public void testCheckMergeabilityWithConflictsAndVeto() {
        when(canMergeCommand.call()).thenReturn(Boolean.FALSE);

        mockMergeChecks(VETO);

        PullRequestMergeability mergeability = service.checkMergeability(mergeRequest);
        assertNotNull(mergeability);
        assertFalse(mergeability.canMerge());
        assertTrue(mergeability.isConflicted());
        assertVetoed(mergeability.getVetoes());
    }

    @Test
    public void testCheckMergeabilityWithVeto() {
        when(canMergeCommand.call()).thenReturn(Boolean.TRUE);

        mockMergeChecks(VETO);

        PullRequestMergeability mergeability = service.checkMergeability(mergeRequest);
        assertNotNull(mergeability);
        assertFalse(mergeability.canMerge());
        assertFalse(mergeability.isConflicted());
        assertVetoed(mergeability.getVetoes());
    }

    @Test
    public void testCheckContinuesAfterBrokenMergeChecks() throws Exception {
        mockMergeChecks(THROW, VETO); //Verify that the broken check does not block the next check

        Collection<PullRequestMergeVeto> vetoes = service.check(pullRequest);

        assertNotNull(vetoes);
        assertEquals(2, vetoes.size());

        PullRequestMergeVeto veto = Iterables.get(vetoes, 0);
        assertEquals(EXCEPTION_SUMMARY, veto.getSummaryMessage());
        assertEquals(EXCEPTION_DESCRIPTION, veto.getDetailedMessage());

        veto = Iterables.get(vetoes, 1);
        assertEquals(SUMMARY, veto.getSummaryMessage());
        assertEquals(DESCRIPTION, veto.getDetailedMessage());
    }

    @Test
    public void testCheckVetoesForBrokenMergeChecks() throws Exception {
        mockMergeChecks(THROW);

        Collection<PullRequestMergeVeto> vetoes = service.check(pullRequest);

        assertNotNull(vetoes);
        assertEquals(1, vetoes.size());
        assertEquals(EXCEPTION_SUMMARY, Iterables.getOnlyElement(vetoes).getSummaryMessage());
        assertEquals(EXCEPTION_DESCRIPTION, Iterables.getOnlyElement(vetoes).getDetailedMessage());
    }

    @Test
    public void testCheckWithNoMergeChecks() throws Exception {
        mockMergeChecks();

        assertNotVetoed(service.check(pullRequest));
    }

    @Test
    public void testCheckWithOneVeto() throws Exception {
        mockMergeChecks(VETO);

        assertVetoed(service.check(pullRequest));
    }

    private static void assertNotVetoed(Collection<PullRequestMergeVeto> vetoes) {
        assertNotNull(vetoes);
        assertTrue(vetoes.isEmpty());
    }

    private static void assertVetoed(Collection<PullRequestMergeVeto> vetoes) {
        assertNotNull(vetoes);
        assertEquals(1, vetoes.size());

        PullRequestMergeVeto veto = Iterables.get(vetoes, 0);
        assertEquals(SUMMARY, veto.getSummaryMessage());
        assertEquals(DESCRIPTION, veto.getDetailedMessage());
    }

    private static MergeRequestCheckModuleDescriptor mockDescriptor(MergeRequestCheck check) {
        MergeRequestCheckModuleDescriptor descriptor = mock(MergeRequestCheckModuleDescriptor.class);
        when(descriptor.getModule()).thenReturn(check);
        return descriptor;
    }

    private void mockMergeChecks(MergeRequestCheck... checks) {
        when(pluginAccessor.getEnabledModuleDescriptorsByClass(eq(MergeRequestCheckModuleDescriptor.class)))
                .thenReturn(Lists.transform(Lists.newArrayList(checks),
                        new Function<MergeRequestCheck, MergeRequestCheckModuleDescriptor>() {
                            @Override
                            public MergeRequestCheckModuleDescriptor apply(MergeRequestCheck check) {
                                return mockDescriptor(check);
                            }
                        }));
    }
}
