package com.atlassian.stash.internal.user;

import com.atlassian.bitbucket.auth.AuthenticationContext;
import com.atlassian.bitbucket.event.permission.*;
import com.atlassian.bitbucket.permission.*;
import com.atlassian.bitbucket.user.ApplicationUser;
import com.atlassian.bitbucket.user.TestApplicationUser;
import com.atlassian.crowd.embedded.api.CrowdService;
import com.atlassian.event.api.EventPublisher;
import com.atlassian.bitbucket.AbstractServiceTest;
import com.atlassian.bitbucket.AuthorisationException;
import com.atlassian.bitbucket.ForbiddenException;
import com.atlassian.bitbucket.IntegrityException;
import com.atlassian.bitbucket.license.LicenseLimitException;
import com.atlassian.bitbucket.i18n.I18nService;
import com.atlassian.bitbucket.i18n.KeyedMessage;
import com.atlassian.stash.internal.project.InternalNormalProject;
import com.atlassian.stash.internal.project.InternalPersonalProject;
import com.atlassian.stash.internal.project.InternalProject;
import com.atlassian.stash.internal.repository.InternalRepository;
import com.atlassian.bitbucket.license.LicenseService;
import com.atlassian.bitbucket.project.Project;
import com.atlassian.bitbucket.util.ListPageProvider;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Lists;
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.Mockito;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.runners.MockitoJUnitRunner;
import org.mockito.stubbing.Answer;

import java.util.ArrayList;
import java.util.List;
import java.util.function.Predicate;

import static com.atlassian.bitbucket.permission.Permission.*;
import static org.mockito.Matchers.eq;
import static org.mockito.Mockito.*;

@RunWith(MockitoJUnitRunner.class)
public class DefaultPermissionAdminServiceTest extends AbstractServiceTest {

    private static final InternalProject TEST_PROJECT = new InternalNormalProject.Builder().id(1).key("TEST").name("Test Project").build();
    private static final InternalRepository TEST_REPO = new InternalRepository.Builder().id(1).name("Test Repository").project(TEST_PROJECT).build();
    private static final InternalApplicationUser TEST_USER = new InternalNormalUser.Builder().id(1).name("Tester").build();
    private static final InternalApplicationUser TEST_USER2 = new InternalNormalUser.Builder().id(2).name("Tester2").build();

    @Mock
    private AuthenticationContext authenticationContext;
    @Mock
    private CrowdService crowdService;
    @Mock
    private EventPublisher eventPublisher;
    private List<Object> eventsPublished = new ArrayList<>();
    @Mock
    private GlobalPermissionDao globalPermissionDao;
    @Mock
    private ProjectPermissionDao projectPermissionDao;
    @Mock
    private RepositoryPermissionDao repositoryPermissionDao;
    @Mock
    private I18nService i18nService;
    @Mock
    private LicenseService licenseService;
    @Mock
    private PermissionService permissionService;
    @Mock
    private InternalUserService userService;
    @Mock
    private PermissionValidationService permissionValidationService;

    @InjectMocks
    private DefaultPermissionAdminService permissionAdminService;

    private List<InternalGlobalPermission> globalPermissions = Lists.newArrayList();
    private List<InternalProjectPermission> projectPermissions = Lists.newArrayList();
    private List<InternalRepositoryPermission> repositoryPermissions = Lists.newArrayList();

    public DefaultPermissionAdminServiceTest() {
        super(DefaultPermissionAdminService.class, PermissionAdminService.class);
    }

    @Before
    public void setUp() throws Exception {
        setPageLimit(permissionAdminService, "maxGroups", 1000);
        setPageLimit(permissionAdminService, "maxUsers", 1000);

        when(globalPermissionDao.create(isA(InternalGlobalPermission.class))).thenAnswer(new Answer<InternalGlobalPermission>() {
            @Override
            public InternalGlobalPermission answer(InvocationOnMock invocation) throws Throwable {
                InternalGlobalPermission permission = (InternalGlobalPermission) invocation.getArguments()[0];
                globalPermissions.add(permission);
                return permission;
            }
        });

        when(projectPermissionDao.create(isA(InternalProjectPermission.class))).thenAnswer(new Answer<InternalProjectPermission>() {
            @Override
            public InternalProjectPermission answer(InvocationOnMock invocation) throws Throwable {
                InternalProjectPermission permission = (InternalProjectPermission) invocation.getArguments()[0];
                projectPermissions.add(permission);
                return permission;
            }
        });

        when(repositoryPermissionDao.create(isA(InternalRepositoryPermission.class))).thenAnswer(new Answer<InternalRepositoryPermission>() {
            @Override
            public InternalRepositoryPermission answer(InvocationOnMock invocation) throws Throwable {
                InternalRepositoryPermission permission = (InternalRepositoryPermission) invocation.getArguments()[0];
                repositoryPermissions.add(permission);
                return permission;
            }
        });

        when(globalPermissionDao.revoke(isA(InternalGlobalPermission.class))).thenAnswer(new Answer<Object>() {
            @Override
            public Object answer(InvocationOnMock invocation) throws Throwable {
                InternalGlobalPermission permission = (InternalGlobalPermission) invocation.getArguments()[0];
                return globalPermissions.remove(permission) ? 1 : 0;
            }
        });
        when(projectPermissionDao.revoke(isA(InternalProjectPermission.class))).thenAnswer(new Answer<Object>() {
            @Override
            public Object answer(InvocationOnMock invocation) throws Throwable {
                InternalProjectPermission permission = (InternalProjectPermission) invocation.getArguments()[0];
                return projectPermissions.remove(permission) ? 1 : 0;
            }
        });
        when(repositoryPermissionDao.revoke(isA(InternalRepositoryPermission.class))).thenAnswer(new Answer<Object>() {
            @Override
            public Object answer(InvocationOnMock invocation) throws Throwable {
                InternalRepositoryPermission permission = (InternalRepositoryPermission) invocation.getArguments()[0];
                return repositoryPermissions.remove(permission) ? 1 : 0;
            }
        });

        when(globalPermissionDao.hasPermissionEntry(isA(InternalGlobalPermission.class))).thenAnswer(
                new Answer<Object>() {
                    @Override
                    public Object answer(InvocationOnMock invocation) throws Throwable {
                        InternalGlobalPermission perm = (InternalGlobalPermission) invocation.getArguments()[0];
                        return globalPermissions.contains(perm);
                    }
                }
        );
        when(projectPermissionDao.hasPermissionEntry(isA(InternalProjectPermission.class))).thenAnswer(
                new Answer<Object>() {
                    @Override
                    public Object answer(InvocationOnMock invocation) throws Throwable {
                        InternalProjectPermission perm = (InternalProjectPermission) invocation.getArguments()[0];
                        return projectPermissions.contains(perm);
                    }
                }
        );
        when(repositoryPermissionDao.hasPermissionEntry(isA(InternalRepositoryPermission.class))).thenAnswer(
                new Answer<Object>() {
                    @Override
                    public Object answer(InvocationOnMock invocation) throws Throwable {
                        InternalRepositoryPermission perm = (InternalRepositoryPermission) invocation.getArguments()[0];
                        return repositoryPermissions.contains(perm);
                    }
                }
        );

        when(i18nService.createKeyedMessage(anyString(), Matchers.<Object[]>anyVararg()))
                .thenReturn(new KeyedMessage("key", "localizedmessage", "rootmessage"));

        when(authenticationContext.getCurrentUser()).thenReturn(TEST_USER);

        doAnswer(new Answer<Void>() {
            @Override
            public Void answer(InvocationOnMock invocation) throws Throwable {
                Object event = invocation.getArguments()[0];
                eventsPublished.add(event);
                return null;
            }
        }).when(eventPublisher).publish(any());
    }

    @Test
    public void testAnyonePermissionsOnProject() throws Exception {
        projectPermissions.add(new InternalProjectPermission.Builder().permission(PROJECT_READ).project(TEST_PROJECT).build());
        assertTrue("Unexpected permission not granted", permissionAdminService.hasAllProjectPermission(Permission.PROJECT_READ, TEST_PROJECT));
    }

    @Test(expected = AuthorisationException.class)
    public void testGrantingGlobalPermissionWithoutAdminPermission() throws Exception {
        permissionAdminService.setPermission(new SetPermissionRequest.Builder()
                .globalPermission(LICENSED_USER)
                .user(TEST_USER2)
                .build());
    }

    @Test(expected = AuthorisationException.class)
    public void testGrantingSysadminGlobalPermissionWithAdminPermission() throws Exception {
        // These are superfluous, but document the intent of this test
        when(permissionService.hasGlobalPermission(eq(ADMIN))).thenReturn(true);
        when(permissionService.hasGlobalPermission(eq(SYS_ADMIN))).thenReturn(false);
        permissionAdminService.setPermission(new SetPermissionRequest.Builder()
                .globalPermission(SYS_ADMIN)
                .user(TEST_USER2)
                .build());
    }

    @Test
    public void testGrantingGlobalPermissionWithSysadminPermission() throws Exception {
        when(permissionService.hasGlobalPermission(eq(SYS_ADMIN))).thenReturn(true);
        permissionAdminService.setPermission(new SetPermissionRequest.Builder()
                .globalPermission(SYS_ADMIN)
                .user(TEST_USER2)
                .build());
        // Doesn't throw an exception
    }

    @Test
    public void testSettingGlobalPermission() throws Exception {
        when(permissionService.hasGlobalPermission(eq(ADMIN))).thenReturn(true);
        permissionAdminService.setPermission(new SetPermissionRequest.Builder()
                .globalPermission(LICENSED_USER)
                .user(TEST_USER2)
                .build());
        assertTrue(globalPermissions.contains(new InternalGlobalPermission.Builder()
                .permission(LICENSED_USER).user(TEST_USER2).build()));
    }

    @Test(expected = AuthorisationException.class)
    public void testSettingProjectPermissionWithoutProjectAdminPermission() throws Exception {
        permissionAdminService.setPermission(new SetPermissionRequest.Builder()
                .projectPermission(PROJECT_READ, TEST_PROJECT)
                .user(TEST_USER2)
                .build());
    }

    @Test
    public void testSettingProjectPermission() throws Exception {
        when(permissionService.hasProjectPermission(same(TEST_PROJECT), eq(PROJECT_ADMIN))).thenReturn(true);
        permissionAdminService.setPermission(new SetPermissionRequest.Builder()
                .projectPermission(PROJECT_READ, TEST_PROJECT)
                .user(TEST_USER2)
                .build());
        assertTrue(projectPermissions.contains(new InternalProjectPermission.Builder()
                .permission(PROJECT_READ).user(TEST_USER2).project(TEST_PROJECT).build()));
    }

    @Test(expected = AuthorisationException.class)
    public void testSettingRepositoryPermissionWithoutRepositoryAdminPermission() throws Exception {
        permissionAdminService.setPermission(new SetPermissionRequest.Builder()
                .repositoryPermission(REPO_READ, TEST_REPO)
                .user(TEST_USER2)
                .build());
    }

    @Test
    public void testSettingRepositoryPermission() throws Exception {
        when(permissionService.hasRepositoryPermission(same(TEST_REPO), eq(REPO_ADMIN))).thenReturn(true);
        permissionAdminService.setPermission(new SetPermissionRequest.Builder()
                .repositoryPermission(REPO_READ, TEST_REPO)
                .user(TEST_USER2)
                .build());
        assertTrue(repositoryPermissions.contains(new InternalRepositoryPermission.Builder()
                .permission(REPO_READ).user(TEST_USER2).repository(TEST_REPO).build()));
    }

    @Test
    public void testSettingGlobalPermissionForGroup() throws Exception {
        final String testGroup = "TestGroup";

        when(permissionService.hasGlobalPermission(ADMIN)).thenReturn(true);

        permissionAdminService.setPermission(new SetPermissionRequest.Builder()
                .globalPermission(ADMIN)
                .group(testGroup)
                .build());
        assertEquals("Permission was not set", 1, globalPermissions.size());
        assertTrue("Incorrect Permission stored", globalPermissions.contains(new InternalGlobalPermission.Builder().permission(Permission.ADMIN).group(testGroup).build()));

        permissionAdminService.setPermission(new SetPermissionRequest.Builder()
                .globalPermission(PROJECT_CREATE)
                .group(testGroup)
                .build());
        assertTrue("Permission was not correctly set", globalPermissions.contains(new InternalGlobalPermission.Builder().permission(Permission.PROJECT_CREATE).group(testGroup).build()));
        assertEquals("Old permission was not correctly removed", 1, globalPermissions.size());

        when(permissionService.hasGlobalPermission(SYS_ADMIN)).thenReturn(true);

        permissionAdminService.setPermission(new SetPermissionRequest.Builder()
                .globalPermission(SYS_ADMIN)
                .group(testGroup)
                .build());
        assertTrue("Permission was not correctly set", globalPermissions.contains(new InternalGlobalPermission.Builder().permission(Permission.SYS_ADMIN).group(testGroup).build()));
        assertEquals("Old permission was not correctly removed", 1, globalPermissions.size());
    }

    @Test(expected = IllegalArgumentException.class)
    public void testSettingGlobalPermissionForGroupWithProjectPermission() throws Exception {
        permissionAdminService.setPermission(new SetPermissionRequest.Builder()
                .globalPermission(PROJECT_READ)
                .group("TestGroup")
                .build());
    }

    @Test(expected = IllegalArgumentException.class)
    public void testSettingGlobalPermissionForGroupWithRepositoryPermission() throws Exception {
        permissionAdminService.setPermission(new SetPermissionRequest.Builder()
                .globalPermission(REPO_READ)
                .group("TestGroup")
                .build());
    }

    @Test
    public void testSettingGlobalPermissionForUser() throws Exception {
        when(permissionService.hasGlobalPermission(ADMIN)).thenReturn(true);

        permissionAdminService.setPermission(new SetPermissionRequest.Builder()
                .globalPermission(ADMIN)
                .user(TEST_USER2)
                .build());
        assertEquals("Permission was not stored", 1, globalPermissions.size());
        assertTrue("Incorrect Permission stored", globalPermissions.contains(new InternalGlobalPermission.Builder().permission(ADMIN).user(TEST_USER2).build()));

        permissionAdminService.setPermission(new SetPermissionRequest.Builder()
                .globalPermission(PROJECT_CREATE)
                .user(TEST_USER2)
                .build());
        assertTrue("Incorrect Permission stored", globalPermissions.contains(new InternalGlobalPermission.Builder().permission(PROJECT_CREATE).user(TEST_USER2).build()));
        assertEquals("Old permission was not correctly removed", 1, globalPermissions.size());

        when(permissionService.hasGlobalPermission(SYS_ADMIN)).thenReturn(true);

        permissionAdminService.setPermission(new SetPermissionRequest.Builder()
                .globalPermission(SYS_ADMIN)
                .user(TEST_USER2)
                .build());
        assertTrue("Incorrect Permission stored", globalPermissions.contains(new InternalGlobalPermission.Builder().permission(SYS_ADMIN).user(TEST_USER2).build()));
        assertEquals("Old permission was not correctly removed", 1, globalPermissions.size());
    }

    @Test(expected = IllegalArgumentException.class)
    public void testSettingGlobalPermissionForUserWithProjectPermission() throws Exception {
        permissionAdminService.setPermission(new SetPermissionRequest.Builder()
                .globalPermission(PROJECT_READ)
                .user(TEST_USER2)
                .build());
    }

    @Test(expected = IllegalArgumentException.class)
    public void testSettingGlobalPermissionForUserWithRepositoryPermission() throws Exception {
        permissionAdminService.setPermission(new SetPermissionRequest.Builder()
                .globalPermission(REPO_READ)
                .user(TEST_USER2)
                .build());
    }

    @Test
    public void testSettingProjectPermissionForGroup() throws Exception {
        final String testgroup = "TestGroup";

        when(permissionService.hasProjectPermission(eq(TEST_PROJECT), any(Permission.class))).thenReturn(true);

        permissionAdminService.setPermission(new SetPermissionRequest.Builder()
                .projectPermission(PROJECT_WRITE, TEST_PROJECT)
                .group(testgroup)
                .build());
        assertEquals("Permission was not stored", 1, projectPermissions.size());
        assertTrue("Incorrect Permission stored", projectPermissions.contains(new InternalProjectPermission.Builder().permission(PROJECT_WRITE).project(TEST_PROJECT).group(testgroup).build()));

        permissionAdminService.setPermission(new SetPermissionRequest.Builder()
                .projectPermission(PROJECT_READ, TEST_PROJECT)
                .group(testgroup)
                .build());
        assertTrue("Permission was not set", projectPermissions.contains(new InternalProjectPermission.Builder().permission(PROJECT_READ).project(TEST_PROJECT).group(testgroup).build()));
        assertEquals("Old permission was not correctly removed", 1, projectPermissions.size());

        permissionAdminService.setPermission(new SetPermissionRequest.Builder()
                .projectPermission(PROJECT_ADMIN, TEST_PROJECT)
                .group(testgroup)
                .build());
        assertTrue("Permission was not set", projectPermissions.contains(new InternalProjectPermission.Builder().permission(PROJECT_ADMIN).project(TEST_PROJECT).group(testgroup).build()));
        assertEquals("Old permission was not correctly removed", 1, projectPermissions.size());
    }

    @Test(expected = IllegalArgumentException.class)
    public void testSettingProjectPermissionForGroupWithGlobalPermission() throws Exception {
        permissionAdminService.setPermission(new SetPermissionRequest.Builder()
                .projectPermission(ADMIN, TEST_PROJECT)
                .group("TestGroup")
                .build());
    }

    @Test(expected = IllegalArgumentException.class)
    public void testSettingProjectPermissionForGroupWithRepositoryPermission() throws Exception {
        permissionAdminService.setPermission(new SetPermissionRequest.Builder()
                .projectPermission(REPO_READ, TEST_PROJECT)
                .group("TestGroup")
                .build());
    }

    @Test(expected = IntegrityException.class)
    public void testSettingProjectPermissionForGroupOnPersonalProject() throws Exception {
        when(permissionService.hasProjectPermission(any(Project.class), eq(PROJECT_ADMIN))).thenReturn(true);

        permissionAdminService.setPermission(new SetPermissionRequest.Builder()
                .projectPermission(PROJECT_READ, createPersonalProject())
                .group("TestGroup")
                .build());
    }

    @Test(expected = IllegalArgumentException.class)
    public void testSettingProjectViewPermissionForGroup() throws Exception {
        when(permissionService.hasProjectPermission(any(Project.class), eq(PROJECT_ADMIN))).thenReturn(true);

        permissionAdminService.setPermission(new SetPermissionRequest.Builder()
                .projectPermission(PROJECT_VIEW, createPersonalProject())
                .group("TestGroup")
                .build());
    }

    @Test
    public void testSettingProjectPermissionForUser() throws Exception {
        when(permissionService.hasProjectPermission(TEST_PROJECT, PROJECT_ADMIN)).thenReturn(true);

        permissionAdminService.setPermission(new SetPermissionRequest.Builder()
                .projectPermission(PROJECT_WRITE, TEST_PROJECT)
                .user(TEST_USER2)
                .build());
        assertEquals("Permission was not stored", 1, projectPermissions.size());
        assertTrue("Incorrect Permission stored", projectPermissions.contains(new InternalProjectPermission.Builder().permission(PROJECT_WRITE).project(TEST_PROJECT).user(TEST_USER2).build()));

        permissionAdminService.setPermission(new SetPermissionRequest.Builder()
                .projectPermission(PROJECT_READ, TEST_PROJECT)
                .user(TEST_USER2)
                .build());
        assertTrue("Permission was not set", projectPermissions.contains(new InternalProjectPermission.Builder().permission(PROJECT_READ).project(TEST_PROJECT).user(TEST_USER2).build()));
        assertEquals("Old permission was not correctly removed", 1, projectPermissions.size());

        permissionAdminService.setPermission(new SetPermissionRequest.Builder()
                .projectPermission(PROJECT_ADMIN, TEST_PROJECT)
                .user(TEST_USER2)
                .build());
        assertTrue("Permission was not set", projectPermissions.contains(new InternalProjectPermission.Builder().permission(PROJECT_ADMIN).project(TEST_PROJECT).user(TEST_USER2).build()));
        assertEquals("Old permission was not correctly removed", 1, projectPermissions.size());
    }

    @Test(expected = IllegalArgumentException.class)
    public void testSettingProjectPermissionForUserWithGlobalPermission() throws Exception {
        permissionAdminService.setPermission(new SetPermissionRequest.Builder()
                .projectPermission(ADMIN, TEST_PROJECT)
                .user(TEST_USER2)
                .build());
    }

    @Test(expected = IllegalArgumentException.class)
    public void testSettingProjectPermissionForUserWithRepositoryPermission() throws Exception {
        permissionAdminService.setPermission(new SetPermissionRequest.Builder()
                .projectPermission(REPO_READ, TEST_PROJECT)
                .user(TEST_USER2)
                .build());
    }

    @Test(expected = IntegrityException.class)
    public void testSettingProjectPermissionForUserOnPersonalProject() throws Exception {
        when(permissionService.hasProjectPermission(any(Project.class), eq(PROJECT_ADMIN))).thenReturn(true);

        permissionAdminService.setPermission(new SetPermissionRequest.Builder()
                .projectPermission(PROJECT_READ, createPersonalProject())
                .user(TEST_USER2)
                .build());
    }

    @Test(expected = IllegalArgumentException.class)
    public void testSettingProjectViewPermissionForUser() throws Exception {
        when(permissionService.hasProjectPermission(any(Project.class), eq(PROJECT_ADMIN))).thenReturn(true);

        permissionAdminService.setPermission(new SetPermissionRequest.Builder()
                .projectPermission(PROJECT_VIEW, createPersonalProject())
                .user(TEST_USER2)
                .build());
    }

    @Test
    public void testSettingRepositoryPermissionForGroup() throws Exception {
        final String testgroup = "TestGroup";

        when(permissionService.hasRepositoryPermission(TEST_REPO, REPO_ADMIN)).thenReturn(true);

        permissionAdminService.setPermission(new SetPermissionRequest.Builder()
                .repositoryPermission(REPO_WRITE, TEST_REPO)
                .group(testgroup)
                .build());
        assertEquals("Permission was not stored", 1, repositoryPermissions.size());
        assertTrue("Incorrect Permission stored", repositoryPermissions.contains(new InternalRepositoryPermission.Builder().permission(REPO_WRITE).repository(TEST_REPO).group(testgroup).build()));

        permissionAdminService.setPermission(new SetPermissionRequest.Builder()
                .repositoryPermission(REPO_READ, TEST_REPO)
                .group(testgroup)
                .build());
        assertTrue("Permission was not set", repositoryPermissions.contains(new InternalRepositoryPermission.Builder().permission(REPO_READ).repository(TEST_REPO).group(testgroup).build()));
        assertEquals("Old permission was not correctly removed", 1, repositoryPermissions.size());

        permissionAdminService.setPermission(new SetPermissionRequest.Builder()
                .repositoryPermission(REPO_ADMIN, TEST_REPO)
                .group(testgroup)
                .build());
        assertTrue("Permission was not set", repositoryPermissions.contains(new InternalRepositoryPermission.Builder().permission(REPO_ADMIN).repository(TEST_REPO).group(testgroup).build()));
        assertEquals("Old permission was not correctly removed", 1, repositoryPermissions.size());
    }

    @Test(expected = IllegalArgumentException.class)
    public void testSettingRepositoryPermissionForGroupWithGlobalPermission() throws Exception {
        permissionAdminService.setPermission(new SetPermissionRequest.Builder()
                .repositoryPermission(ADMIN, TEST_REPO)
                .group("TestGroup")
                .build());
    }

    @Test(expected = IllegalArgumentException.class)
    public void testSettingRepositoryPermissionForGroupWithProjectPermission() throws Exception {
        permissionAdminService.setPermission(new SetPermissionRequest.Builder()
                .repositoryPermission(PROJECT_READ, TEST_REPO)
                .group("TestGroup")
                .build());
    }

    @Test
    public void testSettingRepositoryPermissionForUser() throws Exception {
        when(permissionService.hasRepositoryPermission(TEST_REPO, REPO_ADMIN)).thenReturn(true);

        permissionAdminService.setPermission(new SetPermissionRequest.Builder()
                .repositoryPermission(REPO_WRITE, TEST_REPO)
                .user(TEST_USER2)
                .build());
        assertEquals("Permission was not stored", 1, repositoryPermissions.size());
        assertTrue("Incorrect Permission stored", repositoryPermissions.contains(new InternalRepositoryPermission.Builder().permission(REPO_WRITE).repository(TEST_REPO).user(TEST_USER2).build()));

        permissionAdminService.setPermission(new SetPermissionRequest.Builder()
                .repositoryPermission(REPO_READ, TEST_REPO)
                .user(TEST_USER2)
                .build());
        assertTrue("Permission was not set", repositoryPermissions.contains(new InternalRepositoryPermission.Builder().permission(REPO_READ).repository(TEST_REPO).user(TEST_USER2).build()));
        assertEquals("Old permission was not correctly removed", 1, repositoryPermissions.size());

        permissionAdminService.setPermission(new SetPermissionRequest.Builder()
                .repositoryPermission(REPO_ADMIN, TEST_REPO)
                .user(TEST_USER2)
                .build());
        assertTrue("Permission was not set", repositoryPermissions.contains(new InternalRepositoryPermission.Builder().permission(REPO_ADMIN).repository(TEST_REPO).user(TEST_USER2).build()));
        assertEquals("Old permission was not correctly removed", 1, repositoryPermissions.size());
    }

    @Test(expected = IllegalArgumentException.class)
    public void testSettingRepositoryPermissionForUserWithGlobalPermission() throws Exception {
        permissionAdminService.setPermission(new SetPermissionRequest.Builder()
                .repositoryPermission(ADMIN, TEST_REPO)
                .user(TEST_USER2)
                .build());
    }

    @Test(expected = IllegalArgumentException.class)
    public void testSettingRepositoryPermissionForUserWithProjectPermission() throws Exception {
        permissionAdminService.setPermission(new SetPermissionRequest.Builder()
                .repositoryPermission(PROJECT_READ, TEST_REPO)
                .user(TEST_USER2)
                .build());
    }

    @Test
    public void testPermissionGrantedEvent() {
        when(permissionService.hasProjectPermission(TEST_PROJECT, PROJECT_ADMIN)).thenReturn(true);

        permissionAdminService.setPermission(new SetPermissionRequest.Builder()
                .projectPermission(PROJECT_WRITE, TEST_PROJECT)
                .group("group")
                .build());

        boolean eventPublished = false;
        for (Object event : eventsPublished) {
            if (event instanceof ProjectPermissionGrantedEvent) {
                assertSame("Incorrect permission on event", PROJECT_WRITE, ((ProjectPermissionGrantedEvent) event).getPermission());
                assertEquals("Incorrect Group", "group", ((ProjectPermissionGrantedEvent) event).getAffectedGroup());
                assertSame("Incorrect Project", TEST_PROJECT, ((ProjectPermissionGrantedEvent) event).getProject());
                assertNull("User set incorrectly", ((ProjectPermissionGrantedEvent) event).getAffectedUser());
                eventPublished = true;
                break;
            }
        }

        assertTrue("Could not find permission granted event", eventPublished);
    }

    @Test
    public void testPermissionRevokedEvent() {
        globalPermissions.add(new InternalGlobalPermission.Builder().permission(SYS_ADMIN).user(TEST_USER2).build());
        permissionAdminService.revokeAllGlobalPermissions(TEST_USER2);
        boolean eventPublished = false;
        for (Object event : eventsPublished) {
            if (event instanceof GlobalPermissionRevokedEvent) {
                assertSame("Incorrect permission on event", SYS_ADMIN, ((GlobalPermissionRevokedEvent) event).getPermission());
                assertNull("Group set incorrectly", ((GlobalPermissionRevokedEvent) event).getAffectedGroup());
                assertSame("User set incorrectly", TEST_USER2, ((GlobalPermissionRevokedEvent) event).getAffectedUser());
                eventPublished = true;
                break;
            }
        }

        assertTrue("Could not find permission granted event", eventPublished);
    }

    @Test
    public void testDoubleCreate() throws Exception {
        when(permissionService.hasProjectPermission(TEST_PROJECT, PROJECT_ADMIN)).thenReturn(true);

        permissionAdminService.setPermission(new SetPermissionRequest.Builder()
                .projectPermission(PROJECT_WRITE, TEST_PROJECT)
                .group("group")
                .build());
        assertTrue("Incorrect Permission stored", projectPermissions.contains(new InternalProjectPermission.Builder().permission(Permission.PROJECT_WRITE).project(TEST_PROJECT).group("group").build()));

        permissionAdminService.setPermission(new SetPermissionRequest.Builder()
                .projectPermission(PROJECT_WRITE, TEST_PROJECT)
                .group("group")
                .build());
        assertTrue("Permission was doubled up", projectPermissions.size() == 1);
    }

    @Test
    public void testGrantAllProjectPermission() throws Exception {
        permissionAdminService.grantAllProjectPermission(Permission.PROJECT_WRITE, TEST_PROJECT);
        assertTrue("Incorrect Permission stored", projectPermissions.contains(new InternalProjectPermission.Builder().permission(Permission.PROJECT_WRITE).project(TEST_PROJECT).build()));
    }

    @Test(expected = IntegrityException.class)
    public void testGrantAllProjectPermissionOnPersonalProject() throws Exception {
        permissionAdminService.grantAllProjectPermission(PROJECT_READ, createPersonalProject());
    }

    @Test(expected = IllegalArgumentException.class)
    public void testGrantAllProjectViewPermission() throws Exception {
        permissionAdminService.grantAllProjectPermission(PROJECT_VIEW, createPersonalProject());
    }

    @Test
    public void testGrantAllProjectPermissionShouldFireProjectPermissionGrantedRequestedEvent() throws Exception {
        permissionAdminService.grantAllProjectPermission(Permission.PROJECT_WRITE, TEST_PROJECT);
        boolean eventPublished = false;
        for (Object event : eventsPublished) {
            if (event instanceof ProjectPermissionGrantRequestedEvent) {
                assertSame("Incorrect permission on event", PROJECT_WRITE, ((ProjectPermissionGrantRequestedEvent) event).getPermission());
                assertSame("Incorrect Project", TEST_PROJECT, ((ProjectPermissionGrantRequestedEvent) event).getProject());
                eventPublished = true;
                break;
            }
        }
        assertTrue(eventPublished);
    }

    @Test
    public void testGrantAllProjectPermissionShouldFireProjectPermissionGrantedEvent() throws Exception {
        permissionAdminService.grantAllProjectPermission(Permission.PROJECT_WRITE, TEST_PROJECT);
        boolean eventPublished = false;
        for (Object event : eventsPublished) {
            if (event instanceof ProjectPermissionGrantedEvent) {
                assertSame("Incorrect permission on event", PROJECT_WRITE, ((ProjectPermissionGrantedEvent) event).getPermission());
                assertSame("Incorrect Project", TEST_PROJECT, ((ProjectPermissionGrantedEvent) event).getProject());
                eventPublished = true;
                break;
            }
        }
        assertTrue(eventPublished);
    }

    @Test
    public void testGrantAllProjectPermissionShouldNotFireProjectPermissionGrantedEventIfEffectivePermissionDoesNotChange() throws Exception {
        when(projectPermissionDao.getHighestDefaultPermission(TEST_PROJECT.getId()))
                .thenReturn(Permission.PROJECT_WRITE);

        permissionAdminService.revokeAllProjectPermission(Permission.PROJECT_READ, TEST_PROJECT);

        permissionAdminService.grantAllProjectPermission(Permission.PROJECT_WRITE, TEST_PROJECT);
        eventsPublished.clear();

        permissionAdminService.grantAllProjectPermission(Permission.PROJECT_READ, TEST_PROJECT);

        boolean eventPublished = false;
        for (Object event : eventsPublished) {
            if (event instanceof ProjectPermissionGrantedEvent || event instanceof ProjectPermissionGrantRequestedEvent) {
                eventPublished = true;
                break;
            }
        }

        assertFalse(eventPublished);
    }

    @Test
    public void testGrantAllProjectPermissionShouldNotFireProjectPermissionGrantedEventIfPermissionAlreadyGranted() throws Exception {
        permissionAdminService.grantAllProjectPermission(Permission.PROJECT_WRITE, TEST_PROJECT);
        eventsPublished.clear();
        permissionAdminService.grantAllProjectPermission(Permission.PROJECT_WRITE, TEST_PROJECT);
        boolean eventPublished = false;
        for (Object event : eventsPublished) {
            if (event instanceof ProjectPermissionGrantedEvent) {
                eventPublished = true;
                break;
            }
        }
        assertFalse(eventPublished);
    }

    @Test
    public void testRevokeProjectGroupPermission() throws Exception {
        when(permissionService.hasProjectPermission(TEST_PROJECT, PROJECT_ADMIN)).thenReturn(true);

        permissionAdminService.setPermission(new SetPermissionRequest.Builder()
                .projectPermission(PROJECT_WRITE, TEST_PROJECT)
                .group("group")
                .build());
        assertTrue("Permission not stored", projectPermissions.size() == 1);

        permissionAdminService.revokeAllProjectPermissions(TEST_PROJECT, "group");
        assertTrue("Permission not revoked", projectPermissions.size() == 0);
    }

    @Test(expected = IntegrityException.class)
    public void testRevokeProjectAdminGroupPermission() {
        // The user is a member of the group being revoked, do not have their own project_admin permission,
        // does not have the privilege through another group, so should not be allowed
        when(userService.isUserInGroup(TEST_USER, "group")).thenReturn(true);
        when(permissionService.hasProjectPermission(TEST_PROJECT, PROJECT_ADMIN)).thenReturn(true);

        permissionAdminService.revokeAllProjectPermissions(TEST_PROJECT, "group");
    }

    @Test
    public void testRevokeProjectAdminGroupPermission1() {
        // The user is not a member of the group being revoked, so no problems
        when(userService.isUserInGroup(TEST_USER, "group")).thenReturn(false);

        permissionAdminService.revokeAllProjectPermissions(TEST_PROJECT, "group");
    }

    @Test
    public void testRevokeProjectAdminGroupPermission2() {
        // The user is a member of the group being revoked, but have a higher permission so should be allowed
        when(userService.isUserInGroup(TEST_USER, "group")).thenReturn(true);
        when(permissionService.hasGlobalPermission(ADMIN)).thenReturn(true);

        permissionAdminService.revokeAllProjectPermissions(TEST_PROJECT, "group");
    }

    @Test(expected = IntegrityException.class)
    public void testRevokeProjectGroupPermissionOnPersonalProject() throws Exception {
        permissionAdminService.revokeAllProjectPermissions(createPersonalProject(), "group");
    }

    @Test
    public void testRevokeUsersProjectPermission() throws Exception {
        when(permissionService.hasProjectPermission(TEST_PROJECT, PROJECT_ADMIN)).thenReturn(true);

        permissionAdminService.setPermission(new SetPermissionRequest.Builder()
                .projectPermission(PROJECT_WRITE, TEST_PROJECT)
                .user(TEST_USER2)
                .build());
        assertTrue("Permission not stored", projectPermissions.size() == 1);

        permissionAdminService.revokeAllGlobalPermissions(TEST_USER2);
        assertTrue("Permission was incorrectly revoked", projectPermissions.size() == 1);

        permissionAdminService.revokeAllProjectPermissions(TEST_PROJECT, TEST_USER2);
        assertTrue("Permission was not revoked", projectPermissions.size() == 0);
    }

    @Test(expected = IntegrityException.class)
    public void testRevokeProjectAdminUserPermission() {
        // The user is revoking PROJECT_ADMIN and it is their project admin so fail
        when(permissionService.hasProjectPermission(TEST_PROJECT, PROJECT_ADMIN)).thenReturn(true);

        permissionAdminService.revokeAllProjectPermissions(TEST_PROJECT, TEST_USER);
    }
    @Test
    public void testRevokeProjectAdminUserPermission1() throws Exception {
        // The user is revoking their PROJECT_ADMIN permission, but it is implied by their sysadmin rights, so allow
        when(permissionService.hasGlobalPermission(SYS_ADMIN)).thenReturn(true);

        permissionAdminService.revokeAllProjectPermissions(TEST_PROJECT, TEST_USER);
    }

    @Test(expected = IntegrityException.class)
    public void testRevokeProjectUserPermissionOnPersonalProject() throws Exception {
        permissionAdminService.revokeAllProjectPermissions(createPersonalProject(), TEST_USER);
    }

    @Test
    public void testRevokeAllProjectPermission() throws Exception {
        permissionAdminService.grantAllProjectPermission(Permission.PROJECT_WRITE, TEST_PROJECT);
        assertTrue("Permission not stored", projectPermissions.size() == 1);

        permissionAdminService.revokeAllProjectPermission(Permission.PROJECT_READ, TEST_PROJECT);
        assertTrue("Permission should not have been removed stored", projectPermissions.size() == 1);

        permissionAdminService.revokeAllProjectPermission(Permission.PROJECT_WRITE, TEST_PROJECT);
        assertTrue("Permission should have been removed stored", projectPermissions.size() == 0);
    }

    @Test
    public void testRevokeAllProjectPermissionShouldFireProjectPermissionModifiedEvent() throws Exception {
        when(projectPermissionDao.getHighestDefaultPermission(TEST_PROJECT.getId()))
                .thenReturn(Permission.PROJECT_READ);

        permissionAdminService.grantAllProjectPermission(Permission.PROJECT_READ, TEST_PROJECT);
        permissionAdminService.grantAllProjectPermission(Permission.PROJECT_WRITE, TEST_PROJECT);
        eventsPublished.clear();
        permissionAdminService.revokeAllProjectPermission(Permission.PROJECT_WRITE, TEST_PROJECT);

        boolean eventPublished = false;
        for (Object event : eventsPublished) {
            if (event instanceof ProjectPermissionModifiedEvent) {
                ProjectPermissionModifiedEvent projectPermissionModifiedEvent = (ProjectPermissionModifiedEvent) event;
                assertSame("Incorrect permission on event", PROJECT_READ, projectPermissionModifiedEvent.getPermission());
                assertSame("Incorrect Project", TEST_PROJECT, projectPermissionModifiedEvent.getProject());
                assertSame("Incorrent old event", PROJECT_WRITE, projectPermissionModifiedEvent.getOldValue());
                eventPublished = true;
                break;
            }
        }
        assertTrue(eventPublished);
    }

    @Test
    public void testRevokeAllProjectPermissionShouldNotFireProjectPermissionModifiedEventIfEffectivePermissionDoesnotChange() throws Exception {
        when(projectPermissionDao.getHighestDefaultPermission(TEST_PROJECT.getId()))
                .thenReturn(Permission.PROJECT_READ);

        permissionAdminService.grantAllProjectPermission(Permission.PROJECT_READ, TEST_PROJECT);
        permissionAdminService.grantAllProjectPermission(Permission.PROJECT_WRITE, TEST_PROJECT);
        eventsPublished.clear();
        permissionAdminService.revokeAllProjectPermission(Permission.PROJECT_READ, TEST_PROJECT);

        boolean eventPublished = false;
        for (Object event : eventsPublished) {
            if (event instanceof ProjectPermissionRevokedEvent || event instanceof ProjectPermissionRevocationRequestedEvent) {
                eventPublished = true;
                break;
            }
        }

        assertFalse(eventPublished);
    }

    @Test(expected = IntegrityException.class)
    public void testRevokeAllProjectPermissionOnPersonalProject() throws Exception {
        permissionAdminService.revokeAllProjectPermission(PROJECT_READ, createPersonalProject());
    }

    @Test(expected = IllegalArgumentException.class)
    public void testRevokeAllProjectViewPermission() throws Exception {
        permissionAdminService.revokeAllProjectPermission(PROJECT_VIEW, createPersonalProject());
    }

    @Test
    public void testRevokeAllUserPermissionsByUsername() throws Exception {
        permissionAdminService.revokeAllUserPermissions("nonexistent");
        verify(globalPermissionDao, never()).revokeAll(anyInt());
        verify(projectPermissionDao, never()).revokeAll(anyInt());
        verify(repositoryPermissionDao, never()).revokeAll(anyInt());

        when(userService.getUserByName(TEST_USER.getName(), true)).thenReturn(TEST_USER);
        permissionAdminService.revokeAllUserPermissions(TEST_USER.getName());
        verify(globalPermissionDao).revokeAll(TEST_USER.getId());
        verify(projectPermissionDao).revokeAll(TEST_USER.getId());
        verify(repositoryPermissionDao).revokeAll(TEST_USER.getId());
    }

    @Test
    public void testRevokeAllUserPermissions() throws Exception {
        permissionAdminService.revokeAllUserPermissions(TEST_USER);
        verify(globalPermissionDao).revokeAll(TEST_USER.getId());
        verify(projectPermissionDao).revokeAll(TEST_USER.getId());
        verify(repositoryPermissionDao).revokeAll(TEST_USER.getId());
    }

    @Test
    public void testRevokeAllGroupPermissions() throws Exception {
        permissionAdminService.revokeAllGroupPermissions("testrevoke");
        verify(globalPermissionDao).revokeAll("testrevoke");
        verify(projectPermissionDao).revokeAll("testrevoke");
        verify(repositoryPermissionDao).revokeAll("testrevoke");
    }

    @Test
    public void testRevokingAllGlobalPermissionsForUser() throws Exception {
        when(permissionService.hasGlobalPermission(ADMIN)).thenReturn(true);

        permissionAdminService.setPermission(new SetPermissionRequest.Builder()
                .globalPermission(PROJECT_CREATE)
                .user(TEST_USER2)
                .build());

        assertTrue("Permission not stored", globalPermissions.size() == 1);

        permissionAdminService.revokeAllGlobalPermissions(TEST_USER2);
        assertTrue("Permission should have been removed stored", globalPermissions.size() == 0);
    }

    @Test
    public void testRevokeProjectPermissionDoesNotRemoveGlobalPermission() throws Exception {
        globalPermissions.add(new InternalGlobalPermission.Builder().permission(SYS_ADMIN).user(TEST_USER2).build());
        projectPermissions.add(new InternalProjectPermission.Builder().permission(PROJECT_ADMIN).project(TEST_PROJECT).user(TEST_USER2).build());
        when(permissionService.hasProjectPermission(TEST_PROJECT, PROJECT_ADMIN)).thenReturn(true);
        permissionAdminService.revokeAllProjectPermissions(TEST_PROJECT, TEST_USER2);
        assertTrue("Permission should not have been removed", globalPermissions.size() == 1);
    }

    @Test
    public void testCanAddUserToGroupWhenSysAdmin() throws Exception {
        when(permissionService.hasGlobalPermission(SYS_ADMIN)).thenReturn(true);

        permissionAdminService.canAddUserToGroup("blah");
    }

    @Test(expected=ForbiddenException.class)
    public void testCannotAddUserToGroupWhenAdmin() throws Exception {
        when(permissionService.hasGlobalPermission(ADMIN)).thenReturn(true);
        when(permissionService.hasGlobalGroupPermission(SYS_ADMIN, "blah")).thenReturn(true);
        permissionAdminService.canAddUserToGroup("blah");
    }

    @Test
    public void testCanAddUserToGroupWhenAdmin() throws Exception {
        when(permissionService.hasGlobalPermission(ADMIN)).thenReturn(true);
        when(permissionService.hasGlobalGroupPermission(SYS_ADMIN, "blah")).thenReturn(true);
        permissionAdminService.canAddUserToGroup("notblah");
    }

    @Test
    public void testCanRemoveFromSysadminGroup1() throws Exception {
        // The group has sys-admin, but user has direct sys-admin so should be ok
        when(userService.isUserInGroup(TEST_USER, "group")).thenReturn(true);
        when(permissionService.hasGlobalGroupPermission(SYS_ADMIN, "group")).thenReturn(true);
        when(permissionService.hasDirectGlobalUserPermission(SYS_ADMIN)).thenReturn(true);
        permissionAdminService.canRemoveUserFromGroup(TEST_USER.getName(), "group");
    }

    @Test
    public void testCanRemoveFromSysadminGroup2() throws Exception {
        // The group has sys-admin, user does not have direct but does have through another group
        when(userService.isUserInGroup(TEST_USER, "group")).thenReturn(true);
        when(permissionService.hasGlobalGroupPermission(SYS_ADMIN, "group")).thenReturn(true);
        when(permissionService.hasDirectGlobalUserPermission(SYS_ADMIN)).thenReturn(false);
        when(permissionService.hasGlobalPermissionThroughGroupMembership(eq(SYS_ADMIN), anySetOf(String.class))).thenReturn(true);
        permissionAdminService.canRemoveUserFromGroup(TEST_USER.getName(), "group");
    }

    @Test(expected = IntegrityException.class)
    public void testCanRemoveFromSysadminGroup3() throws Exception {
        // The group has sys-admin, user does not have sys-admin and does not have through another group
        when(userService.isUserInGroup(TEST_USER, "group")).thenReturn(true);
        when(permissionService.hasGlobalGroupPermission(SYS_ADMIN, "group")).thenReturn(true);
        when(permissionService.hasDirectGlobalUserPermission(SYS_ADMIN)).thenReturn(false);
        when(permissionService.hasGlobalPermissionThroughGroupMembership(eq(SYS_ADMIN), anySetOf(String.class))).thenReturn(false);

        permissionAdminService.canRemoveUserFromGroup(TEST_USER.getName(), "group");
    }

    @Test
    public void testCanRemoveFromSysadminGroup4() throws Exception {
        // The user is not a member of the group, so should be allowed even if call should never happen
        when(userService.isUserInGroup(TEST_USER, "group")).thenReturn(false);
        when(permissionService.hasGlobalGroupPermission(SYS_ADMIN, "group")).thenReturn(true);
        when(permissionService.hasDirectGlobalUserPermission(SYS_ADMIN)).thenReturn(false);
        when(permissionService.hasGlobalPermissionThroughGroupMembership(eq(SYS_ADMIN), anySetOf(String.class))).thenReturn(false);

        permissionAdminService.canRemoveUserFromGroup(TEST_USER.getName(), "group");
    }

    @Test(expected = ForbiddenException.class)
    public void testCanRemoveFromSysAdminGroup5() throws Exception {
        // The user removing someone else from a sys_admin group and they do not have sys admin
        when(permissionService.hasGlobalGroupPermission(SYS_ADMIN, "group")).thenReturn(true);

        permissionAdminService.canRemoveUserFromGroup(TEST_USER2.getName(), "group");
    }

    @Test
    public void testCanRemoveFromSysAdminGroup6() throws Exception {
        // The user removing someone else from a sys_admin group and they do have sys admin
        when(permissionService.hasGlobalGroupPermission(SYS_ADMIN, "group")).thenReturn(true);
        when(permissionService.hasGlobalPermission(SYS_ADMIN)).thenReturn(true);

        permissionAdminService.canRemoveUserFromGroup(TEST_USER2.getName(), "group");
    }

    @Test
    public void testCanRemoveFromOrdinaryGroup() throws Exception {
        // The group has neither admin, nor sys-admin so there should be no problems
        when(userService.isUserInGroup(TEST_USER, "group")).thenReturn(true);
        when(permissionService.hasGlobalGroupPermission(ADMIN, "group")).thenReturn(false);
        when(permissionService.hasGlobalGroupPermission(SYS_ADMIN, "group")).thenReturn(false);
        permissionAdminService.canRemoveUserFromGroup(TEST_USER.getName(), "group");
    }

    @Test
    public void testCanRemoveFromAdminGroup1() throws Exception {
        // The group has admin, but user has sys-admin so ok
        when(userService.isUserInGroup(TEST_USER, "group")).thenReturn(true);
        when(permissionService.hasGlobalGroupPermission(ADMIN, "group")).thenReturn(true);
        when(permissionService.hasGlobalPermission(SYS_ADMIN)).thenReturn(true);
        permissionAdminService.canRemoveUserFromGroup(TEST_USER.getName(), "group");
    }

    @Test
    public void testCanRemoveFromAdminGroup2() throws Exception {
        // The group has admin, user does not have sys-admin but does have admin directly so ok
        when(userService.isUserInGroup(TEST_USER, "group")).thenReturn(true);
        when(permissionService.hasGlobalGroupPermission(ADMIN, "group")).thenReturn(true);
        when(permissionService.hasGlobalPermission(SYS_ADMIN)).thenReturn(false);
        when(permissionService.hasDirectGlobalUserPermission(ADMIN)).thenReturn(true);
        permissionAdminService.canRemoveUserFromGroup(TEST_USER.getName(), "group");
    }

    @Test
    public void testCanRemoveFromAdminGroup3() throws Exception {
        // The group has admin, user does not have sys-admin does not have admin directly
        // but is a member of another group with admin so ok
        when(userService.isUserInGroup(TEST_USER, "group")).thenReturn(true);
        when(permissionService.hasGlobalGroupPermission(ADMIN, "group")).thenReturn(true);
        when(permissionService.hasGlobalPermission(SYS_ADMIN)).thenReturn(false);
        when(permissionService.hasDirectGlobalUserPermission(ADMIN)).thenReturn(false);
        when(permissionService.hasGlobalPermissionThroughGroupMembership(eq(ADMIN), anySetOf(String.class))).thenReturn(true);
        permissionAdminService.canRemoveUserFromGroup(TEST_USER.getName(), "group");
    }

    @Test(expected = IntegrityException.class)
    public void testCanRemoveFromAdminGroup4() throws Exception {
        // The group has admin, user does not have sys-admin does not have admin directly
        // is not a member of another group with admin so should throw
        when(userService.isUserInGroup(TEST_USER, "group")).thenReturn(true);
        when(permissionService.hasGlobalGroupPermission(ADMIN, "group")).thenReturn(true);
        when(permissionService.hasGlobalPermission(SYS_ADMIN)).thenReturn(false);
        when(permissionService.hasDirectGlobalUserPermission(ADMIN)).thenReturn(false);
        when(permissionService.hasGlobalPermissionThroughGroupMembership(eq(ADMIN), anySetOf(String.class))).thenReturn(false);
        permissionAdminService.canRemoveUserFromGroup(TEST_USER.getName(), "group");
    }

    @Test(expected = IntegrityException.class)
    public void testDowngradingSysadminGroupPermissionAsAdmin() {
        String testGroup = "TestGroup";

        when(permissionService.hasGlobalPermission(ADMIN)).thenReturn(true);
        when(permissionService.hasGlobalPermission(SYS_ADMIN)).thenReturn(false);         // Current authenticated user
        when(permissionService.hasGlobalPermission(eq(testGroup), eq(SYS_ADMIN))).thenReturn(true);     // Target sysadmin group

        permissionAdminService.setPermission(new SetPermissionRequest.Builder()
                .globalPermission(LICENSED_USER)
                .group(testGroup)
                .build());
    }

    @Test(expected = IntegrityException.class)
    public void testDowngradingSysadminUserPermissionAsAdmin() {
        when(permissionService.hasGlobalPermission(ADMIN)).thenReturn(true);
        when(permissionService.hasGlobalPermission(SYS_ADMIN)).thenReturn(false);         // Current authenticated user
        when(permissionService.hasGlobalPermission(eq(TEST_USER2), eq(SYS_ADMIN))).thenReturn(true);    // Target sysadmin user

        permissionAdminService.setPermission(new SetPermissionRequest.Builder()
                .globalPermission(LICENSED_USER)
                .user(TEST_USER2)
                .build());
    }

    @Test(expected = IntegrityException.class)
    public void testRevokingSysadminGroupPermissionAsAdmin() {
        String testGroup = "TestGroup";
        when(permissionService.hasGlobalPermission(SYS_ADMIN)).thenReturn(false);         // Current authenticated user
        when(permissionService.hasGlobalPermission(eq(testGroup), eq(SYS_ADMIN))).thenReturn(true);     // Target sysadmin group
        permissionAdminService.revokeAllGlobalPermissions(testGroup);
    }

    @Test(expected = IntegrityException.class)
    public void testRevokingSysadminUserPermissionAsAdmin() {
        when(permissionService.hasGlobalPermission(SYS_ADMIN)).thenReturn(false);         // Current authenticated user
        when(permissionService.hasGlobalPermission(eq(TEST_USER2), eq(SYS_ADMIN))).thenReturn(true);    // Target sysadmin user
        permissionAdminService.revokeAllGlobalPermissions(TEST_USER2);
    }


    @Test
    public void testRevokeGlobalUserPermissionSysAdmin1() throws Exception {
        // The user is not revoking their own privilege so ok
        when(authenticationContext.getCurrentUser()).thenReturn(TEST_USER2);

        permissionAdminService.revokeAllGlobalPermissions(TEST_USER);
    }

    @Test
    public void testRevokeGlobalUserPermissionSysAdmin2() throws Exception {
        // The user is working on their account but they have sys_admin privilege from another group.
        when(permissionService.hasGlobalPermissionThroughGroupMembership(any(Permission.class), anySetOf(String.class))).thenReturn(true);

        permissionAdminService.revokeAllGlobalPermissions(TEST_USER);
    }

    @Test(expected = IntegrityException.class)
    public void testRevokeGlobalUserPermissionSysAdmin3() throws Exception {
        // The user is removing their own sys_admin permission and do not have it through another group, which should fail
        globalPermissions.add(new InternalGlobalPermission.Builder().permission(SYS_ADMIN).user(TEST_USER).build());
        when(permissionService.hasGlobalPermissionThroughGroupMembership(eq(SYS_ADMIN), anySetOf(String.class))).thenReturn(false);

        permissionAdminService.revokeAllGlobalPermissions(TEST_USER);
    }

    @Test
    public void testRevokeGlobalGroupPermissionSysAdmin1() throws Exception {
        // The user is not a member of the group being revoked
        when(userService.isUserInGroup(TEST_USER, "group")).thenReturn(false);

        permissionAdminService.revokeAllGlobalPermissions("group");
    }

    @Test
    public void testRevokeGlobalGroupPermissionSysAdmin2() throws Exception {
        // The user is a member of the group being revoked but have their own permission
        when(userService.isUserInGroup(TEST_USER, "group")).thenReturn(true);
        when(permissionService.hasDirectGlobalUserPermission(isA(Permission.class))).thenReturn(true);

        permissionAdminService.revokeAllGlobalPermissions("group");
    }

    @Test
    public void testRevokeGlobalGroupPermissionSysAdmin3() throws Exception {
        // The user is a member of the group being revoked, do no have their own permission
        // but they have membership through another group
        when(userService.isUserInGroup(TEST_USER, "group")).thenReturn(true);
        when(permissionService.hasDirectGlobalUserPermission(isA(Permission.class))).thenReturn(false);
        when(permissionService.hasGlobalPermissionThroughGroupMembership(isA(Permission.class), anySetOf(String.class))).thenReturn(true);

        permissionAdminService.revokeAllGlobalPermissions("group");
    }

    @Test(expected = IntegrityException.class)
    public void testRevokeGlobalGroupPermissionSysAdmin4() throws Exception {
        // The user is a member of the group being revoked and does not have his own permission
        // or privilege through another group, so should not be allowed
        globalPermissions.add(new InternalGlobalPermission.Builder().permission(SYS_ADMIN).group("group").build());
        when(userService.isUserInGroup(TEST_USER, "group")).thenReturn(true);
        when(permissionService.hasDirectGlobalUserPermission(SYS_ADMIN)).thenReturn(false);
        when(permissionService.hasGlobalPermissionThroughGroupMembership(eq(SYS_ADMIN), anySetOf(String.class))).thenReturn(false);

        permissionAdminService.revokeAllGlobalPermissions("group");
    }

    @Test
    public void testRevokeGlobalGroupPermissionAdmin() throws Exception {
        // The user is a member of the group being revoked but is also a member of another group that has SYS_ADMIN
        globalPermissions.add(new InternalGlobalPermission.Builder().permission(ADMIN).group("group").build());
        when(userService.isUserInGroup(TEST_USER, "group")).thenReturn(true);
        when(permissionService.hasGlobalPermission(SYS_ADMIN)).thenReturn(true);        // through another group

        permissionAdminService.revokeAllGlobalPermissions("group");
    }

    @Test
    public void testRevokeGlobalGroupPermissionProjectCreator() throws Exception {
        // The user is a member of the group being revoked but has it's own ADMIN privilege
        globalPermissions.add(new InternalGlobalPermission.Builder().permission(ADMIN).user(TEST_USER).build());
        when(permissionService.hasGlobalPermission(SYS_ADMIN)).thenReturn(true);           // through another group

        permissionAdminService.revokeAllGlobalPermissions(TEST_USER);
    }

    @Test
    public void testRevokeAsAnonymous() throws Exception {
        when(authenticationContext.getCurrentUser()).thenReturn(null);
        permissionAdminService.revokeAllGlobalPermissions("group");
    }

    @Test(expected = IntegrityException.class)
    public void testCanDeleteGroup() throws Exception {
        // user is a member of a sys-admin group and doesn't have privileges any other way
        when(userService.isUserInGroup(TEST_USER, "group")).thenReturn(true);
        when(permissionService.hasDirectGlobalUserPermission(SYS_ADMIN)).thenReturn(false);
        when(permissionService.hasGlobalGroupPermission(SYS_ADMIN, "group")).thenReturn(true);
        when(permissionService.hasGlobalPermissionThroughGroupMembership(eq(SYS_ADMIN), anySetOf(String.class))).thenReturn(false);
        permissionAdminService.canDeleteGroup("group");
    }

    @Test
    public void testCanDeleteGroup1() throws Exception {
        // user is a member of a sys-admin group but does have privileges from another group
        when(userService.isUserInGroup(TEST_USER, "group")).thenReturn(true);
        when(permissionService.hasDirectGlobalUserPermission(SYS_ADMIN)).thenReturn(false);
        when(permissionService.hasGlobalGroupPermission(SYS_ADMIN, "group")).thenReturn(true);
        when(permissionService.hasGlobalPermissionThroughGroupMembership(eq(SYS_ADMIN), anySetOf(String.class))).thenReturn(true);
        permissionAdminService.canDeleteGroup("group");
    }

    @Test(expected = ForbiddenException.class)
    public void testCanDeleteSysAdminGroup() throws Exception {
        // User is not a member of the group, User has admin privilege but not sys-admin privilege
        // group has sys-admin privilege
        when(userService.isUserInGroup(TEST_USER, "group")).thenReturn(false);
        when(permissionService.hasDirectGlobalUserPermission(ADMIN)).thenReturn(true);
        when(permissionService.hasDirectGlobalUserPermission(SYS_ADMIN)).thenReturn(false);
        when(permissionService.hasGlobalGroupPermission(SYS_ADMIN, "group")).thenReturn(true);
        permissionAdminService.canDeleteGroup("group");
    }

    @Test
    public void testCanDeleteAdminGroup() throws Exception {
        // User is not a member of the group, User has admin privilege but not sys-admin privilege
        // group has admin privilege
        when(userService.isUserInGroup(TEST_USER, "group")).thenReturn(false);
        when(permissionService.hasDirectGlobalUserPermission(ADMIN)).thenReturn(true);
        when(permissionService.hasDirectGlobalUserPermission(SYS_ADMIN)).thenReturn(false);
        when(permissionService.hasGlobalGroupPermission(SYS_ADMIN, "group")).thenReturn(false);
        when(permissionService.hasGlobalGroupPermission(ADMIN, "group")).thenReturn(true);
        permissionAdminService.canDeleteGroup("group");
    }

    @Test(expected = IntegrityException.class)
    public void testDeleteUserMyself() throws Exception {
        when(userService.getUserByName("tester")).thenReturn(TEST_USER);
        permissionAdminService.canDeleteUser("tester");
    }

    @Test(expected = ForbiddenException.class)
    public void testDeleteSysAdminUser() throws Exception {
        when(userService.getUserByName("tester2")).thenReturn(TEST_USER2);
        when(permissionService.hasGlobalPermission(TEST_USER2, SYS_ADMIN)).thenReturn(true);
        when(permissionService.hasGlobalPermission(SYS_ADMIN)).thenReturn(false);

        permissionAdminService.canDeleteUser("tester2");
    }

    @Test
    public void testDeleteNonSysAdminUser() throws Exception {
        when(userService.getUserByName("tester2")).thenReturn(TEST_USER2);
        when(permissionService.hasGlobalPermission(TEST_USER2, ADMIN)).thenReturn(true);
        when(permissionService.hasGlobalPermission(ADMIN)).thenReturn(true);

        permissionAdminService.canDeleteUser("tester2");
    }

    @Test
    public void testDeleteNoLocalUser() throws Exception {
        permissionAdminService.canDeleteUser("unknown");
    }

    @Test(expected = LicenseLimitException.class)
    public void testGrantSysAdminExceedsLicenseLimit() throws Exception {
        testGrantPermissionExceedsLicenseLimit(SYS_ADMIN);
    }

    @Test(expected = LicenseLimitException.class)
    public void testGrantAdminExceedsLicenseLimit() throws Exception {
        testGrantPermissionExceedsLicenseLimit(ADMIN);
    }

    @Test(expected = LicenseLimitException.class)
    public void testGrantProjectCreateExceedsLicenseLimit() throws Exception {
        testGrantPermissionExceedsLicenseLimit(PROJECT_CREATE);
    }

    @Test(expected = LicenseLimitException.class)
    public void testGrantLicensedUserExceedsLicenseLimit() throws Exception {
        testGrantPermissionExceedsLicenseLimit(LICENSED_USER);
    }

    private void testGrantPermissionExceedsLicenseLimit(Permission permission) {
        if (permission == SYS_ADMIN) {
            when(permissionService.hasGlobalPermission(SYS_ADMIN)).thenReturn(true);
        } else {
            when(permissionService.hasGlobalPermission(ADMIN)).thenReturn(true);
        }

        doThrow(new LicenseLimitException(Mockito.mock(KeyedMessage.class))).
                when(licenseService).validateCanLicenseUser((ApplicationUser) any(), (Permission) any());

        permissionAdminService.setPermission(new SetPermissionRequest.Builder()
                .globalPermission(permission)
                .user(TEST_USER2)
                .build());
    }

    @Test(expected = LicenseLimitException.class)
    public void testGrantSysAdminGroupExceedsLicenseLimit() throws Exception {
        testGrantPermissionGroupExceedsLicenseLimit(SYS_ADMIN);
    }

    @Test(expected = LicenseLimitException.class)
    public void testGrantAdminGroupExceedsLicenseLimit() throws Exception {
        testGrantPermissionGroupExceedsLicenseLimit(ADMIN);
    }

    @Test(expected = LicenseLimitException.class)
    public void testGrantProjectCreateGroupExceedsLicenseLimit() throws Exception {
        testGrantPermissionGroupExceedsLicenseLimit(PROJECT_CREATE);
    }

    @Test(expected = LicenseLimitException.class)
    public void testGrantLicensedUserGroupExceedsLicenseLimit() throws Exception {
        testGrantPermissionGroupExceedsLicenseLimit(LICENSED_USER);
    }

    @Test
    public void testWithoutGroups() {
        List<String> groups = ImmutableList.of("AbC", "def", "deG", "xyz");

        Predicate<String> predicate = DefaultPermissionAdminService.withoutGroups(new ListPageProvider<>(groups));
        assertFalse(predicate.test("ABC"));
        assertFalse(predicate.test("abc"));
        assertTrue(predicate.test("gh"));
        assertFalse(predicate.test("XYZ"));
        assertTrue(predicate.test("zzz"));
    }

    @Test
    public void testWithoutUsers() {
        List<ApplicationUser> users = ImmutableList.<ApplicationUser>of(
                new TestApplicationUser("AbC"),
                new TestApplicationUser("def"),
                new TestApplicationUser("deG"),
                new TestApplicationUser("xyz")
        );

        Predicate<ApplicationUser> predicate = DefaultPermissionAdminService.withoutUsers(new ListPageProvider<>(users));
        assertFalse(predicate.test(new TestApplicationUser("ABC")));
        assertFalse(predicate.test(new TestApplicationUser("abc")));
        assertTrue(predicate.test(new TestApplicationUser("gh")));
        assertFalse(predicate.test(new TestApplicationUser("XYZ")));
        assertTrue(predicate.test(new TestApplicationUser("zzz")));
    }

    private void testGrantPermissionGroupExceedsLicenseLimit(Permission permission) {
        if (permission == SYS_ADMIN) {
            when(permissionService.hasGlobalPermission(SYS_ADMIN)).thenReturn(true);
        } else {
            when(permissionService.hasGlobalPermission(ADMIN)).thenReturn(true);
        }

        doThrow(new LicenseLimitException(Mockito.mock(KeyedMessage.class))).
                when(licenseService).validateCanLicenseGroup(anyString(), (Permission) any());

        permissionAdminService.setPermission(new SetPermissionRequest.Builder()
                .globalPermission(permission)
                .group("group")
                .build());
    }

    private Project createPersonalProject() {
        InternalNormalUser owner = mock(InternalNormalUser.class);
        when(owner.getName()).thenReturn("admin");

        return new InternalPersonalProject.Builder().id(42).owner(owner).build();
    }
}
