package com.atlassian.stash.internal.user;

import com.atlassian.bitbucket.server.StandardFeature;
import com.atlassian.bitbucket.server.FeatureManager;
import com.atlassian.crowd.manager.recovery.RecoveryModeService;
import com.atlassian.crowd.model.user.User;
import com.atlassian.stash.internal.project.InternalProject;
import com.atlassian.stash.internal.project.ProjectDao;
import com.atlassian.stash.internal.repository.InternalRepository;
import com.atlassian.stash.internal.repository.RepositoryDao;
import com.atlassian.bitbucket.project.Project;
import com.atlassian.bitbucket.repository.Repository;
import com.atlassian.bitbucket.user.ApplicationUser;
import com.atlassian.bitbucket.permission.Permission;
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.google.common.base.Function;
import com.google.common.base.Functions;
import com.google.common.collect.Collections2;
import com.google.common.collect.Iterables;
import com.google.common.collect.Maps;
import org.hamcrest.Description;
import org.hamcrest.TypeSafeMatcher;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.InjectMocks;
import org.mockito.Matchers;
import org.mockito.Mock;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.runners.MockitoJUnitRunner;
import org.mockito.stubbing.Answer;
import org.springframework.beans.DirectFieldAccessor;

import java.util.*;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicLong;
import java.util.function.Predicate;

import static org.hamcrest.Matchers.containsInAnyOrder;
import static org.hamcrest.Matchers.hasItems;
import static org.junit.Assert.*;
import static org.mockito.Matchers.eq;
import static org.mockito.Mockito.*;

@RunWith(MockitoJUnitRunner.class)
@SuppressWarnings({"ConstantConditions", "unchecked"})
public class PermissionServiceImplTest {

    @Mock
    private InternalAuthenticationContext authenticationContext;
    @Mock
    private FeatureManager featureManager;
    @Mock
    private EffectivePermissionDao permissionDao;
    @Mock
    private PermissionGraphFactory graphFactory;
    @Mock
    private RecoveryModeService recoveryModeService;
    @Mock
    private RepositoryPermissionDao repositoryPermissionDao;
    @Mock
    private ProjectDao projectDao;
    @Mock
    private RepositoryDao repositoryDao;
    @Mock
    private UserService userService;
    @InjectMocks
    private PermissionServiceImpl service;

    private StashUserAuthenticationToken currentToken;
    private boolean publicAccessFeatureEnabled;

    private Map<Integer, PermissionGraph> userGraphs;

    @Before
    public void setUp() throws Exception {
        // manually set mocks that would otherwise be autowired
        service.setFeatureManager(featureManager);
        service.setPermissionGraphFactory(graphFactory);
        service.setUserService(userService);

        when(authenticationContext.getCurrentToken()).thenAnswer(new Answer<Object>() {
            @Override
            public Object answer(InvocationOnMock invocation) throws Throwable {
                return currentToken;
            }
        });
        when(authenticationContext.getCurrentUser()).thenAnswer(new Answer<Object>() {
            @Override
            public Object answer(InvocationOnMock invocationOnMock) throws Throwable {
                return currentToken != null ? currentToken.getPrincipal() : null;
            }
        });
        when(featureManager.isEnabled(StandardFeature.PUBLIC_ACCESS)).thenAnswer(new Answer<Boolean>() {
            @Override
            public Boolean answer(InvocationOnMock invocationOnMock) throws Throwable {
                return publicAccessFeatureEnabled;
            }
        });

        userGraphs = Maps.newHashMap();
        when(graphFactory.createGraph(any(ApplicationUser.class))).thenAnswer(new Answer<PermissionGraph>() {
            @Override
            public PermissionGraph answer(InvocationOnMock invocationOnMock) throws Throwable {
                ApplicationUser user = (ApplicationUser) invocationOnMock.getArguments()[0];
                PermissionGraph graph = userGraphs.get(user.getId());
                return graph != null ? graph : mock(IterablePermissionGraph.class);
            }
        });
    }

    @Test
    public void testGetHighestGlobalPermission() throws Exception {
        mockGroupMemberships(anyString());

        ApplicationUser sysAdmin = createMockUser(1, Permission.SYS_ADMIN);
        ApplicationUser admin = createMockUser(2, Permission.ADMIN);
        ApplicationUser projectCreator = createMockUser(3, Permission.PROJECT_CREATE);
        ApplicationUser licensedUser = createMockUser(4, Permission.LICENSED_USER);
        ApplicationUser noAccess = createMockUser(5);

        assertEquals(null, service.getHighestGlobalPermission(noAccess));
        assertEquals(Permission.LICENSED_USER, service.getHighestGlobalPermission(licensedUser));
        assertEquals(Permission.PROJECT_CREATE, service.getHighestGlobalPermission(projectCreator));
        assertEquals(Permission.ADMIN, service.getHighestGlobalPermission(admin));
        assertEquals(Permission.SYS_ADMIN, service.getHighestGlobalPermission(sysAdmin));
    }

    @Test
    public void testGetHighestGlobalGroupPermission() throws Exception {
        final Map<String, List<Permission>> permsForGroups = new HashMap<String, List<Permission>>() { {
            put("group1", Arrays.asList(Permission.SYS_ADMIN, Permission.ADMIN, Permission.LICENSED_USER));
            put("group2", Arrays.asList(Permission.ADMIN, Permission.LICENSED_USER));
            put("group3", Arrays.asList(Permission.LICENSED_USER));
            put("group4", Collections.<Permission>emptyList());
        }};
        when(permissionDao.isGrantedToGroup(notNull(GroupPermissionCriteria.class))).then(new Answer<Object>() {
            @Override
            public Object answer(InvocationOnMock invocation) throws Throwable {
                GroupPermissionCriteria criteria = (GroupPermissionCriteria) invocation.getArguments()[0];
                Permission requested = criteria.getPermission();
                String group = Iterables.getOnlyElement(criteria.getGroups());
                List<Permission> permissions = permsForGroups.get(group);
                return permissions.contains(requested);
            }
        });

        assertEquals(null, service.getHighestGlobalGroupPermission(null));
        assertEquals(Permission.SYS_ADMIN, service.getHighestGlobalGroupPermission("group1"));
        assertEquals(Permission.ADMIN, service.getHighestGlobalGroupPermission("group2"));
        assertEquals(Permission.LICENSED_USER, service.getHighestGlobalGroupPermission("group3"));
        assertEquals(null, service.getHighestGlobalGroupPermission("group4"));
    }

    @Test
    public void testGetUsersWithPermission() throws Exception {
        new DirectFieldAccessor(service).setPropertyValue("maxUserPageSize", 100);
        Permission permission = Permission.LICENSED_USER;

        final ApplicationUser user1 = createActiveUser("user1");
        final ApplicationUser user2 = createActiveUser("user2");
        final ApplicationUser user3 = createActiveUser("user3");
        final ApplicationUser user4 = createUser("user4"); // Not active

        when(permissionDao.findUsers(eq(permission), notNull(PageRequest.class), notNull(Predicate.class)))
                .thenAnswer(new Answer<Page<ApplicationUser>>() {

                    @Override
                    public Page<ApplicationUser> answer(InvocationOnMock invocation) throws Throwable {
                        PageRequest pageRequest = (PageRequest) invocation.getArguments()[1];
                        Predicate<? super ApplicationUser> predicate = (Predicate<? super ApplicationUser>) invocation.getArguments()[2];

                        return PageUtils.createPage(Collections2.filter(Arrays.asList(
                                user1,
                                user2,
                                user4
                        ), predicate::test), pageRequest);
                    }
                });

        when(permissionDao.findGroups(eq(permission), notNull(PageRequest.class)))
                .thenAnswer(new Answer<Page<String>>() {

                    @Override
                    public Page<String> answer(InvocationOnMock invocation) throws Throwable {
                        PageRequest pageRequest = (PageRequest) invocation.getArguments()[1];
                        return PageUtils.createPage(Arrays.asList("group1"), pageRequest);
                    }
                });

        when(userService.findUsersByGroup(eq("group1"), Matchers.<PageRequest>any()))
                .thenAnswer(new Answer<Page<ApplicationUser>>() {

                    @Override
                    public Page<ApplicationUser> answer(InvocationOnMock invocation) throws Throwable {
                        PageRequest pageRequest = (PageRequest) invocation.getArguments()[1];
                        return PageUtils.createPage(Arrays.asList(
                                user2,
                                user3,
                                user4
                        ), pageRequest);
                    }
                });


        Set<String> users = service.getUsersWithPermission(permission);
        assertThat(users, containsInAnyOrder("user1", "user2", "user3"));
        assertEquals("Number of users is incorrect", 3, users.size());
    }

    @Test
    public void testHasAnySpecificUserPermission() throws Exception {
        ApplicationUser user = createMockUser(1, "name");
        ApplicationUser otherUser = createMockUser(2, "other_name");

        currentToken = StashUserAuthenticationToken.forUser(user);
        mockGrantedPermission(otherUser.getId(), Permission.PROJECT_READ, 99);
        mockGroupMemberships(anyString());

        assertTrue("Project read should be granted to any", service.hasAnyUserPermission(otherUser, Permission.PROJECT_READ));
    }

    @Test
    public void testHasAnyUserPermission() throws Exception {
        ApplicationUser user = createMockUser(1, "name");
        currentToken = StashUserAuthenticationToken.forUser(user);
        mockGrantedPermission(1, Permission.PROJECT_READ, 99);

        assertTrue("Project read should be granted to any", service.hasAnyUserPermission(Permission.PROJECT_READ));
        assertTrue("Project read should be granted to any", service.hasAnyUserPermission(user, Permission.PROJECT_READ));
        assertFalse("Project write should not be granted to any", service.hasAnyUserPermission(Permission.PROJECT_WRITE));
        assertFalse("Project write should not be granted to any", service.hasAnyUserPermission(user, Permission.PROJECT_WRITE));
    }

    @Test
    public void testHasAnyUserPermissionWithRunWithPermission() throws Exception {
        currentToken = token(null, Permission.REPO_ADMIN);

        assertTrue("User should have REPO_ADMIN permission due runWithPermission", service.hasAnyUserPermission(Permission.REPO_ADMIN));
        assertFalse("User should not have PROJECT_ADMIN permission", service.hasAnyUserPermission(Permission.PROJECT_ADMIN));
    }

    @Test
    public void testHasGlobalGroupPermission() throws Exception {
        ApplicationUser user = createMockUser(1, "name");
        currentToken = StashUserAuthenticationToken.forUser(user);
        when(userService.findGroupsByUser(eq("name"), Matchers.<PageRequest>any())).thenAnswer(new Answer<Page<String>>() {

            @Override
            public Page<String> answer(InvocationOnMock invocation) throws Throwable {
                return PageUtils.createPage(Collections.singleton("sysadmin"), (PageRequest) invocation.getArguments()[1]);
            }
        });
        when(permissionDao.isGrantedToGroup(matchesCriteria(Permission.SYS_ADMIN, "sysadmin", null))).thenReturn(true);

        assertFalse("User should not have sys_admin as user", service.hasDirectGlobalUserPermission(Permission.SYS_ADMIN));
        assertTrue("group should have sys-admin permission", service.hasGlobalGroupPermission(Permission.SYS_ADMIN, "sysadmin"));
        assertFalse("other group should have sys-admin permission", service.hasGlobalGroupPermission(Permission.SYS_ADMIN, "other"));
    }

    @Test
    public void testHasDirectGlobalUserPermission() throws Exception {
        ApplicationUser user = createMockUser(1, "name");
        currentToken = StashUserAuthenticationToken.forUser(user);

        when(permissionDao.isGrantedToUser(matchesCriteria(Permission.SYS_ADMIN, 1, null))).thenReturn(true);
        mockGroupMemberships(eq("name"), "sysadmin");

        assertFalse("User should have not sys_admin through a group",
                service.hasGlobalPermissionThroughGroupMembership(Permission.SYS_ADMIN, Collections.<String>emptySet()));
        assertTrue("User should have direct sys-admin permission", service.hasDirectGlobalUserPermission(Permission.SYS_ADMIN));
    }

    @Test
    public void testHasDirectProjectPermission() {
        currentToken = StashUserAuthenticationToken.forUser(createMockUser(2, "user"));
        Project project = createMockProject(9);

        mockGroupMemberships(eq("user"), "sysadmin");
        when(permissionDao.isGrantedToUser(matchesCriteria(Permission.PROJECT_WRITE, 2, 9))).thenReturn(true);

        assertFalse("User should not have project permission through group",
                service.hasProjectPermissionThroughGroupMembership(project, Permission.PROJECT_WRITE, Collections.<String>emptySet()));
        assertTrue("User shoulf have direct project permission",
                service.hasDirectProjectUserPermission(project, Permission.PROJECT_WRITE));
    }

    @Test
    public void testHasDirectRepositoryPermission() {
        currentToken = StashUserAuthenticationToken.forUser(createMockUser(2, "user"));
        Repository repository = createMockRepository(7, createMockProject(9));

        mockGroupMemberships(eq("user"), "sysadmin");
        when(permissionDao.isGrantedToUser(matchesCriteria(Permission.REPO_READ, 2, 9))).thenReturn(true);

        assertFalse("User should not have repository permission through group",
                service.hasRepositoryPermissionThroughGroupMembership(repository, Permission.REPO_READ, Collections.<String>emptySet()));
        assertTrue("User should have direct repository permission",
                service.hasDirectRepositoryUserPermission(repository, Permission.REPO_READ));
    }

    @Test
    public void testHasGlobalPermission() {
        ApplicationUser user = createMockUser(1, "name", Permission.PROJECT_CREATE);
        currentToken = StashUserAuthenticationToken.forUser(user);

        assertFalse(service.hasGlobalPermission((ApplicationUser) null, Permission.PROJECT_CREATE));
        assertTrue(service.hasGlobalPermission(user, Permission.PROJECT_CREATE));
        assertTrue(service.hasGlobalPermission("name", Permission.PROJECT_CREATE));
        assertTrue(service.hasGlobalPermission(Permission.PROJECT_CREATE));
    }


    @Test(expected = IllegalArgumentException.class)
    public void testHasGlobalPermissionForUserWithNonGlobalPermission() {
        service.hasGlobalPermission((ApplicationUser) null, Permission.PROJECT_ADMIN);
    }

    @Test
    public void testHasGlobalPermissionThroughGroupMembership() throws Exception {
        ApplicationUser user = createMockUser(1, "name");
        currentToken = StashUserAuthenticationToken.forUser(user);

        when(userService.findGroupsByUser(eq("name"), Matchers.<PageRequest>any())).thenAnswer(new Answer<Page<String>>() {

            @Override
            public Page<String> answer(InvocationOnMock invocation) throws Throwable {
                return PageUtils.createPage(Arrays.asList("sysadmin", "admin"), (PageRequest) invocation.getArguments()[1]);
            }
        });
        when(permissionDao.isGrantedToUser(matchesCriteria(Permission.SYS_ADMIN, 1, null))).thenReturn(false);
        when(permissionDao.isGrantedToGroup(matchesCriteria(Permission.SYS_ADMIN, Arrays.asList("sysadmin", "admin"), null))).thenReturn(true);
        when(permissionDao.isGrantedToGroup(matchesCriteria(Permission.SYS_ADMIN, "sysadmin", null))).thenReturn(true);
        when(permissionDao.isGrantedToGroup(matchesCriteria(Permission.ADMIN, "admin", null))).thenReturn(true);
        when(permissionDao.isGrantedToUser(matchesCriteria(Permission.SYS_ADMIN, 1, null))).thenReturn(false);
        when(permissionDao.isGrantedToUser(matchesCriteria(Permission.ADMIN, 1, null))).thenReturn(false);

        assertFalse("User should not have direct sys_admin permission", service.hasDirectGlobalUserPermission(Permission.SYS_ADMIN));
        assertFalse("User should not have direct admin permissions", service.hasDirectGlobalUserPermission(Permission.ADMIN));

        assertTrue("User should have sys-admin permission through group membership",
                service.hasGlobalPermissionThroughGroupMembership(Permission.SYS_ADMIN, Collections.<String>emptySet()));
        assertTrue("User should have admin permission through group membership",
                service.hasGlobalPermissionThroughGroupMembership(Permission.ADMIN, Collections.<String>emptySet()));
        assertFalse("User should not have sys-admin permission because group excluded",
                service.hasGlobalPermissionThroughGroupMembership(Permission.SYS_ADMIN, Collections.singleton("sysadmin")));
        assertFalse("User should not have admin permission because group excluded",
                service.hasGlobalPermissionThroughGroupMembership(Permission.ADMIN, Collections.singleton("admin")));
        assertTrue("User should have admin permission through group membership",
                service.hasGlobalPermissionThroughGroupMembership(Permission.SYS_ADMIN, Collections.singleton("admin")));
    }

    @Test(expected = IllegalArgumentException.class)
    public void testHasGlobalPermissionWithNonGlobalPermission() {
        service.hasGlobalPermission(Permission.PROJECT_ADMIN);
    }

    @Test
    public void testHasProjectPermission() {
        InternalProject project = createMockProject(10);
        InternalProject otherProject = createMockProject(20);

        ApplicationUser user = createMockUser(1, "name");
        currentToken = StashUserAuthenticationToken.forUser(user);

        mockGroupMemberships(eq("name"), "first");
        mockGrantedPermission(1, Permission.PROJECT_READ, project);

        assertFalse(service.hasProjectPermission(null, project, Permission.PROJECT_READ));
        assertFalse(service.hasProjectPermission(null, 10, Permission.PROJECT_READ));

        assertTrue(service.hasProjectPermission(project, Permission.PROJECT_READ));
        assertTrue(service.hasProjectPermission(10, Permission.PROJECT_READ));
        assertTrue(service.hasProjectPermission(user, project, Permission.PROJECT_READ));

        assertFalse(service.hasProjectPermission(otherProject, Permission.PROJECT_READ));
        assertFalse(service.hasProjectPermission(20, Permission.PROJECT_READ));
        assertFalse(service.hasProjectPermission(user, otherProject, Permission.PROJECT_READ));
    }

    @Test(expected = IllegalArgumentException.class)
    public void testHasProjectPermissionForProjectIdAndUserWithGlobalPermission() {
        service.hasProjectPermission(null, 1, Permission.SYS_ADMIN);
    }

    @Test(expected = IllegalArgumentException.class)
    public void testHasProjectPermissionForProjectIdWithGlobalPermission() {
        service.hasProjectPermission(1, Permission.SYS_ADMIN);
    }

    @Test(expected = IllegalArgumentException.class)
    public void testHasProjectPermissionForUserWithGlobalPermission() {
        service.hasProjectPermission(null, mock(Project.class), Permission.SYS_ADMIN);
    }

    @Test(expected = IllegalArgumentException.class)
    public void testHasProjectPermissionWithGlobalPermission() {
        service.hasProjectPermission(null, mock(Project.class), Permission.SYS_ADMIN);
    }

    @Test
    public void testHasRepositoryPermission() {
        InternalRepository repository = createMockRepository(20, createMockProject(10));

        ApplicationUser user = createMockUser(1, "name");
        currentToken = StashUserAuthenticationToken.forUser(user);
        mockGrantedPermission(1, Permission.REPO_READ, repository);

        assertFalse(service.hasRepositoryPermission(null, repository, Permission.REPO_READ));
        assertTrue(service.hasRepositoryPermission(repository, Permission.REPO_READ));
        assertTrue(service.hasRepositoryPermission(20, Permission.REPO_READ));
        assertTrue(service.hasRepositoryPermission(user, repository, Permission.REPO_READ));
        assertTrue(service.hasRepositoryPermission(user, 20, Permission.REPO_READ));
    }

    @Test(expected = IllegalArgumentException.class)
    public void testHasRepositoryPermissionForUserWithGlobalPermission() {
        service.hasRepositoryPermission(null, mock(Repository.class), Permission.ADMIN);
    }

    @Test
    public void testNonGrantableAnonymous() throws Exception {
        currentToken = StashUserAuthenticationToken.forUser(null);
        // This is to check that even if the DB has the permissions, they are not granted.
        when(permissionDao.isGrantedToUser(matchesCriteria(Permission.SYS_ADMIN, 1, null))).thenReturn(true);
        when(permissionDao.isGrantedToUser(matchesCriteria(Permission.ADMIN, 1, null))).thenReturn(true);

        assertFalse("Anonymous should not have sysadmin permission", service.hasGlobalPermission(Permission.SYS_ADMIN));
        assertFalse("Anonymous should not have admin permission", service.hasGlobalPermission(Permission.ADMIN));
    }

    @Test
    public void testRunWithPermissionAnonymous() {
        StashUserAuthenticationToken anonAdminToken = currentToken = token(null, Permission.ADMIN);

        assertFalse("Anonymous should not have permission", service.hasGlobalPermission((ApplicationUser) null, Permission.ADMIN));
        assertTrue("Auth token should have admin", service.hasGlobalPermission(anonAdminToken, Permission.ADMIN));
        assertFalse("Auth token should not have sys_admin", service.hasGlobalPermission(anonAdminToken, Permission.SYS_ADMIN));
    }

    @Test
    public void testRunWithPermissions() throws Exception {
        InternalProject project = mock(InternalProject.class);
        when(project.getId()).thenReturn(10);
        when(projectDao.getById(10)).thenReturn(project);

        ApplicationUser user = createMockUser(1, "name");
        when(permissionDao.isGrantedToUser(matchesCriteria(Permission.PROJECT_READ, 1, 10))).thenReturn(false);
        when(userService.findGroupsByUser(anyString(), Matchers.<PageRequest>any())).thenAnswer(new Answer<Page<String>>() {

            @Override
            public Page<String> answer(InvocationOnMock invocation) throws Throwable {
                return PageUtils.createPage(Collections.<String>emptyList(), (PageRequest) invocation.getArguments()[1]);
            }
        });

        assertFalse("User should not have permission", service.hasProjectPermission(user, 10, Permission.PROJECT_READ));

        currentToken = token(user, Permission.PROJECT_READ);
        assertTrue("Auth token should have permission", service.hasProjectPermission(10, Permission.PROJECT_READ));

        currentToken = token(user, Permission.PROJECT_WRITE);
        assertTrue("Auth token2 should have permission", service.hasProjectPermission(10, Permission.PROJECT_READ));

        currentToken = token(user, Permission.PROJECT_READ);
        assertFalse("Auth token3 should not have permission", service.hasProjectPermission(10, Permission.PROJECT_WRITE));
    }

    @Test
    public void testUserWithLargeNumGroups1() throws Exception {
        ApplicationUser user = createMockUser(1, "name");
        currentToken = StashUserAuthenticationToken.forUser(user);

        final String grantedGroup = "Group" + (PermissionServiceImpl.USER_GROUP_PAGE_LIMIT);

        when(userService.findGroupsByUser(eq("name"), Matchers.<PageRequest>any())).thenAnswer(new Answer<Page<String>>() {

            @Override
            public Page<String> answer(InvocationOnMock invocation) throws Throwable {
                PageRequest pageRequest = (PageRequest) invocation.getArguments()[1];
                List<String> usersGroups = new ArrayList<String>();
                for (int i = pageRequest.getStart(); i < PermissionServiceImpl.USER_GROUP_PAGE_LIMIT + 1; ++i) {
                    usersGroups.add("Group" + i);
                }
                return PageUtils.createPage(usersGroups, pageRequest);
            }
        });
        when(permissionDao.isGrantedToGroup(matchesCriteria(Permission.SYS_ADMIN, grantedGroup, null))).thenReturn(true);

        assertFalse("User should not have permission directly", service.hasDirectGlobalUserPermission(Permission.SYS_ADMIN));
        assertTrue("User should have permission through group",
                service.hasGlobalPermissionThroughGroupMembership(Permission.SYS_ADMIN, Collections.<String>emptySet()));
    }

    @Test
    public void testUserWithLargeNumGroups2() throws Exception {
        ApplicationUser user = createMockUser(1, "name");
        currentToken = StashUserAuthenticationToken.forUser(user);

        final String grantedGroup = "Group" + (PermissionServiceImpl.USER_GROUP_PAGE_LIMIT);

        when(userService.findGroupsByUser(eq("name"), Matchers.<PageRequest>any())).thenAnswer(new Answer<Page<String>>() {

            @Override
            public Page<String> answer(InvocationOnMock invocation) throws Throwable {
                PageRequest pageRequest = (PageRequest) invocation.getArguments()[1];
                List<String> usersGroups = new ArrayList<String>();
                for (int i = pageRequest.getStart(); i < PermissionServiceImpl.USER_GROUP_PAGE_LIMIT + 1; ++i) {
                    usersGroups.add("Group" + i);
                }
                return PageUtils.createPage(usersGroups, pageRequest);
            }
        });
        when(permissionDao.isGrantedToGroup(matchesCriteria(Permission.SYS_ADMIN, grantedGroup, null))).thenReturn(true);

        assertFalse("User should not have permission directly", service.hasDirectGlobalUserPermission(Permission.SYS_ADMIN));
        assertTrue("User should have permission through group", service.hasGlobalPermissionThroughGroupMembership(Permission.SYS_ADMIN, Collections.<String>emptySet()));
    }

    @Test
    public void testCountByProjectWithProjectRead() throws Exception {
        InternalProject project = createMockProject(1);

        ApplicationUser user = createMockUser(1, "name");
        currentToken = StashUserAuthenticationToken.forUser(user);
        mockGrantedPermission(1, Permission.PROJECT_READ, project);
        when(repositoryDao.countByProject(eq(1))).thenReturn(10L);

        assertEquals(10, service.getCountOfAccessibleRepositories(project));
    }

    @Test
    public void testCountByProjectWithoutProjectRead() throws Exception {
        currentToken = StashUserAuthenticationToken.forUser(createMockUser(1));
        InternalProject project = mockFindGroupsByUser(Functions.constant(Collections.singleton("group")));

        when(repositoryPermissionDao.countWithPermission(eq(1), argThat(new TypeSafeMatcher<PartitionedGroups>() {
            @Override
            protected boolean matchesSafely(PartitionedGroups item) {
                return hasItems(hasItems("group")).matches(item);
            }

            @Override
            public void describeTo(Description description) {
                description.appendText("Missing group");
            }
        }), eq(1))).thenReturn(10L);
        assertEquals(10, service.getCountOfAccessibleRepositories(project));
    }

    @Test
    public void testGetAllGroups() throws Exception {
        final int[] count = new int[]{0};
        mockFindGroupsByUserWithPage(new Function<PageRequest, Page<String>>() {
            @Override
            public Page<String> apply(PageRequest request) {
                count[0]++;
                Iterable<String> values = Iterables.limit(Iterables.cycle("group" + request.getStart()), request.getLimit());
                return PageUtils.createPage(values, request.getStart() >= 1000, request);
            }
        });
        Iterator<Collection<String>> groups = service.getAllGroups("a").iterator();
        assertEquals("Have made a single lookup", 1, count[0]);
        assertThat(groups.next(), hasItems("group0"));
        assertEquals("Does not incur another lookup", 1, count[0]);
        assertThat(groups.next(), hasItems("group500"));
        assertEquals("Business as usual", 2, count[0]);
        assertThat(groups.next(), hasItems("group1000"));
        assertFalse(groups.hasNext());
    }

    @Test
    public void testGetAllGroupsEmpty() throws Exception {
        mockFindGroupsByUser(Functions.constant(Collections.<String>emptyList()));
        Iterator<Collection<String>> groups = service.getAllGroups("a").iterator();
        assertTrue(groups.next().isEmpty());
        assertFalse(groups.hasNext());
    }

    @Test
    public void testHasProjectPermissionNonExistingProjectId() {
        when(projectDao.getById(99)).thenReturn(null);

        // non-existing project ID is treated as "yes"
        assertTrue(service.hasProjectPermission(99, Permission.PROJECT_ADMIN));
        assertTrue(service.hasProjectPermission(99, Permission.PROJECT_WRITE));
        assertTrue(service.hasProjectPermission(99, Permission.PROJECT_READ));
    }

    @Test
    public void testHasRepositoryPermissionNonExistingRepositoryId() {
        when(repositoryDao.getById(99)).thenReturn(null);

        // non-existing repository ID is treated as "yes"
        assertTrue(service.hasRepositoryPermission(99, Permission.REPO_ADMIN));
        assertTrue(service.hasRepositoryPermission(99, Permission.REPO_WRITE));
        assertTrue(service.hasRepositoryPermission(99, Permission.REPO_READ));
    }

    @Test
    public void testIsProjectAccessible() throws Exception {
        int projectId = 99;
        InternalProject project = mock(InternalProject.class);
        when(project.getId()).thenReturn(projectId);
        final AtomicBoolean isPublic = new AtomicBoolean(false);
        final AtomicLong repoCount = new AtomicLong(0);
        when(projectDao.getById(projectId)).thenReturn(project);
        when(project.isPublic()).thenAnswer(new Answer<Boolean>() {
            @Override
            public Boolean answer(InvocationOnMock invocationOnMock) throws Throwable {
                return isPublic.get();
            }
        });
        when(repositoryDao.countPublicByProject(project.getId())).then(new Answer<Long>() {
            @Override
            public Long answer(InvocationOnMock invocationOnMock) throws Throwable {
                return repoCount.get();
            }
        });

        publicAccessFeatureEnabled = false;
        assertFalse(service.isProjectAccessible(project));
        assertFalse(service.isProjectAccessible(projectId));
        assertFalse(service.hasProjectPermission(project, Permission.PROJECT_VIEW));
        assertFalse(service.hasProjectPermission(projectId, Permission.PROJECT_VIEW));

        isPublic.set(true);
        assertFalse(service.isProjectAccessible(project));
        assertFalse(service.isProjectAccessible(projectId));
        assertFalse(service.hasProjectPermission(project, Permission.PROJECT_VIEW));
        assertFalse(service.hasProjectPermission(projectId, Permission.PROJECT_VIEW));

        publicAccessFeatureEnabled = true;
        assertTrue(service.isProjectAccessible(project));
        assertTrue(service.isProjectAccessible(projectId));
        assertFalse(service.hasProjectPermission(project, Permission.PROJECT_VIEW));
        assertFalse(service.hasProjectPermission(projectId, Permission.PROJECT_VIEW));

        isPublic.set(false);
        repoCount.set(1);
        assertTrue(service.isProjectAccessible(project));
        assertTrue(service.isProjectAccessible(projectId));
        assertFalse(service.hasProjectPermission(project, Permission.PROJECT_VIEW));
        assertFalse(service.hasProjectPermission(projectId, Permission.PROJECT_VIEW));

        repoCount.set(0);
        PermissionGraph graph = mock(IterablePermissionGraph.class);
        final AtomicBoolean isGrantedProjectView = new AtomicBoolean(false);
        when(graph.isGranted(Permission.PROJECT_VIEW, project)).thenAnswer(new Answer<Boolean>() {
            @Override
            public Boolean answer(InvocationOnMock invocationOnMock) throws Throwable {
                return isGrantedProjectView.get();
            }
        });
        currentToken = StashUserAuthenticationToken.forUser(createMockUser(1));
        currentToken.setPermissions(graph);
        assertFalse(service.isProjectAccessible(project));
        assertFalse(service.isProjectAccessible(projectId));
        assertFalse(service.hasProjectPermission(project, Permission.PROJECT_VIEW));
        assertFalse(service.hasProjectPermission(projectId, Permission.PROJECT_VIEW));

        isPublic.set(true);
        assertTrue(service.isProjectAccessible(project));
        assertTrue(service.isProjectAccessible(projectId));
        assertTrue(service.hasProjectPermission(project, Permission.PROJECT_VIEW));
        assertTrue(service.hasProjectPermission(projectId, Permission.PROJECT_VIEW));

        isPublic.set(false);
        isGrantedProjectView.set(true);
        assertTrue(service.isProjectAccessible(project));
        assertTrue(service.isProjectAccessible(projectId));
        assertTrue(service.hasProjectPermission(project, Permission.PROJECT_VIEW));
        assertTrue(service.hasProjectPermission(projectId, Permission.PROJECT_VIEW));

        publicAccessFeatureEnabled = false;
        assertTrue(service.isProjectAccessible(project));
        assertTrue(service.isProjectAccessible(projectId));
        assertTrue(service.hasProjectPermission(project, Permission.PROJECT_VIEW));
        assertTrue(service.hasProjectPermission(projectId, Permission.PROJECT_VIEW));
    }

    @Test
    public void testIsRepositoryAccessible() throws Exception {
        int repoId = 99;
        InternalRepository repository = mock(InternalRepository.class);
        when(repository.getId()).thenReturn(repoId);
        InternalProject project = mock(InternalProject.class);
        when(repository.getProject()).thenReturn(project);
        final AtomicBoolean isRepoPublic = new AtomicBoolean(false);
        final AtomicBoolean isProjectPublic = new AtomicBoolean(false);
        when(repositoryDao.getById(repoId)).thenReturn(repository);
        when(repository.isPublic()).thenAnswer(new Answer<Boolean>() {
            @Override
            public Boolean answer(InvocationOnMock invocationOnMock) throws Throwable {
                return isRepoPublic.get();
            }
        });
        when(project.isPublic()).thenAnswer(new Answer<Boolean>() {
            @Override
            public Boolean answer(InvocationOnMock invocationOnMock) throws Throwable {
                return isProjectPublic.get();
            }
        });

        publicAccessFeatureEnabled = false;
        assertFalse(service.isPubliclyAccessible(repository));
        assertFalse(service.isPubliclyAccessible(project));
        assertFalse(service.isRepositoryAccessible(repository));
        assertFalse(service.isRepositoryAccessible(repoId));
        assertFalse(service.hasRepositoryPermission(repository, Permission.REPO_READ));
        assertFalse(service.hasRepositoryPermission(repoId, Permission.REPO_READ));

        isRepoPublic.set(true);
        assertFalse(service.isPubliclyAccessible(repository));
        assertFalse(service.isPubliclyAccessible(project));
        assertFalse(service.isRepositoryAccessible(repository));
        assertFalse(service.isRepositoryAccessible(repoId));
        assertFalse(service.hasRepositoryPermission(repository, Permission.REPO_READ));
        assertFalse(service.hasRepositoryPermission(repoId, Permission.REPO_READ));

        publicAccessFeatureEnabled = true;
        assertTrue(service.isPubliclyAccessible(repository));
        assertFalse(service.isPubliclyAccessible(project));
        assertTrue(service.isRepositoryAccessible(repository));
        assertTrue(service.isRepositoryAccessible(repoId));
        assertFalse(service.hasRepositoryPermission(repository, Permission.REPO_READ));
        assertFalse(service.hasRepositoryPermission(repoId, Permission.REPO_READ));

        isRepoPublic.set(false);
        isProjectPublic.set(true);
        assertTrue(service.isPubliclyAccessible(repository));
        assertTrue(service.isPubliclyAccessible(project));
        assertTrue(service.isRepositoryAccessible(repository));
        assertTrue(service.isRepositoryAccessible(repoId));
        assertFalse(service.hasRepositoryPermission(repository, Permission.REPO_READ));
        assertFalse(service.hasRepositoryPermission(repoId, Permission.REPO_READ));

        isProjectPublic.set(false);
        PermissionGraph graph = mock(IterablePermissionGraph.class);
        final AtomicBoolean isGrantedRepoRead = new AtomicBoolean(false);
        when(graph.isGranted(Permission.REPO_READ, repository)).thenAnswer(new Answer<Boolean>() {
            @Override
            public Boolean answer(InvocationOnMock invocationOnMock) throws Throwable {
                return isGrantedRepoRead.get();
            }
        });
        currentToken = StashUserAuthenticationToken.forUser(createMockUser(1));
        currentToken.setPermissions(graph);
        assertFalse(service.isPubliclyAccessible(repository));
        assertFalse(service.isPubliclyAccessible(project));
        assertFalse(service.isRepositoryAccessible(repository));
        assertFalse(service.isRepositoryAccessible(repoId));
        assertFalse(service.hasRepositoryPermission(repository, Permission.REPO_READ));
        assertFalse(service.hasRepositoryPermission(repoId, Permission.REPO_READ));

        isRepoPublic.set(true);
        assertTrue(service.isPubliclyAccessible(repository));
        assertFalse(service.isPubliclyAccessible(project));
        assertTrue(service.isRepositoryAccessible(repository));
        assertTrue(service.isRepositoryAccessible(repoId));
        assertTrue(service.hasRepositoryPermission(repository, Permission.REPO_READ));
        assertTrue(service.hasRepositoryPermission(repoId, Permission.REPO_READ));

        isRepoPublic.set(false);
        isGrantedRepoRead.set(true);
        assertFalse(service.isPubliclyAccessible(repository));
        assertFalse(service.isPubliclyAccessible(project));
        assertTrue(service.isRepositoryAccessible(repository));
        assertTrue(service.isRepositoryAccessible(repoId));
        assertTrue(service.hasRepositoryPermission(repository, Permission.REPO_READ));
        assertTrue(service.hasRepositoryPermission(repoId, Permission.REPO_READ));

        publicAccessFeatureEnabled = false;
        assertFalse(service.isPubliclyAccessible(repository));
        assertFalse(service.isPubliclyAccessible(project));
        assertTrue(service.isRepositoryAccessible(repository));
        assertTrue(service.isRepositoryAccessible(repoId));
        assertTrue(service.hasRepositoryPermission(repository, Permission.REPO_READ));
        assertTrue(service.hasRepositoryPermission(repoId, Permission.REPO_READ));
    }

    @Test
    public void testRecoveryMode() {
        // user with no explicitly granted permission
        ApplicationUser recoveryUser = createMockUser(1, "recovery_admin");
        assertFalse(service.hasGlobalPermission(recoveryUser, Permission.SYS_ADMIN));

        // recovery mode activated
        when(recoveryModeService.isRecoveryModeOn()).thenReturn(true);
        when(recoveryModeService.getRecoveryUsername()).thenReturn("recovery_admin");

        assertTrue(service.hasGlobalPermission(recoveryUser, Permission.SYS_ADMIN));
    }

    private InternalProject createMockProject(int projectId) {
        InternalProject project = mock(InternalProject.class);
        when(project.getId()).thenReturn(projectId);
        when(projectDao.getById(projectId)).thenReturn(project);

        return project;
    }

    private InternalRepository createMockRepository(int repoId, InternalProject project) {
        InternalRepository repo = mock(InternalRepository.class);
        when(repo.getId()).thenReturn(repoId);
        when(repo.getProject()).thenReturn(project);
        when(repositoryDao.getById(eq(repoId))).thenReturn(repo);

        return repo;
    }

    private InternalProject mockFindGroupsByUser(final Function<? super PageRequest, ? extends Iterable<String>> callback) {
        return mockFindGroupsByUserWithPage(new Function<PageRequest, Page<String>>() {
            @Override
            public Page<String> apply(PageRequest request) {
                return PageUtils.createPage(callback.apply(request), request);
            }
        });
    }

    private InternalProject mockFindGroupsByUserWithPage(final Function<? super PageRequest, ? extends Page<String>> callback) {
        InternalProject project = mock(InternalProject.class);
        when(project.getId()).thenReturn(1);

        when(userService.findGroupsByUser(anyString(), any(PageRequest.class))).thenAnswer(new Answer<Page>() {
            public Page answer(InvocationOnMock invocation) throws Throwable {
                return callback.apply((PageRequest) invocation.getArguments()[1]);
            }
        });
        return project;
    }

    private void mockGroupMemberships(String userMatcher, final String...groups) {
        when(userService.findGroupsByUser(userMatcher, (PageRequest) any()))
                .thenAnswer(new Answer<Page<String>>() {
                    public Page<String> answer(InvocationOnMock invocation) throws Throwable {
                        return PageUtils.createPage(Arrays.asList(groups), (PageRequest) invocation.getArguments()[1]);
                    }
                });
    }

    private static GroupPermissionCriteria matchesCriteria(final Permission permission,
                                                           final String group,
                                                           final Integer projectId) {
        return matchesCriteria(permission, Collections.singleton(group), projectId);
    }

    private static GroupPermissionCriteria matchesCriteria(final Permission permission,
                                                           final Collection<String> groups,
                                                           final Integer projectId) {
        return argThat(new TypeSafeMatcher<GroupPermissionCriteria>() {
            @Override
            public boolean matchesSafely(GroupPermissionCriteria criteria) {
                if (permission != criteria.getPermission()) {
                    return false;
                }

                if (!Objects.equals(projectId, criteria.getProjectId())) {
                    return false;
                }

                // use the same logic as the removed matchesCriteria() to match the groups
                if (groups.isEmpty()) {
                    return criteria.getGroups().isEmpty();
                }

                for (String element : groups) {
                    if (!criteria.getGroups().contains(element)) {
                        return false;
                    }
                }

                return true;
            }

            @Override
            public void describeTo(Description description) {
                description.appendText("GroupPermissionCriteria(")
                        .appendValue(groups)
                        .appendText(", permission")
                        .appendValue(permission)
                        .appendText(", projectId=")
                        .appendValue(projectId)
                        .appendText(")");
            }
        });
    }

    private static UserPermissionCriteria matchesCriteria(final Permission permission,
                                                          final int userId,
                                                          final Integer projectId) {
        return argThat(new TypeSafeMatcher<UserPermissionCriteria>() {
            @Override
            public boolean matchesSafely(UserPermissionCriteria criteria) {
                return permission == criteria.getPermission() &&
                        userId == criteria.getUserId() &&
                        Objects.equals(projectId, criteria.getProjectId());
            }

            @Override
            public void describeTo(Description description) {
                description.appendText("UserPermissionCriteria(")
                        .appendValue(userId)
                        .appendText(", permission=")
                        .appendValue(permission)
                        .appendText(", projectId=")
                        .appendValue(projectId)
                        .appendText(")");
            }
        });
    }

    private ApplicationUser createMockUser(int userId, Permission... grantedPermissions) {
        for (Permission permission : grantedPermissions) {
            mockGrantedPermission(userId, permission, (Integer) null);
        }

        final ApplicationUser user = mock(ApplicationUser.class);
        when(user.isActive()).thenReturn(true);
        when(user.getId()).thenReturn(userId);
        return user;
    }

    private ApplicationUser createMockUser(int userId, String username, Permission... grantedPermissions) {
        ApplicationUser user =  createMockUser(userId, grantedPermissions);
        when(userService.getUserByName(eq(username))).thenReturn(user);
        when(user.getName()).thenReturn(username);
        return user;
    }

    private void mockGrantedPermission(Integer userId, Permission permission, Project project) {
        mockGrantedPermission(userId, permission, project.getId());
    }

    private void mockGrantedPermission(Integer userId, Permission permission, Repository repository) {
        mockGrantedPermission(userId, permission, repository.getId());
    }

    private void mockGrantedPermission(Integer userId, Permission permission, Integer resourceId) {
        PermissionGraph graph = mockPermissionGraph(userId);

        recordMockGrantedPermission(graph, userId, permission, resourceId);
        for (Permission p : permission.getInheritedPermissions()) {
            recordMockGrantedPermission(graph, userId, p, resourceId);
        }
    }

    private void recordMockGrantedPermission(PermissionGraph graph, Integer userId, Permission permission, final Integer resourceId) {
        when(permissionDao.isGrantedToUser(matchesCriteria(permission, userId, resourceId))).thenReturn(true);
        // record the 'any' permission for the requested permission
        when(graph.isGranted(eq(permission), isNull())).thenReturn(true);
        when(graph.isGranted(eq(permission), argThat(new TypeSafeMatcher<Object>() {
            @Override
            protected boolean matchesSafely(Object o) {
                return resourceId == null ||
                        (o instanceof Project && resourceId.equals(((Project) o).getId())) ||
                        (o instanceof Repository && resourceId.equals(((Repository) o).getId()));
            }

            @Override
            public void describeTo(Description description) {
                description.appendText("Resource(id =  " + resourceId + ")");
            }
        }))).thenReturn(true);
    }

    private PermissionGraph mockPermissionGraph(Integer userId) {
        PermissionGraph graph = userGraphs.get(userId);
        if (graph == null) {
            graph = mock(IterablePermissionGraph.class);
            userGraphs.put(userId, graph);
        }
        return graph;
    }

    private InternalApplicationUser createActiveUser(String name) {
        User crowdUser = mock(User.class);
        when(crowdUser.isActive()).thenReturn(true);
        when(crowdUser.getName()).thenReturn(name);
        return new InternalNormalUser.Builder()
                .name(name)
                .crowdUser(crowdUser)
                .build();
    }

    private InternalApplicationUser createUser(String name) {
        return new InternalNormalUser.Builder()
                .name(name)
                .build();
    }

    private StashUserAuthenticationToken token(ApplicationUser user, Permission... runAsPermissions) {
        DefaultPermissionGraph.Builder permBuilder = new DefaultPermissionGraph.Builder();
        for (Permission permission : runAsPermissions) {
            permBuilder.add(permission, null);
        }
        return new StashUserAuthenticationToken.Builder().user(user).elevatedPermissions(permBuilder.build()).build();
    }
}
