package com.atlassian.stash.internal.repository.ref.restriction;

import com.atlassian.bitbucket.auth.AuthenticationContext;
import com.atlassian.bitbucket.commit.CommitService;
import com.atlassian.bitbucket.commit.CommitsBetweenRequest;
import com.atlassian.bitbucket.i18n.I18nService;
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.*;
import com.atlassian.bitbucket.server.ApplicationPropertiesService;
import com.atlassian.bitbucket.user.*;
import com.atlassian.bitbucket.util.Page;
import com.atlassian.bitbucket.util.PageImpl;
import com.atlassian.bitbucket.util.PageRequest;
import com.atlassian.bitbucket.util.PageUtils;
import com.atlassian.crowd.embedded.impl.IdentifierUtils;
import com.atlassian.event.api.EventPublisher;
import com.atlassian.sal.api.transaction.TransactionCallback;
import com.atlassian.sal.api.transaction.TransactionTemplate;
import com.atlassian.stash.internal.repository.ref.restriction.dao.AoAccessGrant;
import com.atlassian.stash.internal.repository.ref.restriction.dao.AoRefRestriction;
import com.atlassian.stash.internal.repository.ref.restriction.dao.RefRestrictionDao;
import com.atlassian.stash.internal.repository.ref.restriction.provider.BranchMatcher;
import com.atlassian.stash.internal.repository.ref.restriction.provider.BranchMatcherProvider;
import com.atlassian.stash.internal.repository.ref.restriction.provider.PatternMatcherProvider;
import com.google.common.collect.*;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.Spy;
import org.mockito.runners.MockitoJUnitRunner;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

import static com.atlassian.bitbucket.repository.ref.restriction.RefRestrictionType.*;
import static org.hamcrest.CoreMatchers.hasItem;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.junit.Assert.*;
import static org.mockito.Matchers.any;
import static org.mockito.Matchers.anyInt;
import static org.mockito.Matchers.eq;
import static org.mockito.Matchers.isA;
import static org.mockito.Mockito.anyCollectionOf;
import static org.mockito.Mockito.anySetOf;
import static org.mockito.Mockito.anyString;
import static org.mockito.Mockito.*;

@RunWith(MockitoJUnitRunner.class)
public class DefaultRefRestrictionServiceTest {

    private static final int CURRENT_USER_ID = 1;
    private static final String FROM_HASH = "0123456789abcdef0123456789abcdef01234567";
    private static final String TO_HASH = "123456789abcdef0123456789abcdef012345678";
    private static final int MAX_RESOURCES = 100;
    private static final int MAX_GRANTED_ACCESS_PER_RESOURCE = 50;
    private static final int REPOSITORY_ID = 1;
    private static final String GROUP_NAME = "bitbucket-users";

    private static final RefMatcherType BRANCH_MATCHER = mockRefMatcherType("BRANCH", "Branch");
    private static final RefMatcherType PATTERN_MATCHER = mockRefMatcherType("PATTERN", "Pattern");

    private static final List<RefMatcher> REF_MATCHERS = ImmutableList.of(
            mockRefMatcher("refs/heads/branch-1", "branch-1", BRANCH_MATCHER),
            mockRefMatcher("refs/heads/branch-2", "branch-2", BRANCH_MATCHER),
            mockRefMatcher("refs/heads/branch-3", "branch-3", BRANCH_MATCHER),
            mockRefMatcher("refs/heads/branch-4", "branch-4", BRANCH_MATCHER),
            mockRefMatcher("master", "master", PATTERN_MATCHER),
            mockRefMatcher("master-1", "master-1", PATTERN_MATCHER),
            mockRefMatcher("master-2", "master-2", PATTERN_MATCHER),
            mockRefMatcher("master-3", "master-3", PATTERN_MATCHER)
    );

    private static final List<AoRefRestriction> READ_ONLY_RESTRICTIONS = ImmutableList.of(
            mockAoRefRestriction(1, REF_MATCHERS.get(0), REPOSITORY_ID, READ_ONLY),
            mockAoRefRestriction(2, REF_MATCHERS.get(1), REPOSITORY_ID, READ_ONLY),
            mockAoRefRestriction(3, REF_MATCHERS.get(2), REPOSITORY_ID, READ_ONLY),
            mockAoRefRestriction(4, REF_MATCHERS.get(3), REPOSITORY_ID, READ_ONLY),
            mockAoRefRestriction(5, REF_MATCHERS.get(4), REPOSITORY_ID, READ_ONLY),
            mockAoRefRestriction(6, REF_MATCHERS.get(5), REPOSITORY_ID, READ_ONLY),
            mockAoRefRestriction(7, REF_MATCHERS.get(6), REPOSITORY_ID, READ_ONLY),
            mockAoRefRestriction(8, REF_MATCHERS.get(7), REPOSITORY_ID, READ_ONLY)
    );

    private static final List<AoRefRestriction> FAST_FORWARD_ONLY_RESTRICTIONS = ImmutableList.of(
            mockAoRefRestriction(1, REF_MATCHERS.get(0), REPOSITORY_ID, FAST_FORWARD_ONLY),
            mockAoRefRestriction(2, REF_MATCHERS.get(1), REPOSITORY_ID, FAST_FORWARD_ONLY),
            mockAoRefRestriction(3, REF_MATCHERS.get(2), REPOSITORY_ID, FAST_FORWARD_ONLY),
            mockAoRefRestriction(4, REF_MATCHERS.get(3), REPOSITORY_ID, FAST_FORWARD_ONLY),
            mockAoRefRestriction(5, REF_MATCHERS.get(4), REPOSITORY_ID, FAST_FORWARD_ONLY),
            mockAoRefRestriction(6, REF_MATCHERS.get(5), REPOSITORY_ID, FAST_FORWARD_ONLY),
            mockAoRefRestriction(7, REF_MATCHERS.get(6), REPOSITORY_ID, FAST_FORWARD_ONLY),
            mockAoRefRestriction(8, REF_MATCHERS.get(7), REPOSITORY_ID, FAST_FORWARD_ONLY)
    );

    private static final List<AoRefRestriction> NO_DELETES_RESTRICTIONS = ImmutableList.of(
            mockAoRefRestriction(1, REF_MATCHERS.get(0), REPOSITORY_ID, NO_DELETES),
            mockAoRefRestriction(2, REF_MATCHERS.get(1), REPOSITORY_ID, NO_DELETES),
            mockAoRefRestriction(3, REF_MATCHERS.get(2), REPOSITORY_ID, NO_DELETES),
            mockAoRefRestriction(4, REF_MATCHERS.get(3), REPOSITORY_ID, NO_DELETES),
            mockAoRefRestriction(5, REF_MATCHERS.get(4), REPOSITORY_ID, NO_DELETES),
            mockAoRefRestriction(6, REF_MATCHERS.get(5), REPOSITORY_ID, NO_DELETES),
            mockAoRefRestriction(7, REF_MATCHERS.get(6), REPOSITORY_ID, NO_DELETES),
            mockAoRefRestriction(8, REF_MATCHERS.get(7), REPOSITORY_ID, NO_DELETES)
    );

    @Mock
    private BranchMatcher branchMatcher;
    private ApplicationUser currentUser;
    @Mock
    private CommitService commitService;
    @Mock
    private RefRestrictionDao dao;
    @Mock
    private EventPublisher eventPublisher;
    @Mock
    private I18nService i18nService;
    @Mock
    private PermissionService permissionService;
    @Mock
    private AuthenticationContext authenticationContext;
    @Mock
    private RefService refService;
    @Mock
    private ApplicationPropertiesService propertiesService;
    @Mock
    private RefMatcherProviderRegistry refMatcherProviderRegistry;
    @Mock
    private Repository repository;
    @Mock
    private RepositorySupplier repositorySupplier;
    @Spy
    private SecurityService securityService = new DummySecurityService();
    @Mock
    private TransactionTemplate transactionTemplate;
    @Mock
    private PermissionValidationService validationService;
    @Mock
    private UserService userService;

    private DefaultRefRestrictionService underTest;

    @Before
    public void setup() {
        currentUser = mockUser(CURRENT_USER_ID);
        repository = mockRepository(REPOSITORY_ID);

        when(authenticationContext.getCurrentUser()).thenReturn(currentUser);
        // All users are in the GROUP_NAME group
        when(userService.findGroupsByUser(anyString(), any()))
                .thenReturn(PageUtils.createPage(ImmutableList.of(GROUP_NAME), PageUtils.newRequest(0, 1)));
        // Return current user for any ID(s)
        when(userService.getUsersById(anySetOf(Integer.class), eq(true))).thenReturn(ImmutableSet.of(currentUser));

        // Set up the limits for restrictions
        when(propertiesService.getPluginProperty(eq(RestrictionConstants.PROPERTY_MAX_RESOURCES), eq(MAX_RESOURCES)))
                .thenReturn(MAX_RESOURCES);

        // Set up the limits for Access Grants
        when(propertiesService.getPluginProperty(eq(RestrictionConstants.PROPERTY_MAX_GRANTED_ACCESS_PER_RESOURCE), eq(MAX_GRANTED_ACCESS_PER_RESOURCE)))
                .thenReturn(MAX_GRANTED_ACCESS_PER_RESOURCE);

        // Execute all transaction callbacks immediately
        when(transactionTemplate.execute(isA(TransactionCallback.class))).thenAnswer(invocationOnMock ->
                ((TransactionCallback) invocationOnMock.getArguments()[0]).doInTransaction());

        when(permissionService.hasRepositoryPermission(isA(Repository.class), eq(Permission.REPO_WRITE))).thenReturn(true);

        // Set up a branch and pattern matcher provider
        ArrayList<RefMatcherProvider> providers = new ArrayList<>();
        providers.add(new BranchMatcherProvider(propertiesService, refService));
        providers.add(new PatternMatcherProvider(propertiesService));
        when(refMatcherProviderRegistry.getProviders()).thenReturn(providers);

        underTest = new DefaultRefRestrictionService(authenticationContext, commitService, dao, eventPublisher,
                i18nService, permissionService, propertiesService, refMatcherProviderRegistry, repositorySupplier,
                securityService, transactionTemplate, userService, validationService);
    }

    @Test
    public void getRestrictionById() {
        AoRefRestriction expected = FAST_FORWARD_ONLY_RESTRICTIONS.get(3);
        when(expected.getAccessGrants()).thenReturn(new AoAccessGrant[]{});
        when(dao.getById(eq(expected.getId()))).thenReturn(expected);
        Repository repository = mockRepository(expected.getRepositoryId());

        RefRestriction result = underTest.getById(expected.getId());

        verify(validationService).validateForRepository(eq(repository), eq(Permission.REPO_ADMIN));
        assertNotNull(result);
        assertCorrectRestriction(expected, result);
    }

    @Test
    public void getRestrictionByIdNoRepo() {
        AoRefRestriction expected = mockRestriction("master", "BRANCH", NO_DELETES);
        when(expected.getId()).thenReturn(7);
        when(expected.getRepositoryId()).thenReturn(999);
        when(dao.getById(7)).thenReturn(expected);

        RefRestriction result = underTest.getById(expected.getId());

        verify(dao, times(1)).delete(expected);
        assertNull(result);
    }

    @Test
    public void removeRestrictionById() {
        AoRefRestriction expected = FAST_FORWARD_ONLY_RESTRICTIONS.get(2);
        when(dao.getById(eq(expected.getId()))).thenReturn(expected);

        boolean deleted = underTest.removeRefRestriction(expected.getId());

        verify(dao).getById(expected.getId());
        verify(dao).delete(expected);
        verify(eventPublisher).publish(isA(RefRestrictionDeletedEvent.class));
        assertTrue(deleted);
    }

    @Test
    public void removeRestrictionByIdNoRepository() {
        AoRefRestriction expected = mockRestriction("branch", "BRANCH", NO_DELETES);
        when(expected.getId()).thenReturn(7);
        when(expected.getRepositoryId()).thenReturn(991);
        when(dao.getById(7)).thenReturn(expected);

        boolean deleted = underTest.removeRefRestriction(expected.getId());

        verify(dao).getById(expected.getId());
        verify(repositorySupplier).getById(expected.getRepositoryId());
        assertFalse(deleted);
    }

    @Test
    public void removeRestrictionByIdNoRestriction() {
        boolean deleted = underTest.removeRefRestriction(64);

        verify(dao).getById(64);
        assertFalse(deleted);
    }

    @Test
    public void searchForRestriction() {
        PageRequest pageRequest = PageUtils.newRequest(0, 1048575);
        RestrictionSearchRequest searchRequest = new RestrictionSearchRequest.Builder(repository).build();
        when(dao.find(eq(REPOSITORY_ID), anyCollectionOf(RefRestrictionType.class), any(RefMatcher.class),
                anyCollectionOf(RefMatcherType.class), isA(PageRequest.class)))
                .thenReturn(new PageImpl<>(pageRequest, new ArrayList<>(), false));

        underTest.search(searchRequest, pageRequest);

        verify(dao).find(eq(REPOSITORY_ID), anyCollectionOf(RefRestrictionType.class), any(RefMatcher.class),
                anyCollectionOf(RefMatcherType.class), eq(PageUtils.newRequest(0, MAX_RESOURCES)));
    }

    @Test
    public void setMultipleRestrictions() {
        List<AoRefRestriction> expected = ImmutableList.of(
                FAST_FORWARD_ONLY_RESTRICTIONS.get(0),
                NO_DELETES_RESTRICTIONS.get(1)
        );

        List<SetRestrictionRequest> requests = ImmutableList.of(
                new SetRestrictionRequest.Builder(
                        repository,
                        REF_MATCHERS.get(0),
                        FAST_FORWARD_ONLY)
                        .build(),
                new SetRestrictionRequest.Builder(
                        repository,
                        REF_MATCHERS.get(1),
                        NO_DELETES)
                        .build()

        );

        when(dao.create(eq(repository.getId()), isA(RefRestrictionType.class), eq(REF_MATCHERS.get(0)),
                anyCollectionOf(String.class), anyCollectionOf(Integer.class))).thenReturn(expected.get(0));

        when(dao.create(eq(repository.getId()), isA(RefRestrictionType.class), eq(REF_MATCHERS.get(1)),
                anyCollectionOf(String.class), anyCollectionOf(Integer.class))).thenReturn(expected.get(1));

        List<RefRestriction> restrictions = underTest.setRestrictions(requests);

        verify(dao, times(1)).create(eq(repository.getId()), isA(RefRestrictionType.class), eq(REF_MATCHERS.get(0)),
                anyCollectionOf(String.class), anyCollectionOf(Integer.class));
        verify(dao, times(1)).create(eq(repository.getId()), isA(RefRestrictionType.class), eq(REF_MATCHERS.get(1)),
                anyCollectionOf(String.class), anyCollectionOf(Integer.class));

        assertEquals(restrictions.size(), requests.size());
        assertCorrectRestriction(expected.get(0), restrictions.get(0));
        assertCorrectRestriction(expected.get(1), restrictions.get(1));
    }

    @Test
    public void updatesAccessGrantsForExistingRestriction() throws Throwable {
        AoRefRestriction expected = mockRestriction("bla", "BRANCH", READ_ONLY, "everyone");

        SetRestrictionRequest request = new SetRestrictionRequest.Builder(
                repository,
                REF_MATCHERS.get(0),
                READ_ONLY).grantGroups(ImmutableList.of("everyone")).build();

        when(dao.get(eq(repository.getId()), eq(READ_ONLY), isA(RefMatcher.class))).thenReturn(expected);
        when(dao.setAccessGrants(eq(expected), anyCollectionOf(String.class), anyCollectionOf(Integer.class)))
                .thenReturn(expected);

        RefRestriction restriction = underTest.setRestriction(request);

        assertNotNull(restriction);
        assertCorrectRestriction(expected, restriction);
        assertEquals(1, restriction.getAccessGrants().size());

        verify(dao).get(eq(expected.getRepositoryId()), eq(READ_ONLY), isA(RefMatcher.class));
        verify(dao).setAccessGrants(eq(expected), eq(request.getGroupGrants()), anyCollectionOf(Integer.class));
    }

    @Test
    public void noDeleteRestrictionBlocksDelete() throws Throwable {
        setUpMockSearch(ImmutableList.of(mockRestriction("master", "BRANCH", NO_DELETES)));

        RefAccessRequest accessRequest = new RefAccessRequest.Builder(repository, RefAccessType.DELETE)
                .ref(mockBranch("master"))
                .build();

        assertFalse(underTest.hasPermission(accessRequest));
    }

    @Test
    public void noDeleteRestrictionWithGrantAllowsDelete() throws Throwable {
        setUpMockSearch(ImmutableList.of(mockRestriction("bar", "BRANCH", NO_DELETES, currentUser.getId())));

        RefAccessRequest accessRequest = new RefAccessRequest.Builder(repository, RefAccessType.DELETE)
                .ref(mockBranch("bar"))
                .build();

        assertTrue(underTest.hasPermission(accessRequest));
    }

    @Test
    public void accessGrantOnReadOnlyDoesNotCancelOutDeleteRestriction() throws Throwable {
        setUpMockSearch(ImmutableList.of(
                mockRestriction("foo", "BRANCH", NO_DELETES),
                mockRestriction("foo", "BRANCH", READ_ONLY, currentUser.getId())));

        RefAccessRequest accessRequest = new RefAccessRequest.Builder(repository, RefAccessType.DELETE)
                .ref(mockBranch("foo"))
                .build();

        assertFalse(underTest.hasPermission(accessRequest));
    }

    @Test
    public void hasPermissionWithoutRestrictions() {
        setUpMockSearch(ImmutableList.of());

        RefAccessRequest accessRequest = new RefAccessRequest.Builder(repository, RefAccessType.UPDATE)
                .ref(mockBranch("master"))
                .build();
        assertTrue(underTest.hasPermission(accessRequest));
    }

    @Test
    public void hasPermissionWithGroupGrant() {
        String groupName = "TEST#GROUP!@#$%^&*()İI";
        setUpMockSearch(ImmutableList.of(mockRestriction("master", "BRANCH", READ_ONLY, groupName)));

        when(userService.findGroupsByUser(eq(currentUser.getName()), isA(PageRequest.class)))
                .thenReturn(
                        PageUtils.createPage(
                                ImmutableList.of(IdentifierUtils.toLowerCase(groupName), "another-group"),
                                PageUtils.newRequest(0, 2)
                        )
                );

        RefAccessRequest accessRequest = new RefAccessRequest.Builder(repository, RefAccessType.UPDATE)
                .ref(mockBranch("master"))
                .build();
        assertTrue(underTest.hasPermission(accessRequest));
    }

    @Test
    public void pullRequestOnlyBlocksUpdate() throws Throwable {
        setUpMockSearch(ImmutableList.of(mockRestriction("foo", "BRANCH", PULL_REQUEST_ONLY)));

        RestrictionMatchRequest request = new RestrictionMatchRequest.Builder(repository, ImmutableList.of(PULL_REQUEST_ONLY))
                .refChange(mockRefChange("foo", RefChangeType.UPDATE))
                .user(currentUser)
                .build();

        assertTrue(!underTest.match(request).isEmpty());
    }

    @Test
    public void accessGrantOnPullRequestOnlyAllowsUpdate() throws Throwable {
        setUpMockSearch(ImmutableList.of(mockRestriction("foo", "BRANCH", PULL_REQUEST_ONLY, currentUser.getId())));

        RestrictionMatchRequest request = new RestrictionMatchRequest.Builder(repository, ImmutableList.of(PULL_REQUEST_ONLY))
                .refChange(mockRefChange("foo", RefChangeType.UPDATE))
                .user(currentUser)
                .build();

        assertTrue(underTest.match(request).isEmpty());
    }

    @Test
    public void accessGrantForDifferentUserOnPullRequestOnlyBlocksUpdate() throws Throwable {
        setUpMockSearch(ImmutableList.of(mockRestriction("foo", "BRANCH", PULL_REQUEST_ONLY, "hipster")));

        RestrictionMatchRequest request = new RestrictionMatchRequest.Builder(repository, ImmutableList.of(PULL_REQUEST_ONLY))
                .refChange(mockRefChange("foo", RefChangeType.UPDATE))
                .user(currentUser)
                .build();

        assertTrue(!underTest.match(request).isEmpty());
    }

    @Test
    public void accessGrantAllowsAccessWhenMultipleRestrictionsMatch1() throws Throwable {
        // See https://jira.atlassian.com/browse/BSERV-7597
        setUpMockSearch(ImmutableList.of(
                mockRestriction("*", "PATTERN", READ_ONLY, "user1"),
                mockRestriction("feature/*", "PATTERN", READ_ONLY, currentUser.getId())));

        RestrictionMatchRequest request = new RestrictionMatchRequest.Builder(repository, ImmutableList.of(READ_ONLY))
                .refChange(mockRefChange("feature/123", RefChangeType.UPDATE))
                .user(currentUser)
                .build();

        assertTrue(underTest.match(request).isEmpty());
    }

    @Test
    public void accessGrantAllowsAccessWhenMultipleRestrictionsMatch2() throws Throwable {
        setUpMockSearch(ImmutableList.of(
                mockRestriction("release/*", "PATTERN", PULL_REQUEST_ONLY),
                mockRestriction("release/4.0", "BRANCH", PULL_REQUEST_ONLY, currentUser.getId())));

        RestrictionMatchRequest request = new RestrictionMatchRequest.Builder(repository, ImmutableList.of(READ_ONLY))
                .refChange(mockRefChange("release/4.0", RefChangeType.UPDATE))
                .user(currentUser)
                .build();

        assertTrue(underTest.match(request).isEmpty());
    }

    @Test
    public void accessGrantAllowsAccessWhenMultipleRestrictionsMatch3() throws Throwable {
        setUpMockSearch(ImmutableList.of(
                mockRestriction("release/*", "PATTERN", PULL_REQUEST_ONLY, currentUser.getId()),
                mockRestriction("release/4.0", "BRANCH", PULL_REQUEST_ONLY)));

        RestrictionMatchRequest request = new RestrictionMatchRequest.Builder(repository, ImmutableList.of(PULL_REQUEST_ONLY))
                .refChange(mockRefChange("release/4.0", RefChangeType.UPDATE))
                .user(currentUser)
                .build();

        // Yes, it may seem counter-intuitive but an access grant on a PATTERN restriction can override a grant-less
        // BRANCH restriction, even though the latter may seem "more specific".  See
        // {@link DefaultRefRestrictionService.match()}.
        assertTrue(underTest.match(request).isEmpty());
    }

    @Test
    public void matchWithNullUserWorks() throws Throwable {
        setUpMockSearch(ImmutableList.of(mockRestriction("foo", "BRANCH", PULL_REQUEST_ONLY, currentUser.getId())));

        RestrictionMatchRequest request = new RestrictionMatchRequest.Builder(repository, ImmutableList.of(PULL_REQUEST_ONLY))
                .refChange(mockRefChange("foo", RefChangeType.UPDATE))
                .user(null)
                .build();

        assertTrue(!underTest.match(request).isEmpty());
    }

    @Test
    public void updateCallsCommitService() {
        setUpMockSearch(ImmutableList.of(mockRestriction("master", "BRANCH", FAST_FORWARD_ONLY)));

        RefChange updateMaster = mockRefChange("master", RefChangeType.UPDATE);
        PageRequest commitPageRequest = PageUtils.newRequest(0, 1);
        Page result = commitsBetweenPageResult();
        when(commitService.getCommitsBetween(any(CommitsBetweenRequest.class), eq(commitPageRequest))).thenReturn(result);
        ArgumentCaptor<CommitsBetweenRequest> commitsBetweenRequest = ArgumentCaptor.forClass(CommitsBetweenRequest.class);

        RestrictionMatchRequest request = new RestrictionMatchRequest.Builder(repository, ImmutableList.of(FAST_FORWARD_ONLY))
                .refChange(updateMaster)
                .user(null)
                .build();
        assertTrue(!underTest.match(request).isEmpty());

        verify(commitService).getCommitsBetween(commitsBetweenRequest.capture(), eq(commitPageRequest));
        assertThat(commitsBetweenRequest.getValue().getIncludes(), hasItem(updateMaster.getFromHash()));
        assertThat(commitsBetweenRequest.getValue().getExcludes(), hasItem(updateMaster.getToHash()));
    }

    @Test
    public void nonFastForwardUpdateWithFastForwardOnlyRestrictionTriggersError() {
        setUpMockSearch(ImmutableList.of(mockRestriction("master", "BRANCH", FAST_FORWARD_ONLY)));

        RefChange updateMaster = mockRefChange("master", RefChangeType.UPDATE);
        PageRequest commitPageRequest = PageUtils.newRequest(0, 1);
        Page result = commitsBetweenPageResult();
        when(commitService.getCommitsBetween(any(CommitsBetweenRequest.class), eq(commitPageRequest))).thenReturn(result);

        RestrictionMatchRequest request = new RestrictionMatchRequest.Builder(repository, ImmutableList.of(FAST_FORWARD_ONLY))
                .refChange(updateMaster)
                .user(null)
                .build();
        assertTrue(!underTest.match(request).isEmpty());
    }

    @Test
    public void anyRefChangeTypeOnBranchWithReadOnlyRestrictionTriggersError() {
        setUpMockSearch(ImmutableList.of(mockRestriction("master", "BRANCH", READ_ONLY)));

        RefChange updateMaster = mockRefChange("master", RefChangeType.UPDATE);
        RefChange addToMaster = mockRefChange("master", RefChangeType.ADD);
        RefChange deleteFromMaster = mockRefChange("master", RefChangeType.DELETE);

        PageRequest commitPageRequest = PageUtils.newRequest(0, 1);
        Page result = commitsBetweenPageResult();
        when(commitService.getCommitsBetween(any(CommitsBetweenRequest.class), eq(commitPageRequest))).thenReturn(result);

        RestrictionMatchRequest request = new RestrictionMatchRequest.Builder(repository, ImmutableList.of(READ_ONLY))
                .refChanges(ImmutableList.of(updateMaster, addToMaster, deleteFromMaster))
                .user(null)
                .build();

        Multimap<RefChange, RefRestriction> restrictionMap = underTest.match(request);

        assertEquals(3, restrictionMap.size());
        assertTrue(restrictionMap.get(updateMaster).removeIf(r -> r.getType() == READ_ONLY));
        assertTrue(restrictionMap.get(addToMaster).removeIf(r -> r.getType() == READ_ONLY));
        assertTrue(restrictionMap.get(deleteFromMaster).removeIf(r -> r.getType() == READ_ONLY));
    }

    @Test
    public void triggersAllRestrictions() {
        setUpMockSearch(ImmutableList.of(
                mockRestriction("master", "BRANCH", READ_ONLY),
                mockRestriction("master", "BRANCH", PULL_REQUEST_ONLY),
                mockRestriction("master", "BRANCH", FAST_FORWARD_ONLY),
                mockRestriction("master", "BRANCH", NO_DELETES)));

        RefChange updateMaster = mockRefChange("master", RefChangeType.UPDATE);
        RefChange addToMaster = mockRefChange("master", RefChangeType.ADD);
        RefChange deleteFromMaster = mockRefChange("master", RefChangeType.DELETE);

        PageRequest commitPageRequest = PageUtils.newRequest(0, 1);
        Page result = commitsBetweenPageResult();
        when(commitService.getCommitsBetween(any(CommitsBetweenRequest.class), eq(commitPageRequest))).thenReturn(result);

        RestrictionMatchRequest request = new RestrictionMatchRequest.Builder(repository, Collections.emptyList())
                .refChanges(ImmutableList.of(updateMaster, addToMaster, deleteFromMaster))
                .user(null)
                .build();

        Multimap<RefChange, RefRestriction> restrictionMap = underTest.match(request);

        assertEquals(6, restrictionMap.size());
        assertEquals(3, restrictionMap.get(updateMaster).size());
        assertTrue(restrictionMap.get(updateMaster).removeIf(r -> r.getType() == READ_ONLY));
        assertTrue(restrictionMap.get(updateMaster).removeIf(r -> r.getType() == PULL_REQUEST_ONLY));
        assertTrue(restrictionMap.get(updateMaster).removeIf(r -> r.getType() == FAST_FORWARD_ONLY));
        assertEquals(1, restrictionMap.get(addToMaster).size());
        assertTrue(restrictionMap.get(addToMaster).removeIf(r -> r.getType() == READ_ONLY));
        assertEquals(2, restrictionMap.get(deleteFromMaster).size());
        assertTrue(restrictionMap.get(deleteFromMaster).removeIf(r -> r.getType() == READ_ONLY));
        assertTrue(restrictionMap.get(deleteFromMaster).removeIf(r -> r.getType() == NO_DELETES));
    }

    @Test
    public void readOnlyRestrictionBlocksCreate() throws Throwable {
        setUpMockSearch(ImmutableList.of(mockRestriction("master", "BRANCH", READ_ONLY)));

        RefAccessRequest accessRequest = new RefAccessRequest.Builder(repository, RefAccessType.CREATE)
                .ref(mockBranch("master"))
                .build();
        assertFalse(underTest.hasPermission(accessRequest));
    }

    @Test
    public void readOnlyRestrictionBlocksDelete() throws Throwable {
        setUpMockSearch(ImmutableList.of(mockRestriction("master", "BRANCH", READ_ONLY)));

        RefAccessRequest accessRequest = new RefAccessRequest.Builder(repository, RefAccessType.CREATE)
                .ref(mockBranch("master"))
                .build();
        assertFalse(underTest.hasPermission(accessRequest));
    }

    @Test
    public void readOnlyRestrictionBlocksUpdate() throws Throwable {
        setUpMockSearch(ImmutableList.of(mockRestriction("master", "BRANCH", READ_ONLY)));

        RefAccessRequest accessRequest = new RefAccessRequest.Builder(repository, RefAccessType.UPDATE)
                .ref(mockBranch("master"))
                .build();
        assertFalse(underTest.hasPermission(accessRequest));
    }

    @Test
    public void readOnlyRestrictionWithAccessAllowsUpdate() throws Throwable {
        setUpMockSearch(ImmutableList.of(mockRestriction("master", "BRANCH", READ_ONLY, currentUser.getId())));

        RefAccessRequest accessRequest = new RefAccessRequest.Builder(repository, RefAccessType.UPDATE)
                .ref(mockBranch("master"))
                .build();
        assertTrue(underTest.hasPermission(accessRequest));
    }

    @Test
    public void readOnlyRestrictionWithoutAccessBlocksUpdate() throws Throwable {
        setUpMockSearch(ImmutableList.of(mockRestriction("master", "BRANCH", READ_ONLY, "hipster")));

        RefAccessRequest accessRequest = new RefAccessRequest.Builder(repository, RefAccessType.UPDATE)
                .ref(mockBranch("master"))
                .build();
        assertFalse(underTest.hasPermission(accessRequest));
    }

    @Test
    public void readOnlyRestrictionWithGroupAccessAllowsUpdate() throws Throwable {
        setUpMockSearch(ImmutableList.of(mockRestriction("master", "BRANCH", READ_ONLY, "hipster")));

        RefAccessRequest accessRequest = new RefAccessRequest.Builder(repository, RefAccessType.UPDATE)
                .ref(mockBranch(GROUP_NAME))
                .build();
        assertTrue(underTest.hasPermission(accessRequest));
    }

    private void setUpMockSearch(List<AoRefRestriction> restrictions) {
        when(dao.find(anyInt(), anyCollectionOf(RefRestrictionType.class), any(RefMatcher.class),
                anyCollectionOf(RefMatcherType.class), any(PageRequest.class)))
                .thenAnswer(invocation ->
                        PageUtils.createPage(restrictions, (PageRequest) invocation.getArguments()[4]));
    }

    private static void assertCorrectRestriction(AoRefRestriction in, RefRestriction out) {
        assertEquals(in.getId(), (Integer) out.getId());
        assertEquals(in.getMatcherId(), out.getMatcher().getId());
        assertEquals(in.getMatcherType(), out.getMatcher().getType().getId());
        assertEquals(in.getRepositoryId(), (Integer) out.getRepositoryId());
    }

    private Page commitsBetweenPageResult() {
        Page result = mock(Page.class);
        when(result.getSize()).thenReturn(1);
        return result;
    }

    private static AoAccessGrant grant(String group) {
        AoAccessGrant accessGrant = mock(AoAccessGrant.class);
        when(accessGrant.getGroup()).thenReturn(group);
        when(accessGrant.getUserId()).thenReturn(null);
        return accessGrant;
    }

    private static AoAccessGrant grant(Integer userId) {
        AoAccessGrant accessGrant = mock(AoAccessGrant.class);
        when(accessGrant.getUserId()).thenReturn(userId);
        return accessGrant;
    }

    private static AoRefRestriction mockAoRefRestriction(int id, RefMatcher matcher, int repositoryId, RefRestrictionType restrictionType) {
        AoRefRestriction mockAoRefRestriction = mock(AoRefRestriction.class);
        when(mockAoRefRestriction.getId()).thenReturn(id);
        when(mockAoRefRestriction.getRepositoryId()).thenReturn(repositoryId);

        String matcherId = matcher.getId();
        when(mockAoRefRestriction.getMatcherId()).thenReturn(matcherId);

        RefMatcherType type = matcher.getType();
        String typeId = type.getId();
        when(mockAoRefRestriction.getMatcherType()).thenReturn(typeId);
        when(mockAoRefRestriction.getType()).thenReturn(restrictionType.getId());
        when(mockAoRefRestriction.getAccessGrants()).thenReturn(new AoAccessGrant[]{});

        return mockAoRefRestriction;
    }

    private static Branch mockBranch(String refId) {
        Branch branch = mock(Branch.class);
        when(branch.getId()).thenReturn(refId);
        return branch;
    }

    private static RefChange mockRefChange(String refId, RefChangeType type) {
        Branch branch = mockBranch(refId);
        return new SimpleRefChange.Builder()
                .ref(branch)
                .type(type)
                .fromHash(FROM_HASH)
                .toHash(TO_HASH)
                .build();
    }

    private static AoRefRestriction mockRestriction(String ref, String matcherType, RefRestrictionType type, Object... grants) {
        AoRefRestriction restriction = mock(AoRefRestriction.class);
        when(restriction.getMatcherId()).thenReturn(ref);
        when(restriction.getMatcherType()).thenReturn(matcherType);
        when(restriction.getRepositoryId()).thenReturn(REPOSITORY_ID);
        when(restriction.getType()).thenReturn(type.getId());

        AoAccessGrant[] accessGrants = new AoAccessGrant[grants.length];
        for (int i = 0; i < grants.length; ++i) {
            if (grants[i] instanceof Integer) {
                accessGrants[i] = grant((Integer) grants[i]);
            } else {
                accessGrants[i] = grant((String) grants[i]);
            }
        }
        when(restriction.getAccessGrants()).thenReturn(accessGrants);
        return restriction;
    }

    private static ApplicationUser mockUser(int id) {
        ApplicationUser user = mock(ApplicationUser.class);
        when(user.getId()).thenReturn(id);
        when(user.getName()).thenReturn("user");
        when(user.getType()).thenReturn(UserType.NORMAL);

        return user;
    }

    private static RefMatcher mockRefMatcher(String id, String displayId, RefMatcherType type) {
        RefMatcher matcher = mock(RefMatcher.class);
        when(matcher.getId()).thenReturn(id);
        when(matcher.getDisplayId()).thenReturn(displayId);
        when(matcher.getType()).thenReturn(type);
        return matcher;
    }

    private static RefMatcherType mockRefMatcherType(String id, String displayId) {
        RefMatcherType type = mock(RefMatcherType.class);
        when(type.getDisplayId()).thenReturn(displayId);
        when(type.getId()).thenReturn(id);
        return type;
    }

    private Repository mockRepository(int id) {
        Repository repo = mock(Repository.class);
        when(repo.getId()).thenReturn(id);
        when(repositorySupplier.getById(id)).thenReturn(repo);
        return repo;
    }
}
