package com.atlassian.bitbucket.internal.defaultreviewers.mergecheck;

import com.atlassian.bitbucket.i18n.I18nService;
import com.atlassian.bitbucket.internal.defaultreviewers.PullRequestConditionService;
import com.atlassian.bitbucket.internal.defaultreviewers.model.DefaultPullRequestCondition;
import com.atlassian.bitbucket.internal.defaultreviewers.model.PullRequestCondition;
import com.atlassian.bitbucket.pull.PullRequest;
import com.atlassian.bitbucket.pull.PullRequestParticipant;
import com.atlassian.bitbucket.pull.PullRequestRole;
import com.atlassian.bitbucket.repository.Repository;
import com.atlassian.bitbucket.repository.ref.restriction.RefMatcher;
import com.atlassian.bitbucket.scm.pull.MergeRequest;
import com.atlassian.bitbucket.user.ApplicationUser;
import com.atlassian.bitbucket.user.TestApplicationUser;
import com.atlassian.bitbucket.util.Page;
import com.atlassian.bitbucket.util.PageRequest;
import com.atlassian.bitbucket.util.PageUtils;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.runners.MockitoJUnitRunner;
import org.mockito.stubbing.Answer;

import java.util.*;

import static java.util.Collections.emptyList;
import static java.util.Collections.emptySet;
import static org.hamcrest.Matchers.equalTo;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.*;

@RunWith(MockitoJUnitRunner.class)
public class RequiredReviewersMergeCheckTest {
    private static final String AUTHOR_NAME = "author";
    private static final String DEFAULT_NAME = "default";
    private static final String REVIEWIER_1_NAME = "user1";
    private static final String REVIEWIER_2_NAME = "user2";
    private static final String VETO_SUMMARY = "reviewersRequired";
    private static final String VETO_GROUP_REQUIRED = "groupReviewersRequired";

    @Mock
    private I18nService i18nService;

    @Mock
    private PullRequestConditionService pullRequestConditionService;

    @InjectMocks
    private RequiredReviewersMergeCheck mergeCheck;

    private ApplicationUser defaultUser = new TestApplicationUser(DEFAULT_NAME);
    private PullRequestParticipant defaultAuthorParticipant = mockPullRequestParticipant(defaultUser, PullRequestRole.AUTHOR, false);

    @Before
    public void setup() throws Exception {
        when(i18nService.getMessage(eq("bitbucket.defaultreviewers.requiredreviewers.mergecheck.vetoed.summary")))
                .thenReturn(VETO_SUMMARY);
        when(i18nService.getMessage(eq("bitbucket.defaultreviewers.requiredreviewers.mergecheck.vetoed.description"), anyString()))
                .thenAnswer(invocation -> {
                    String arg = (String) invocation.getArguments()[1];
                    return arg;
                });
        when(i18nService.getMessage(eq("bitbucket.defaultreviewers.requiredreviewers.mergecheck.vetoed.reason.number"), anyInt(), anyString()))
                .thenReturn(VETO_GROUP_REQUIRED);
        when(i18nService.getMessage("bitbucket.defaultreviewers.requiredreviewers.mergecheck.vetoed.joinerOr")).thenReturn("or");
        when(i18nService.getMessage("bitbucket.defaultreviewers.requiredreviewers.mergecheck.vetoed.joinerAnd")).thenReturn("and");
    }

    @Test
    public void testCheckShouldNotVetoWithNoRequiredReviewers() {
        shouldNotVeto(emptyList(), defaultAuthorParticipant, emptySet());
    }

    @Test
    public void testCheckShouldNotVetoWithOneConditionButNotRequired() {
        ApplicationUser user = new TestApplicationUser(REVIEWIER_1_NAME);

        PullRequestCondition zeroRequired = buildPullRequestCondition(1, Collections.singleton(user), 0);

        shouldNotVeto(Collections.singleton(zeroRequired), defaultAuthorParticipant, emptySet());
    }

    @Test
    public void testCheckShouldNotVetoWithOneRequiredReviewerAndApprovalGiven() {
        ApplicationUser user = new TestApplicationUser(REVIEWIER_1_NAME);
        PullRequestParticipant oneApproved = mockPullRequestParticipant(user, true);

        PullRequestCondition oneRequired = buildPullRequestCondition(1, Collections.singleton(user), 1);

        shouldNotVeto(Collections.singleton(oneRequired), defaultAuthorParticipant, Collections.singleton(oneApproved));
    }

    @Test
    public void testCheckShouldVetoWithOneRequiredReviewerAndNoApprovers() {
        ApplicationUser user = new TestApplicationUser(REVIEWIER_1_NAME);
        PullRequestCondition oneRequired = buildPullRequestCondition(1, Collections.singleton(user), 1);

        shouldVeto(Collections.singleton(oneRequired), defaultAuthorParticipant,
                emptySet(), REVIEWIER_1_NAME);
    }

    @Test
    public void testCheckShouldVetoWithOneRequiredReviewerAndDifferentApprover() {
        ApplicationUser user1 = new TestApplicationUser(REVIEWIER_1_NAME);
        ApplicationUser user2 = new TestApplicationUser(REVIEWIER_2_NAME);

        PullRequestParticipant unapprovedParticipant = mockPullRequestParticipant(user1, false);
        PullRequestParticipant approvedParticipant = mockPullRequestParticipant(user2, true);

        PullRequestCondition oneRequired = buildPullRequestCondition(1, Collections.singleton(user1), 1);

        shouldVeto(Collections.singleton(oneRequired), defaultAuthorParticipant,
                ImmutableSet.of(unapprovedParticipant, approvedParticipant), REVIEWIER_1_NAME);
    }

    @Test
    public void testCheckShouldVetoWithOneOutOfTwoRequiredReviewersAndNoApprovers() {
        ApplicationUser user1 = new TestApplicationUser(REVIEWIER_1_NAME);
        ApplicationUser user2 = new TestApplicationUser(REVIEWIER_2_NAME);

        PullRequestParticipant participant1 = mockPullRequestParticipant(user1, false);
        PullRequestParticipant participant2 = mockPullRequestParticipant(user2, false);

        PullRequestCondition oneRequired = buildPullRequestCondition(1, ImmutableSet.of(user1, user2), 1);

        shouldVeto(Collections.singleton(oneRequired), defaultAuthorParticipant,
                ImmutableSet.of(participant1, participant2), REVIEWIER_1_NAME + " or " + REVIEWIER_2_NAME);

        String namesComponent = "user1 or user2";
        verify(i18nService).getMessage(eq("bitbucket.defaultreviewers.requiredreviewers.mergecheck.vetoed.joinerOr"));
        verify(i18nService).getMessage("bitbucket.defaultreviewers.requiredreviewers.mergecheck.vetoed.description",
                namesComponent);
    }

    @Test
    public void testCheckShouldVetoWithTwoRequiredReviewersAndNoApprovers() {
        ApplicationUser user1 = new TestApplicationUser(REVIEWIER_1_NAME);
        ApplicationUser user2 = new TestApplicationUser(REVIEWIER_2_NAME);

        PullRequestParticipant participant1 = mockPullRequestParticipant(user1, false);
        PullRequestParticipant participant2 = mockPullRequestParticipant(user2, false);

        PullRequestCondition twoRequired = buildPullRequestCondition(1, ImmutableSet.of(user1, user2), 2);

        shouldVeto(Collections.singleton(twoRequired), defaultAuthorParticipant,
                ImmutableSet.of(participant1, participant2), REVIEWIER_1_NAME + " and " + REVIEWIER_2_NAME);
    }

    @Test
    public void testCheckShouldNotVetoWithOneRequiredReviewerWhoIsAuthor() {
        ApplicationUser user = new TestApplicationUser(AUTHOR_NAME);

        PullRequestParticipant author = mockPullRequestParticipant(user, PullRequestRole.AUTHOR, false);

        PullRequestCondition oneRequired = buildPullRequestCondition(1, ImmutableSet.of(user), 1);

        shouldNotVeto(Collections.singleton(oneRequired), author, ImmutableSet.of(author));
    }

    @Test
    public void testCheckShouldVetoWithTwoRequiredReviewersWhereOneIsAuthor() {
        ApplicationUser author = new TestApplicationUser(AUTHOR_NAME);
        ApplicationUser reviewer = new TestApplicationUser(REVIEWIER_1_NAME);

        PullRequestParticipant authorParticipant = mockPullRequestParticipant(author, PullRequestRole.AUTHOR, false);
        PullRequestParticipant reviewerParticipant = mockPullRequestParticipant(reviewer, PullRequestRole.REVIEWER, false);

        PullRequestCondition twoRequired = buildPullRequestCondition(1, ImmutableSet.of(author, reviewer), 1);

        shouldVeto(Collections.singleton(twoRequired), authorParticipant, ImmutableSet.of(authorParticipant, reviewerParticipant), REVIEWIER_1_NAME);
    }

    @Test
    public void testCheckShouldNotVetoWithTwoRequiredReviewersWhereOneIsAuthorAndOtherHasApproved() {
        ApplicationUser author = new TestApplicationUser(AUTHOR_NAME);
        ApplicationUser reviewer = new TestApplicationUser(REVIEWIER_1_NAME);

        PullRequestParticipant authorParticipant = mockPullRequestParticipant(author, PullRequestRole.AUTHOR, false);
        PullRequestParticipant reviewerParticipant = mockPullRequestParticipant(reviewer, PullRequestRole.REVIEWER, true);

        PullRequestCondition twoRequired = buildPullRequestCondition(1, ImmutableSet.of(author, reviewer), 2);

        shouldNotVeto(Collections.singleton(twoRequired), authorParticipant, ImmutableSet.of(authorParticipant, reviewerParticipant));
    }

    @Test
    public void testCheckShouldVetoWithOneOutOfThreeRequiredWhereOneIsAuthor() {
        ApplicationUser author = new TestApplicationUser(AUTHOR_NAME);
        ApplicationUser reviewer1 = new TestApplicationUser(REVIEWIER_1_NAME);
        ApplicationUser reviewer2 = new TestApplicationUser(REVIEWIER_2_NAME);

        PullRequestParticipant authorParticipant = mockPullRequestParticipant(author, PullRequestRole.AUTHOR, false);
        PullRequestParticipant reviewerParticipant1 = mockPullRequestParticipant(reviewer1, false);
        PullRequestParticipant reviewerParticipant2 = mockPullRequestParticipant(reviewer2, false);

        PullRequestCondition oneOutOfThreeRequired = buildPullRequestCondition(1, ImmutableSet.of(author, reviewer1, reviewer2), 1);

        shouldVeto(Collections.singleton(oneOutOfThreeRequired), authorParticipant, ImmutableSet.of(reviewerParticipant1, reviewerParticipant2), REVIEWIER_1_NAME + " or " + REVIEWIER_2_NAME);

        String namesComponent = "user1 or user2";
        verify(i18nService).getMessage(eq("bitbucket.defaultreviewers.requiredreviewers.mergecheck.vetoed.joinerOr"));
        verify(i18nService).getMessage("bitbucket.defaultreviewers.requiredreviewers.mergecheck.vetoed.description",
                namesComponent);
    }

    @Test
    public void testShouldVetoWithMultipleConditions() {
        ApplicationUser author = new TestApplicationUser(AUTHOR_NAME);
        ApplicationUser reviewer1 = new TestApplicationUser(REVIEWIER_1_NAME);
        ApplicationUser reviewer2 = new TestApplicationUser(REVIEWIER_2_NAME);

        PullRequestParticipant authorParticipant = mockPullRequestParticipant(author, PullRequestRole.AUTHOR, false);

        PullRequestCondition condition1 = buildPullRequestCondition(1, ImmutableSet.of(author, reviewer1), 1);
        PullRequestCondition condition2 = buildPullRequestCondition(1, ImmutableSet.of(author, reviewer2), 1);

        shouldVeto(ImmutableList.of(condition1, condition2), authorParticipant, emptySet(), ImmutableList.of(REVIEWIER_1_NAME, REVIEWIER_2_NAME));
    }

    @Test
    public void testShouldExcludeVetoWhenIdenticalOneAdded() {
        ApplicationUser author = new TestApplicationUser(AUTHOR_NAME);
        ApplicationUser reviewer1 = new TestApplicationUser(REVIEWIER_1_NAME);

        PullRequestParticipant authorParticipant = mockPullRequestParticipant(author, PullRequestRole.AUTHOR, false);

        PullRequestCondition condition1 = buildPullRequestCondition(1, ImmutableSet.of(author, reviewer1), 1);
        PullRequestCondition condition2 = buildPullRequestCondition(1, ImmutableSet.of(reviewer1), 1);

        shouldVeto(ImmutableList.of(condition1, condition2), authorParticipant, emptySet(), REVIEWIER_1_NAME);
    }

    @Test
    public void testShouldExcludeVetoWhenStrongerRuleAlreadyAdded() {
        ApplicationUser author = new TestApplicationUser(AUTHOR_NAME);
        ApplicationUser reviewer1 = new TestApplicationUser(REVIEWIER_1_NAME);
        ApplicationUser reviewer2 = new TestApplicationUser(REVIEWIER_2_NAME);

        PullRequestParticipant authorParticipant = mockPullRequestParticipant(author, PullRequestRole.AUTHOR, false);

        PullRequestCondition condition1 = buildPullRequestCondition(1, ImmutableSet.of(reviewer1, reviewer2), 1);
        PullRequestCondition condition2 = buildPullRequestCondition(1, ImmutableSet.of(author, reviewer1, reviewer2), 2);

        shouldVeto(ImmutableList.of(condition1, condition2), authorParticipant, emptySet(), REVIEWIER_1_NAME + " and " + REVIEWIER_2_NAME);
    }

    private MergeRequest buildMergeRequest(Collection<PullRequestCondition> pullRequestConditions, PullRequestParticipant author,
                                           Set<PullRequestParticipant> reviewers) {
        PullRequest pullRequest = mock(PullRequest.class, RETURNS_DEEP_STUBS);
        MergeRequest mergeRequest = mock(MergeRequest.class, RETURNS_DEEP_STUBS);

        Page<PullRequestCondition> conditionPage = PageUtils.createPage(pullRequestConditions, true, PageUtils.newRequest(0, 100));

        when(mergeRequest.getPullRequest()).thenReturn(pullRequest);
        when(pullRequest.getAuthor()).thenReturn(author);
        when(pullRequest.getReviewers()).thenReturn(reviewers);
        when(pullRequestConditionService.getPullRequestConditions(any(Repository.class), any(PageRequest.class))).thenReturn(conditionPage);

        return mergeRequest;
    }

    private PullRequestCondition buildPullRequestCondition(int id, Collection<ApplicationUser> reviewers, int requiredApprovals) {
        Repository repository = mock(Repository.class, RETURNS_DEEP_STUBS);
        RefMatcher sourceRefMatcher = mock(RefMatcher.class, RETURNS_DEEP_STUBS);
        RefMatcher targetRefMatcher = mock(RefMatcher.class, RETURNS_DEEP_STUBS);
        when(sourceRefMatcher.matches(any())).thenReturn(true);
        when(targetRefMatcher.matches(any())).thenReturn(true);

        return DefaultPullRequestCondition.builder()
                .id(id)
                .repository(repository)
                .sourceMatcher(sourceRefMatcher)
                .targetMatcher(targetRefMatcher)
                .requiredApprovals(requiredApprovals)
                .reviewers(reviewers)
                .build();
    }

    private static PullRequestParticipant mockPullRequestParticipant(ApplicationUser user, PullRequestRole role, boolean isApproved) {
        PullRequestParticipant pullRequestParticipant = mock(PullRequestParticipant.class, RETURNS_DEEP_STUBS);
        when(pullRequestParticipant.getUser()).thenReturn(user);
        when(pullRequestParticipant.isApproved()).thenReturn(isApproved);
        when(pullRequestParticipant.getRole()).thenReturn(role);
        return pullRequestParticipant;
    }

    private static PullRequestParticipant mockPullRequestParticipant(ApplicationUser user, boolean isApproved) {
        return mockPullRequestParticipant(user, PullRequestRole.REVIEWER, isApproved);
    }

    private void shouldNotVeto(Collection<PullRequestCondition> pullRequestConditions, PullRequestParticipant author,
                               Set<PullRequestParticipant> reviewers) {
        MergeRequest mergeRequest = buildMergeRequest(pullRequestConditions, author, reviewers);

        mergeCheck.check(mergeRequest);

        verify(mergeRequest).getPullRequest();
        verifyNoMoreInteractions(mergeRequest);
    }

    private void shouldVeto(Collection<PullRequestCondition> pullRequestConditions, PullRequestParticipant author,
                            Set<PullRequestParticipant> reviewers, String vetoDescription) {
        shouldVeto(pullRequestConditions, author, reviewers, ImmutableList.of(vetoDescription));
    }

    private void shouldVeto(Collection<PullRequestCondition> pullRequestConditions, PullRequestParticipant author,
                            Set<PullRequestParticipant> reviewers, List<String> vetoDescriptions) {
        MergeRequest mergeRequest = buildMergeRequest(pullRequestConditions, author, reviewers);
        List<String> vetoes = new ArrayList<>();
        doAnswer(new Answer<Void>() {
            public Void answer(InvocationOnMock invocation) {
                Object[] args = invocation.getArguments();
                vetoes.add((String) args[1]);
                return null;
            }
        }).when(mergeRequest).veto(anyString(), anyString());

        mergeCheck.check(mergeRequest);

        assertThat(vetoes.size(), equalTo(vetoDescriptions.size()));
        vetoDescriptions.forEach(vetoDescription -> assertTrue(vetoDescription + " should be present in the list of vetoes",
                vetoes.stream().anyMatch(vetoMessage -> vetoMessage.equals(vetoDescription))));
    }
}
