package com.atlassian.bitbucket.internal.defaultreviewers;

import com.atlassian.bitbucket.i18n.I18nService;
import com.atlassian.bitbucket.internal.defaultreviewers.dao.AoPullRequestCondition;
import com.atlassian.bitbucket.internal.defaultreviewers.dao.AoReviewer;
import com.atlassian.bitbucket.internal.defaultreviewers.dao.PullRequestConditionDao;
import com.atlassian.bitbucket.internal.defaultreviewers.event.PullRequestConditionCreatedEvent;
import com.atlassian.bitbucket.internal.defaultreviewers.event.PullRequestConditionDeletedEvent;
import com.atlassian.bitbucket.internal.defaultreviewers.event.PullRequestConditionUpdatedEvent;
import com.atlassian.bitbucket.internal.defaultreviewers.matcher.AnyRefMatcher;
import com.atlassian.bitbucket.internal.defaultreviewers.model.PullRequestCondition;
import com.atlassian.bitbucket.permission.Permission;
import com.atlassian.bitbucket.permission.PermissionService;
import com.atlassian.bitbucket.permission.PermissionValidationService;
import com.atlassian.bitbucket.repository.*;
import com.atlassian.bitbucket.repository.ref.restriction.RefMatcher;
import com.atlassian.bitbucket.repository.ref.restriction.RefMatcherProvider;
import com.atlassian.bitbucket.repository.ref.restriction.RefMatcherProviderRegistry;
import com.atlassian.bitbucket.user.ApplicationUser;
import com.atlassian.bitbucket.user.TestApplicationUser;
import com.atlassian.bitbucket.user.UserService;
import com.atlassian.bitbucket.util.Page;
import com.atlassian.bitbucket.util.PageRequest;
import com.atlassian.bitbucket.util.PageUtils;
import com.atlassian.event.api.EventPublisher;
import com.atlassian.stash.internal.repository.ref.restriction.provider.BranchMatcher;
import com.google.common.collect.ImmutableList;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.runners.MockitoJUnitRunner;

import java.util.Collection;
import java.util.Collections;
import java.util.Optional;

import static org.hamcrest.CoreMatchers.notNullValue;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.hasSize;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.fail;
import static org.mockito.Matchers.*;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

@RunWith(MockitoJUnitRunner.class)
public class DefaultPullRequestConditionServiceTest {

    // Static variables for test
    private static final String ANY_REF_MATCHER_ID = "ANY_REF_ID";
    private static final String ANY_REF_MATCHER_TYPE = "ANY_REF_TYPE";

    private static final String USER_NAME_1 = "user1";
    private static final String USER_NAME_2 = "user2";
    private static final String USER_NAME_DELETED = "deletedUser";
    private static final String USER_NAME_REVOKED = "revokedUser";

    private static final int REPOSITORY_ID = 1;
    private static final int USER_ID_1 = 10;
    private static final int USER_ID_2 = 20;
    private static final int USER_ID_DELETED = 30;
    private static final int USER_ID_REVOKED = 40;

    // Service mocks
    @Mock
    private EventPublisher eventPublisher;

    @Mock
    private I18nService i18nService;

    @Mock
    private PullRequestConditionDao pullRequestConditionDao;

    @Mock
    private RefMatcherProviderRegistry refMatcherProviderRegistry;

    @Mock
    private RepositorySupplier repositorySupplier;

    @Mock
    private UserService userService;

    @Mock
    private RefMatcherProvider refMatcherProvider;

    @Mock
    private PermissionValidationService permissionValidationService;

    @Mock
    private PermissionService permissionService;

    // Data objects
    private ApplicationUser applicationUser1 = new TestApplicationUser(USER_NAME_1, USER_ID_1);
    private ApplicationUser applicationUser2 = new TestApplicationUser(USER_NAME_2, USER_ID_2);
    private ApplicationUser deletedApplicationUser = new InactiveTestApplicationUser(USER_NAME_DELETED, USER_ID_DELETED);
    private ApplicationUser revokedApplicationUser = new TestApplicationUser(USER_NAME_REVOKED, USER_ID_REVOKED);

    private RefMatcher refMatcher = AnyRefMatcher.getInstance();

    @Mock
    private Repository repository;

    private AoReviewer[] aoReviewers = new AoReviewer[]{
            mockAoReviewer(1, USER_ID_1),
            mockAoReviewer(2, USER_ID_2)
    };

    private AoReviewer[] fewerAoReviewers = new AoReviewer[]{
            mockAoReviewer(1, USER_ID_1)
    };

    private AoReviewer[] aoReviewersDeleted = new AoReviewer[]{
            mockAoReviewer(1, USER_ID_DELETED)
    };

    private AoReviewer[] aoReviewersRevoked = new AoReviewer[]{
            mockAoReviewer(1, USER_ID_REVOKED)
    };

    private AoPullRequestCondition aoPullRequestCondition1 = mockAoPullRequestCondition(1, REPOSITORY_ID,
            ANY_REF_MATCHER_ID, ANY_REF_MATCHER_TYPE,
            ANY_REF_MATCHER_ID, ANY_REF_MATCHER_TYPE,
            aoReviewers, 0);

    private AoPullRequestCondition aoPullRequestCondition2 = mockAoPullRequestCondition(1, REPOSITORY_ID,
            ANY_REF_MATCHER_ID, ANY_REF_MATCHER_TYPE,
            ANY_REF_MATCHER_ID, ANY_REF_MATCHER_TYPE,
            fewerAoReviewers, 0);

    private AoPullRequestCondition aoPullRequestConditionWithDeletedUser = mockAoPullRequestCondition(1, REPOSITORY_ID,
            ANY_REF_MATCHER_ID, ANY_REF_MATCHER_TYPE,
            ANY_REF_MATCHER_ID, ANY_REF_MATCHER_TYPE,
            aoReviewersDeleted, 0);

    private AoPullRequestCondition aoPullRequestConditionWithRevokedUser = mockAoPullRequestCondition(1, REPOSITORY_ID,
            ANY_REF_MATCHER_ID, ANY_REF_MATCHER_TYPE,
            ANY_REF_MATCHER_ID, ANY_REF_MATCHER_TYPE,
            aoReviewersRevoked, 0);

    private PageRequest singlePageRequest = PageUtils.newRequest(0, 100);

    // Class being tested
    private DefaultPullRequestConditionService service;

    @Before
    public void setup() {
        when(repository.getId()).thenReturn(REPOSITORY_ID);
        when(repositorySupplier.getById(anyInt())).thenReturn(repository);

        when(refMatcherProviderRegistry.getProvider(anyString())).thenReturn(refMatcherProvider);
        when(refMatcherProvider.create(any(Repository.class), anyString())).thenReturn(refMatcher);

        when(userService.getUserById(eq(USER_ID_1), anyBoolean())).thenReturn(applicationUser1);
        when(userService.getUserById(eq(USER_ID_2), anyBoolean())).thenReturn(applicationUser2);
        when(userService.getUserById(eq(USER_ID_DELETED), anyBoolean())).thenReturn(deletedApplicationUser);
        when(userService.getUserById(eq(USER_ID_REVOKED), anyBoolean())).thenReturn(revokedApplicationUser);

        when(permissionService.hasRepositoryPermission(any(), any(), any())).thenReturn(true);

        service = new DefaultPullRequestConditionService(
                eventPublisher, i18nService, permissionService, permissionValidationService, pullRequestConditionDao,
                refMatcherProviderRegistry, repositorySupplier, userService);
    }

    @Test
    public void testGetReviewersWithSingleCondition() {
        when(pullRequestConditionDao.find(anyInt(), any())).thenReturn(PageUtils
                .createPage(Collections.singleton(aoPullRequestCondition1), true, singlePageRequest));

        Ref fromRef = toBranch("fromBranch", false);
        Ref toRef = toBranch("toBranch", false);

        Collection<ApplicationUser> reviewers = service.getReviewers(repository, fromRef, toRef);

        assertThat("There should be two reviewers returned", reviewers, hasSize(2));
    }

    @Test
    public void testGetReviewersWithMultipleConditionsAndOverlappingReviewers() {

        when(pullRequestConditionDao.find(anyInt(), any())).thenReturn(PageUtils
                .createPage(ImmutableList.of(aoPullRequestCondition1, aoPullRequestCondition2), true, singlePageRequest));

        Ref fromRef = toBranch("fromBranch", false);
        Ref toRef = toBranch("toBranch", false);

        Collection<ApplicationUser> reviewers = service.getReviewers(repository, fromRef, toRef);

        assertThat("There should be two reviewers returned without duplicates", reviewers, hasSize(2));
    }

    @Test
    public void testGetReviewersShouldNotReturnDeletedUsers() {
        when(pullRequestConditionDao.find(anyInt(), any())).thenReturn(PageUtils
                .createPage(Collections.singleton(aoPullRequestConditionWithDeletedUser), true, singlePageRequest));

        Ref fromRef = toBranch("fromBranch", false);
        Ref toRef = toBranch("toBranch", false);

        Collection<ApplicationUser> reviewers = service.getReviewers(repository, fromRef, toRef);

        assertThat("There should be zero reviewers returned", reviewers, hasSize(0));
    }

    @Test
    public void testGetReviewersShouldNotReturnUsersWithoutReadPermissionForRepository() {
        when(pullRequestConditionDao.find(anyInt(), any())).thenReturn(PageUtils
                .createPage(Collections.singleton(aoPullRequestConditionWithRevokedUser), true, singlePageRequest));
        when(permissionService.hasRepositoryPermission(eq(revokedApplicationUser), any(), eq(Permission.REPO_READ))).thenReturn(false);

        Ref fromRef = toBranch("fromBranch", false);
        Ref toRef = toBranch("toBranch", false);

        Collection<ApplicationUser> reviewers = service.getReviewers(repository, fromRef, toRef);

        assertThat("There should be zero reviewers returned", reviewers, hasSize(0));
    }

    @Test
    public void testGetPullRequestConditionsReturnsResults() {
        when(pullRequestConditionDao.find(anyInt(), any())).thenReturn(PageUtils
                .createPage(Collections.singleton(aoPullRequestCondition1), true, singlePageRequest));

        Page<PullRequestCondition> pullRequestConditions = service
                .getPullRequestConditions(repository, singlePageRequest);

        assertThat("There should be one condition returned", pullRequestConditions.getSize(), equalTo(1));
    }

    @Test
    public void testGetPullRequestConditionsWithDeletedUser() {
        when(pullRequestConditionDao.find(anyInt(), any())).thenReturn(PageUtils
                .createPage(Collections.singleton(aoPullRequestConditionWithDeletedUser), true, singlePageRequest));

        Page<PullRequestCondition> pullRequestConditions = service
                .getPullRequestConditions(repository, singlePageRequest);

        assertThat("There should be one condition returned", pullRequestConditions.getSize(), equalTo(1));

        Optional<PullRequestCondition> prCondition = pullRequestConditions.stream().findFirst();

        if (!prCondition.isPresent()) {
            fail("There should be one condition returned");
        }

        prCondition.ifPresent(condition -> {
            Optional<ApplicationUser> reviewer = condition.getReviewers().stream().findFirst();

            if (!reviewer.isPresent()) {
                fail("There should be one reviewer assigned to this condition");
            }

            reviewer.ifPresent(user -> assertThat("The user should be the deleted user", user.getId(), equalTo(USER_ID_DELETED)));
        });
    }

    @Test
    public void testCreatePullRequestCondition() {
        RefMatcher sourceMatcher = AnyRefMatcher.getInstance();
        RefMatcher targetMatcher = new BranchMatcher("refs/heads/develop", false, () -> null);
        Collection<ApplicationUser> reviewers = ImmutableList.of(applicationUser1, applicationUser2);

        AoPullRequestCondition mockCondition = mockAoPullRequestCondition(1, 1, sourceMatcher.getId(),
                sourceMatcher.getType().getId(), targetMatcher.getId(), targetMatcher.getType().getId(),
                aoReviewers, 1);

        when(pullRequestConditionDao.create(eq(1), eq(sourceMatcher), eq(targetMatcher), anySetOf(Integer.class), eq(1)))
                .thenReturn(mockCondition);
        when(refMatcherProvider.create(eq(repository), eq(sourceMatcher.getId()))).thenReturn(sourceMatcher);
        when(refMatcherProvider.create(eq(repository), eq(targetMatcher.getId()))).thenReturn(targetMatcher);

        PullRequestCondition createdCondition = service.createPullRequestCondition(repository, sourceMatcher, targetMatcher,
                reviewers, 1);

        verify(pullRequestConditionDao).create(eq(1), eq(sourceMatcher), eq(targetMatcher), anySetOf(Integer.class), eq(1));
        verify(eventPublisher).publish(any(PullRequestConditionCreatedEvent.class));
        assertThat("Created condition should not be null", createdCondition, notNullValue());
        assertThat("Created condition should have correct repo ID", createdCondition.getRepository(), equalTo(repository));
        assertThat("Created condition should have correct source matcher", createdCondition.getFromMatcher(), equalTo(sourceMatcher));
        assertThat("Created condition should have correct target matcher", createdCondition.getToMatcher(), equalTo(targetMatcher));
        assertThat("Created condition should have correct reviewers", createdCondition.getReviewers(), equalTo(reviewers));
        assertThat("Created condition should have correct required approvals", createdCondition.getRequiredApprovals(), equalTo(1));
    }

    @Test
    public void testUpdatePullRequestCondition() {
        Collection<ApplicationUser> newReviewers = ImmutableList.of(applicationUser1, applicationUser2);
        int newRequiredApprovals = 1;

        when(pullRequestConditionDao.get(anyInt())).thenReturn(aoPullRequestCondition1);
        when(pullRequestConditionDao.update(any(), eq(1), any(RefMatcher.class), any(RefMatcher.class),
                eq(ImmutableList.of(USER_ID_1, USER_ID_2)), eq(newRequiredApprovals))).thenReturn(aoPullRequestCondition2);

        PullRequestCondition updatedCondition = service
                .updatePullRequestCondition(1, repository, refMatcher, refMatcher, newReviewers, newRequiredApprovals);

        assertThat("Updated condition should have one reviewer", updatedCondition.getReviewers(), hasSize(1));
        verify(pullRequestConditionDao).update(any(), eq(1), any(RefMatcher.class), any(RefMatcher.class),
                eq(ImmutableList.of(USER_ID_1, USER_ID_2)), eq(newRequiredApprovals));
        verify(eventPublisher).publish(any(PullRequestConditionUpdatedEvent.class));
    }

    @Test
    public void testDeletePullRequestCondition() {
        when(pullRequestConditionDao.get(anyInt())).thenReturn(aoPullRequestCondition1);

        service.removePullRequestCondition(repository, 1);

        verify(pullRequestConditionDao).delete(eq(aoPullRequestCondition1));
        verify(eventPublisher).publish(any(PullRequestConditionDeletedEvent.class));
    }

    @Test(expected = IllegalArgumentException.class)
    public void testDeletePullRequestConditionWithWrongRepositoryId() {
        when(repository.getId()).thenReturn(2);
        when(pullRequestConditionDao.get(anyInt())).thenReturn(aoPullRequestCondition1);

        service.removePullRequestCondition(repository, 1);
    }

    // Helper methods
    private static AoPullRequestCondition mockAoPullRequestCondition(int id, int repositoryId,
                                                                     String sourceMatcherId, String sourceMatcherType,
                                                                     String targetMatcherId, String targetMatcherType,
                                                                     AoReviewer[] reviewers, int requiredApprovals) {
        AoPullRequestCondition mockCondition = mock(AoPullRequestCondition.class);

        when(mockCondition.getId()).thenReturn(id);
        when(mockCondition.getRepositoryId()).thenReturn(repositoryId);

        when(mockCondition.getSourceMatcherId()).thenReturn(sourceMatcherId);
        when(mockCondition.getSourceMatcherType()).thenReturn(sourceMatcherType);
        when(mockCondition.getTargetMatcherId()).thenReturn(targetMatcherId);
        when(mockCondition.getTargetMatcherType()).thenReturn(targetMatcherType);

        when(mockCondition.getReviewers()).thenReturn(reviewers);
        when(mockCondition.getRequiredApprovals()).thenReturn(requiredApprovals);

        return mockCondition;
    }

    private static AoReviewer mockAoReviewer(int id, int userId) {
        AoReviewer mockAoReviewer = mock(AoReviewer.class);

        when(mockAoReviewer.getId()).thenReturn(id);
        when(mockAoReviewer.getUserId()).thenReturn(userId);

        return mockAoReviewer;
    }

    private static Branch toBranch(String branch, boolean isDefault) {
        return new SimpleBranch.Builder()
                .displayId(branch)
                .isDefault(isDefault)
                .build();
    }

    private class InactiveTestApplicationUser extends TestApplicationUser {

        InactiveTestApplicationUser(String name, int id) {
            super(name, id);
        }

        @Override
        public boolean isActive() {
            return false;
        }
    }
}
