package com.atlassian.stash.internal.user;

import com.atlassian.bitbucket.cluster.ClusterNode;
import com.atlassian.bitbucket.event.cluster.ClusterNodeAddedEvent;
import com.atlassian.bitbucket.permission.*;
import com.atlassian.bitbucket.user.*;
import com.atlassian.cache.CacheFactory;
import com.atlassian.cache.memory.MemoryCacheManager;
import com.atlassian.crowd.embedded.impl.IdentifierUtils;
import com.atlassian.bitbucket.event.permission.GlobalPermissionRevokedEvent;
import com.atlassian.bitbucket.event.permission.ProjectPermissionGrantedEvent;
import com.atlassian.bitbucket.event.permission.ProjectPermissionModifiedEvent;
import com.atlassian.bitbucket.event.permission.ProjectPermissionRevokedEvent;
import com.atlassian.stash.internal.project.InternalProject;
import com.atlassian.stash.internal.repository.InternalRepository;
import com.atlassian.bitbucket.util.Page;
import com.atlassian.bitbucket.util.PageRequest;
import com.atlassian.bitbucket.util.PageUtils;
import com.google.common.collect.HashMultimap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Lists;
import com.google.common.collect.Multimap;
import org.apache.commons.lang3.StringUtils;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.runners.MockitoJUnitRunner;
import org.mockito.stubbing.Answer;
import org.springframework.transaction.PlatformTransactionManager;

import java.util.*;
import java.util.function.Consumer;

import static com.atlassian.bitbucket.permission.Permission.*;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.mockito.Matchers.any;
import static org.mockito.Matchers.anyString;
import static org.mockito.Matchers.eq;
import static org.mockito.Mockito.*;

@RunWith(MockitoJUnitRunner.class)
public class InternalUserPermissionGraphFactoryTest {

    private CacheFactory cacheFactory;
    @Mock
    private EffectivePermissionDao effectivePermissionDao;
    private InternalUserPermissionGraphFactory factory;

    private Multimap<String, InternalGrantedPermission> groupPermissions;
    @Mock
    private InternalProject project1;
    @Mock
    private InternalProject project2;
    @Mock
    private InternalProject project3;
    @Mock
    private InternalRepository repository1;
    @Mock
    private ProjectPermissionDao projectPermissionDao;
    @Mock
    private PlatformTransactionManager transactionManager;
    private InternalNormalUser user;
    private Multimap<Integer, InternalGrantedPermission> userPermissions;
    private Map<Integer, Permission> defaultPermissions;
    @Mock
    private UserService userService;
    @Mock
    private EffectivePermissionsProviderTracker permsProviderTracker;

    @Before
    public void setUp() {
        cacheFactory = new MemoryCacheManager();

        user = createMockNormalUser(1, "test-user");

        // create mock projects
        when(project1.getId()).thenReturn(1);
        when(project2.getId()).thenReturn(2);
        when(project3.getId()).thenReturn(3);

        // create mock repositories
        when(repository1.getId()).thenReturn(1);
        when(repository1.getProject()).thenReturn(project3);

        // mock default project permissions: p1: WRITE, p2: READ, p3: NONE
        defaultPermissions = new HashMap<>();
        defaultPermissions.put(1, PROJECT_WRITE);
        defaultPermissions.put(2, PROJECT_READ);
        when(projectPermissionDao.getDefaultPermissions()).thenAnswer(invocation -> defaultPermissions);

        groupPermissions = HashMultimap.create(10, 10);
        userPermissions = HashMultimap.create(10, 10);

        // mock findByGroup backed by the groupPermissions multimap
        when(effectivePermissionDao.findByGroup(anyString(), any(PageRequest.class)))
                .thenAnswer((Answer<Page<InternalGrantedPermission>>) invocation -> {
                    String group = IdentifierUtils.toLowerCase((String) invocation.getArguments()[0]);
                    List<InternalGrantedPermission> results = new ArrayList<>();
                    Collection<InternalGrantedPermission> groupPerms = groupPermissions.get(group);
                    if (groupPerms != null && !groupPerms.isEmpty()) {
                        results.addAll(groupPerms);
                    }

                    return PageUtils.createPage(results, (PageRequest) invocation.getArguments()[1]);
                });

        // mock findByUserId backed by the userPermissions multimap
        when(effectivePermissionDao.findByUserId(any(Integer.class), any(PageRequest.class)))
                .thenAnswer((Answer<Page<InternalGrantedPermission>>) invocation -> {
                    Integer userId = (Integer) invocation.getArguments()[0];
                    List<InternalGrantedPermission> results = new ArrayList<>();
                    Collection<InternalGrantedPermission> perms = userPermissions.get(userId);
                    if (perms != null && !perms.isEmpty()) {
                        results.addAll(perms);
                    }

                    return PageUtils.createPage(results, (PageRequest) invocation.getArguments()[1]);
                });

        // initialize the factory
        factory = buildFactory(50, 50);

        when(permsProviderTracker.getProviders()).thenReturn(Collections.emptySet());
    }

    @Test(expected = IllegalArgumentException.class)
    public void testErrorIfInternalUser() throws Exception {
        ApplicationUser user = UserTestUtils.createMockNormalUser(ApplicationUser.class);
        factory.createGraph(user);
    }

    @Test
    public void testAnyPermission() {
        // tests whether passing a null resourceId returns 'permission on any resource'
        mockNoGroupMemberships(user);
        PermissionGraph graph = factory.createGraph(user);

        assertTrue(graph.isGranted(PROJECT_WRITE, null));
    }

    @Test
    public void testClusterNodeAddedTriggersInvalidation() {
        mockGroupMemberships(user, "group-1", "group-2");

        factory.createGraph(user);
        factory.createGraph(user);

        // 2 createGraph calls should only trigger DB loads once
        verify(projectPermissionDao).getDefaultPermissions();
        verify(effectivePermissionDao).findByUserId(eq(1), any(PageRequest.class));
        verify(effectivePermissionDao).findByGroup(eq("group-1"), any(PageRequest.class));
        verify(effectivePermissionDao).findByGroup(eq("group-2"), any(PageRequest.class));

        ClusterNode node = mock(ClusterNode.class);
        factory.onClusterNodeAdded(new ClusterNodeAddedEvent(this, node, ImmutableSet.of(node)));

        factory.createGraph(user);
        factory.createGraph(user);

        // verify that the permissions are reloaded once
        verify(projectPermissionDao, times(2)).getDefaultPermissions();
        verify(effectivePermissionDao, times(2)).findByUserId(eq(1), any(PageRequest.class));
        verify(effectivePermissionDao, times(2)).findByGroup(eq("group-1"), any(PageRequest.class));
        verify(effectivePermissionDao, times(2)).findByGroup(eq("group-2"), any(PageRequest.class));
    }

    @Test
    public void testDefaultPermissions() {
        // mock empty lists to be returned from the user/group permission queries
        mockNoGroupMemberships(user);

        // test twice to verify cache miss and cache hit result in the same graph
        repeat(2, iteration -> {
            PermissionGraph graph = factory.createGraph(user);
            assertTrue(graph.isGranted(PROJECT_WRITE, project1));
            // check inherited permissions as well
            assertTrue(graph.isGranted(PROJECT_READ, project1));
            assertTrue(graph.isGranted(PROJECT_VIEW, project1));
            assertTrue(graph.isGranted(PROJECT_READ, project2));
            assertTrue(graph.isGranted(PROJECT_VIEW, project2));

            // make sure no other permissions are granted
            assertFalse(graph.isGranted(PROJECT_VIEW, project3));
            assertFalse(graph.isGranted(PROJECT_WRITE, project2));
            assertFalse(graph.isGranted(PROJECT_ADMIN, project1));
        });
    }

    @Test
    public void testGroupPermissions() {
        mockGroupMemberships(user, "ProjectAdmin");
        InternalNormalUser user2 = createMockNormalUser(2, "other");
        mockGroupMemberships(user2, "stash-user", "bla");

        mockPermissions(
                new InternalGlobalPermission.Builder().group("ProjectAdmin").permission(PROJECT_CREATE).build(),
                new InternalGlobalPermission.Builder().group("stash-user").permission(LICENSED_USER).build(),
                new InternalGlobalPermission.Builder().group("bla").permission(LICENSED_USER).build());

        // test twice to verify cache miss and cache hit result in the same graph
        repeat(2, iteration -> {
            PermissionGraph graph = factory.createGraph(user);
            assertTrue(graph.isGranted(PROJECT_CREATE, null));
            assertTrue(graph.isGranted(LICENSED_USER, null)); // inherited from PROJECT_CREATE
            assertFalse(graph.isGranted(SYS_ADMIN, null));

            PermissionGraph graph2 = factory.createGraph(user2);
            assertTrue(graph2.isGranted(LICENSED_USER, null));
            assertFalse(graph2.isGranted(PROJECT_CREATE, null));
        });
    }

    @Test
    public void testMixedPermissions() {
        // tests case where both user and group level permissions are granted
        mockGroupMemberships(user, "ProjectAdmin");

        mockPermissions(
                new InternalGlobalPermission.Builder().group("ProjectAdmin").permission(PROJECT_CREATE).build(),
                new InternalProjectPermission.Builder().user(user).permission(PROJECT_ADMIN).project(project3).build());

        // test twice to verify cache miss and cache hit result in the same graph
        repeat(2, iteration -> {
            PermissionGraph graph = factory.createGraph(user);

            // check group permissions
            assertTrue(graph.isGranted(PROJECT_CREATE, null));
            assertTrue(graph.isGranted(LICENSED_USER, null)); // inherited from PROJECT_CREATE
            assertFalse(graph.isGranted(ADMIN, null));

            // check user permissions
            assertTrue(graph.isGranted(PROJECT_ADMIN, project3));
            assertTrue(graph.isGranted(PROJECT_WRITE, project3));
            assertFalse(graph.isGranted(PROJECT_ADMIN, project2));

            // check default permissions
            assertFalse(graph.isGranted(PROJECT_ADMIN, project1));
            assertTrue(graph.isGranted(PROJECT_WRITE, project1));
            assertFalse(graph.isGranted(PROJECT_WRITE, project2));
            assertTrue(graph.isGranted(PROJECT_READ, project2));
        });
    }

    @Test
    public void testGroupPermissionsAtMultipleLevels() {
        // tests case where both user and group level permissions are granted
        mockGroupMemberships(user, "atlassian-dev");

        mockPermissions(
                new InternalRepositoryPermission.Builder().group("atlassian-dev").permission(REPO_ADMIN).repository(repository1).build(),
                new InternalGlobalPermission.Builder().group("atlassian-dev").permission(LICENSED_USER).build());

        PermissionGraph graph = factory.createGraph(user);

        // global permissions
        assertTrue(graph.isGranted(LICENSED_USER, null));
        assertFalse(graph.isGranted(ADMIN, null));

        // project permissions
        assertTrue(graph.isGranted(PROJECT_VIEW, project3));
        assertFalse(graph.isGranted(PROJECT_READ, project3));
        assertFalse(graph.isGranted(PROJECT_WRITE, project3));
        assertFalse(graph.isGranted(PROJECT_ADMIN, project3));

        // repository permissions
        assertTrue(graph.isGranted(REPO_READ, repository1));
        assertTrue(graph.isGranted(REPO_WRITE, repository1));
        assertTrue(graph.isGranted(REPO_ADMIN, repository1));
    }

    @Test
    public void testMixedPermissionsWithTooSmallCache() {
        factory = buildFactory(1, 1);
        InternalNormalUser user2 = createMockNormalUser(2, "user-2");
        InternalNormalUser user3 = createMockNormalUser(2, "user-3");

        // tests case where both user and group level permissions are granted
        mockGroupMemberships(user, "ProjectAdmin", "stash-users");
        mockGroupMemberships(user2, "stash-users");
        mockGroupMemberships(user3, "stash-users");

        mockPermissions(
                new InternalGlobalPermission.Builder().group("ProjectAdmin").permission(PROJECT_CREATE).build(),
                new InternalGlobalPermission.Builder().group("stash-users").permission(LICENSED_USER).build(),
                new InternalProjectPermission.Builder().user(user).permission(PROJECT_ADMIN).project(project3).build(),
                new InternalProjectPermission.Builder().user(user2).permission(PROJECT_READ).project(project3).build(),
                new InternalProjectPermission.Builder().user(user3).permission(PROJECT_READ).project(project3).build());

        repeat(2, iteration -> {
            PermissionGraph graph = factory.createGraph(user);
            assertTrue(graph.isGranted(PROJECT_CREATE, null));
            assertTrue(graph.isGranted(PROJECT_ADMIN, project3));

            PermissionGraph graph2 = factory.createGraph(user2);
            assertTrue(graph2.isGranted(LICENSED_USER, null));
            assertTrue(graph2.isGranted(PROJECT_READ, project3));
            assertFalse(graph2.isGranted(PROJECT_ADMIN, project3));

            PermissionGraph graph3 = factory.createGraph(user3);
            assertTrue(graph3.isGranted(LICENSED_USER, null));
            assertTrue(graph3.isGranted(PROJECT_READ, project3));
            assertFalse(graph3.isGranted(PROJECT_ADMIN, project3));
        });
    }

    @Test
    public void testOnPermissionEventDefaultPermission() {
        mockNoGroupMemberships(user);

        PermissionGraph graph = factory.createGraph(user);
        factory.createGraph(user);
        assertTrue(graph.isGranted(PROJECT_WRITE, project1)); // granted through default project permission on p1

        // revoke the default permission on p1
        defaultPermissions.remove(1);
        ProjectPermissionRevokedEvent event = mock(ProjectPermissionRevokedEvent.class);
        when(event.getProject()).thenReturn(project1);
        when(event.getPermission()).thenReturn(PROJECT_WRITE);
        factory.onPermissionsChanged(event);

        assertFalse(graph.isGranted(PROJECT_WRITE, project1)); // should be revoked now

        // verify on a fresh instance as well
        graph = factory.createGraph(user);
        assertFalse(graph.isGranted(PROJECT_WRITE, project1)); // should be revoked now

        // now grant a new default permission
        defaultPermissions.put(1, PROJECT_READ);
        ProjectPermissionGrantedEvent event2 = mock(ProjectPermissionGrantedEvent.class);
        when(event2.getProject()).thenReturn(project1);
        when(event2.getPermission()).thenReturn(PROJECT_READ);
        factory.onPermissionsChanged(event2);

        // verify on a fresh instance
        graph = factory.createGraph(user);
        assertFalse(graph.isGranted(PROJECT_WRITE, project1)); // should still be revoked
        assertTrue(graph.isGranted(PROJECT_READ, project1)); // should be granted

        // the DAO should be hit only once, all subsequent updates are in-memory without hitting the DAO
        verify(effectivePermissionDao).findByUserId(eq(1), any(PageRequest.class));
    }

    @Test
    public void testOnPermissionModifiedEventDefaultPermission() {
        mockNoGroupMemberships(user);

        PermissionGraph graph = factory.createGraph(user);
        assertTrue(graph.isGranted(PROJECT_WRITE, project1)); // granted through default project permission on p1

        // modify the default permission on p1
        defaultPermissions.put(1, PROJECT_READ);
        ProjectPermissionModifiedEvent modifiedEvent = mock(ProjectPermissionModifiedEvent.class);
        when(modifiedEvent.getProject()).thenReturn(project1);
        when(modifiedEvent.getPermission()).thenReturn(PROJECT_READ);
        factory.onPermissionsChanged(modifiedEvent);

        graph = factory.createGraph(user);
        assertFalse(graph.isGranted(PROJECT_WRITE, project1)); // should be revoked
        assertTrue(graph.isGranted(PROJECT_READ, project1)); // should be granted
    }

    @Test
    public void testOnPermissionEventNonDefault() {
        mockGroupMemberships(user, "ProjectAdmin", "master-of-the-universe");

        mockPermissions(
                new InternalGlobalPermission.Builder().group("ProjectAdmin").permission(PROJECT_CREATE).build(),
                new InternalProjectPermission.Builder().user(user).permission(PROJECT_ADMIN).project(project1).build());

        PermissionGraph graph = factory.createGraph(user);

        assertTrue(graph.isGranted(PROJECT_ADMIN, project1));
        assertFalse(graph.isGranted(PROJECT_ADMIN, project2));
        assertFalse(graph.isGranted(PROJECT_ADMIN, project3));

        // simulate that someone granted PROJECT_ADMIN to user on p2
        ProjectPermissionGrantedEvent event = mock(ProjectPermissionGrantedEvent.class);
        when(event.getProject()).thenReturn(project2);
        when(event.getAffectedUser()).thenReturn(user);
        when(event.getPermission()).thenReturn(PROJECT_ADMIN);

        resetMockedPermissions();
        mockPermissions(
                new InternalGlobalPermission.Builder().group("ProjectAdmin").permission(PROJECT_CREATE).build(),
                new InternalProjectPermission.Builder().user(user).permission(PROJECT_ADMIN).project(project1).build(),
                new InternalProjectPermission.Builder().user(user).permission(PROJECT_ADMIN).project(project2).build());

        factory.onPermissionsChanged(event);

        // verify on a fresh instance
        graph = factory.createGraph(user);

        assertTrue(graph.isGranted(PROJECT_ADMIN, project1));
        assertTrue(graph.isGranted(PROJECT_ADMIN, project2));
        assertFalse(graph.isGranted(PROJECT_ADMIN, project3));

        // simulate that the PROJ_ADMIN permission on p3 is granted to the master-of-the-universe group
        event = mock(ProjectPermissionGrantedEvent.class);
        when(event.getProject()).thenReturn(project3);
        when(event.getAffectedGroup()).thenReturn("master-of-the-universe");
        when(event.getPermission()).thenReturn(PROJECT_ADMIN);

        resetMockedPermissions();
        mockPermissions(
                new InternalGlobalPermission.Builder().group("ProjectAdmin").permission(PROJECT_CREATE).build(),
                new InternalProjectPermission.Builder().user(user).permission(PROJECT_ADMIN).project(project1).build(),
                new InternalProjectPermission.Builder().user(user).permission(PROJECT_ADMIN).project(project2).build(),
                new InternalProjectPermission.Builder().group("master-of-the-universe").permission(PROJECT_ADMIN).project(project3).build());

        factory.onPermissionsChanged(event);

        // verify on a fresh instance
        graph = factory.createGraph(user);

        assertTrue(graph.isGranted(PROJECT_ADMIN, project1));
        assertTrue(graph.isGranted(PROJECT_ADMIN, project2));
        assertTrue(graph.isGranted(PROJECT_ADMIN, project3));
    }

    @Test
    public void testOnEffectivePermissionsChanged() {
        mockNoGroupMemberships(user);

        EffectivePermissionsProvider provider = mock(EffectivePermissionsProvider.class);
        when(provider.getPermissions(any(ApplicationUser.class))).thenReturn(Arrays.asList(
                new SimpleEffectiveRepositoryPermission(1, REPO_READ),
                new SimpleEffectiveProjectPermission(3, PROJECT_READ)));
        when(permsProviderTracker.getProviders()).thenReturn(Collections.singleton(provider));

        PermissionGraph graph = factory.createGraph(user);
        assertTrue(graph.isGranted(PROJECT_WRITE, project1)); // granted through default project permission on p1
        assertTrue(graph.isGranted(PROJECT_READ, project3));  // granted through the effective permissions provider
        assertTrue(graph.isGranted(REPO_READ, repository1));  // granted through the effective permissions provider

        // verify on a fresh graph as well
        PermissionGraph verificationGraph = factory.createGraph(user);
        assertTrue(verificationGraph.isGranted(PROJECT_WRITE, project1));
        assertTrue(verificationGraph.isGranted(PROJECT_READ, project3));
        assertTrue(verificationGraph.isGranted(REPO_READ, repository1));

        // provider has gone away now
        when(permsProviderTracker.getProviders()).thenReturn(Collections.emptySet());
        EffectivePermissionsChangedEvent event = mock(EffectivePermissionsChangedEvent.class);
        factory.onEffectivePermissionsChanged(event);

        assertTrue(graph.isGranted(PROJECT_WRITE, project1));
        assertFalse(graph.isGranted(PROJECT_READ, project3));
        assertFalse(graph.isGranted(REPO_READ, repository1));

        // verify on a fresh graph as well
        verificationGraph = factory.createGraph(user);
        assertTrue(verificationGraph.isGranted(PROJECT_WRITE, project1));
        assertFalse(verificationGraph.isGranted(PROJECT_READ, project3));
        assertFalse(verificationGraph.isGranted(REPO_READ, repository1));

        // the DAO should be hit only once, all subsequent updates are in-memory without hitting the DAO
        verify(effectivePermissionDao).findByUserId(eq(1), any(PageRequest.class));
    }

    @Test
    public void testNormalUserPermissions() {
        InternalNormalUser user2 = createMockNormalUser(2, "other");
        mockNoGroupMemberships(user);
        mockNoGroupMemberships(user2);

        mockPermissions(
                new InternalGlobalPermission.Builder().user(user).permission(PROJECT_CREATE).build(),
                new InternalGlobalPermission.Builder().user(user2).permission(LICENSED_USER).build(),
                new InternalProjectPermission.Builder().user(user2).permission(PROJECT_ADMIN).project(project3).build());

        PermissionGraph graph = factory.createGraph(user);
        assertTrue(graph.isGranted(PROJECT_CREATE, null));
        assertTrue(graph.isGranted(LICENSED_USER, null)); // inherited from PROJECT_CREATE
        assertFalse(graph.isGranted(SYS_ADMIN, null));

        PermissionGraph graph2 = factory.createGraph(user2);
        assertTrue(graph2.isGranted(LICENSED_USER, null));
        assertFalse(graph2.isGranted(PROJECT_CREATE, null));
        assertTrue(graph2.isGranted(PROJECT_ADMIN, project3));
        assertTrue(graph2.isGranted(PROJECT_WRITE, project3)); // inherited
        assertTrue(graph2.isGranted(PROJECT_READ, project3)); // inherited
        assertTrue(graph2.isGranted(PROJECT_VIEW, project3)); // inherited

        // verify that the default permissions still apply
        assertFalse(graph2.isGranted(PROJECT_ADMIN, project1));
        assertTrue(graph2.isGranted(PROJECT_WRITE, project1));
        assertFalse(graph2.isGranted(PROJECT_WRITE, project2));
        assertTrue(graph2.isGranted(PROJECT_READ, project2));

        // caches should be filled - subsequent calls should not result in cache hits
        factory.createGraph(user);
        factory.createGraph(user);
        factory.createGraph(user2);
        factory.createGraph(user2);

        // verify that the Dao has only been accessed once for each user
        verify(effectivePermissionDao).findByUserId(eq(1), any(PageRequest.class));
        verify(effectivePermissionDao).findByUserId(eq(2), any(PageRequest.class));
    }

    @Test
    public void testServiceUserPermissions() {
        InternalServiceUser user2 = createMockServiceUser(2, "other");

        mockPermissions(
                new InternalGlobalPermission.Builder().user(user2).permission(LICENSED_USER).build(),
                new InternalProjectPermission.Builder().user(user2).permission(PROJECT_ADMIN).project(project3).build());

        PermissionGraph graph2 = factory.createGraph(user2);

        //A service user can be part of no groups
        verify(userService, never()).findGroupsByUser(anyString(), any(PageRequest.class));

        assertTrue(graph2.isGranted(LICENSED_USER, null));
        assertFalse(graph2.isGranted(PROJECT_CREATE, null));
        assertTrue(graph2.isGranted(PROJECT_ADMIN, project3));
        assertTrue(graph2.isGranted(PROJECT_WRITE, project3)); // inherited
        assertTrue(graph2.isGranted(PROJECT_READ, project3)); // inherited
        assertTrue(graph2.isGranted(PROJECT_VIEW, project3)); // inherited

        // verify that the default permissions do *not* apply
        assertFalse(graph2.isGranted(PROJECT_ADMIN, project1));
        assertFalse(graph2.isGranted(PROJECT_WRITE, project1));
        assertFalse(graph2.isGranted(PROJECT_WRITE, project2));
        assertFalse(graph2.isGranted(PROJECT_READ, project2));
    }

    @Test
    public void testGraphRecalculation() throws InterruptedException {
        mockNoGroupMemberships(user);

        mockPermissions(
                new InternalGlobalPermission.Builder().user(user).permission(PROJECT_CREATE).build());

        PermissionGraph graph = factory.createGraph(user);
        assertTrue(graph.isGranted(PROJECT_CREATE, null));
        assertTrue(graph.isGranted(LICENSED_USER, null)); // inherited from PROJECT_CREATE
        assertFalse(graph.isGranted(SYS_ADMIN, null));

        factory.onPermissionsChanged(new GlobalPermissionRevokedEvent(this, PROJECT_CREATE, null, user));
        userPermissions.clear();

        assertFalse(graph.isGranted(PROJECT_CREATE, null));
        assertFalse(graph.isGranted(LICENSED_USER, null)); // inherited from PROJECT_CREATE
        assertFalse(graph.isGranted(SYS_ADMIN, null));

        verify(effectivePermissionDao, times(2)).findByUserId(eq(1), any(PageRequest.class));
    }

    private static void repeat(int times, Consumer<Integer> consumer) {
        for (int i = 1; i <= times; ++i) {
            consumer.accept(i);
        }
    }

    private InternalUserPermissionGraphFactory buildFactory(int maxGroups, int maxUsers) {
        InternalUserPermissionGraphFactory permissionGraphFactory = new InternalUserPermissionGraphFactory(
                cacheFactory,
                effectivePermissionDao,
                projectPermissionDao,
                permsProviderTracker,
                3600, maxGroups, 3600, maxUsers, 3600); // TTLs set to 1h
        permissionGraphFactory.setUserService(userService);
        permissionGraphFactory.warmCaches();

        return permissionGraphFactory;
    }

    private InternalNormalUser createMockNormalUser(int userId, String username) {
        final InternalNormalUser user = mock(InternalNormalUser.class);
        when(user.getId()).thenReturn(userId);
        when(user.getName()).thenReturn(username);
        when(user.getType()).thenReturn(UserType.NORMAL);
        //noinspection unchecked
        when(user.accept(any(ApplicationUserVisitor.class))).thenAnswer(invocation ->
                ((ApplicationUserVisitor) invocation.getArguments()[0]).visit(user));

        return user;
    }

    private InternalServiceUser createMockServiceUser(int userId, String displayName) {
        final InternalServiceUser user = mock(InternalServiceUser.class);
        when(user.getId()).thenReturn(userId);
        when(user.getDisplayName()).thenReturn(displayName);
        when(user.getName()).thenReturn(displayName);
        when(user.getType()).thenReturn(UserType.SERVICE);
        //noinspection unchecked
        when(user.accept(any(ApplicationUserVisitor.class))).thenAnswer(invocation ->
                ((ApplicationUserVisitor) invocation.getArguments()[0]).visit(user));

        return user;
    }

    private void mockGroupMemberships(ApplicationUser user, final String... groups) {
        when(userService.findGroupsByUser(eq(user.getName()), any(PageRequest.class)))
                .thenAnswer((Answer<Page<String>>) invocation -> {
            PageRequest request = (PageRequest) invocation.getArguments()[1];
            if (request.getStart() >= groups.length) {
                return PageUtils.createEmptyPage(request);
            } else {
                List<String> values = Lists.newArrayList();
                for (int i = request.getStart(); i < groups.length && values.size() < request.getLimit(); ++i) {
                    values.add(groups[i]);
                }
                return PageUtils.createPage(values, request);
            }
        });
    }

    private void mockNoGroupMemberships(ApplicationUser user) {
        mockGroupMemberships(user); // no groups provided
    }

    private void mockPermissions(InternalGrantedPermission... grantedPermissions) {
        for (InternalGrantedPermission permission : grantedPermissions) {
            if (StringUtils.isNotBlank(permission.getGroup())) {
                // lowercase the group names - this is what the various DAOs do internally as well
                groupPermissions.put(IdentifierUtils.toLowerCase(permission.getGroup()), permission);
            }
            if (permission.getUser() != null) {
                userPermissions.put(permission.getUser().getId(), permission);
            }
        }
    }

    /**
     * Clears user and group permissions
     */
    private void resetMockedPermissions() {
        groupPermissions.clear();
        userPermissions.clear();
    }
}
