package com.atlassian.stash.internal.user;

import com.atlassian.bitbucket.annotation.Dao;
import com.atlassian.bitbucket.annotation.DataSet;
import com.atlassian.stash.internal.DataSets;
import com.atlassian.stash.internal.project.HibernateProjectDao;
import com.atlassian.stash.internal.project.ProjectDao;
import com.atlassian.stash.internal.repository.HibernateRepositoryDao;
import com.atlassian.stash.internal.repository.RepositoryDao;
import com.atlassian.bitbucket.project.Project;
import com.atlassian.bitbucket.repository.Repository;
import com.atlassian.bitbucket.permission.Permission;
import com.atlassian.bitbucket.util.Page;
import com.atlassian.bitbucket.util.PageRequest;
import com.atlassian.bitbucket.util.PageUtils;
import com.google.common.collect.HashBasedTable;
import com.google.common.collect.Lists;
import com.google.common.collect.Table;
import org.junit.Test;

import java.util.List;

@DataSet({DataSets.USERS,
          DataSets.PROJECTS,
          DataSets.REPOSITORIES,
          DataSets.GLOBAL_PERMISSIONS,
          DataSets.PROJECT_PERMISSIONS,
          DataSets.REPOSITORY_PERMISSIONS})
public class HibernateEffectivePermissionDaoTest extends AbstractPermissionDaoTest {

    @Dao
    private HibernateEffectivePermissionDao dao;
    @Dao(HibernateProjectDao.class)
    private ProjectDao projectDao;
    @Dao(HibernateRepositoryDao.class)
    private RepositoryDao repositoryDao;

    private Project project1, project2, project3, project4;
    private Repository repository1, repository2, repository3, repository4, repository5;

    @Test
    public void testFindByGroupNoPermissions() {
        PageRequest pageRequest = PageUtils.newRequest(0, 10);
        Page<InternalGrantedPermission> page = dao.findByGroup("unknown", pageRequest);

        assertNotNull(page);
        assertTrue(page.getIsLastPage());
        assertEquals(0, page.getSize());
    }

    @SuppressWarnings("ConstantConditions")
    @Test
    public void testFindByGroup() {
        for (String group : new String[] {"stash-admins", "STASH-admins"}) { // verify case insentitivity
            List<InternalGrantedPermission> expectedPage1 = Lists.newArrayList(
                    dao.getById(19L), // LICENSED_USER for 'stash-admins'
                    dao.getById(20L)  // ADMIN for 'stash-admins'
            );

            List<InternalGrantedPermission> expectedPage2 = Lists.newArrayList(
                    dao.getById(21L) // SYS_ADMIN for 'stash-admins'
            );

            // fetch the first page
            PageRequest pageRequest = PageUtils.newRequest(0, 2);
            Page<InternalGrantedPermission> page = dao.findByGroup(group, pageRequest);

            // verify page 1
            assertNotNull(page);
            assertFalse(page.getIsLastPage());
            assertEquals(expectedPage1, Lists.newArrayList(page.getValues()));

            // fetch page 2
            page = dao.findByGroup(group, page.getNextPageRequest());

            // verify page 2
            assertNotNull(page);
            assertTrue(page.getIsLastPage());
            assertEquals(expectedPage2, Lists.newArrayList(page.getValues()));
        }
    }

    @SuppressWarnings("ConstantConditions")
    @Test
    public void testFindByUserId() {
        List<InternalGrantedPermission> expectedPage1 = Lists.newArrayList(
                dao.getById(6L) // PROJECT_WRITE on project 3
        );

        List<InternalGrantedPermission> expectedPage2 = Lists.newArrayList(
                dao.getById(26L) // REPO_ADMIN on repo 1
        );

        // fetch the first page
        PageRequest pageRequest = PageUtils.newRequest(0, 1);
        Page<InternalGrantedPermission> page = dao.findByUserId(6, pageRequest);

        // verify page 1
        assertNotNull(page);
        assertFalse(page.getIsLastPage());
        assertEquals(1, page.getSize());
        assertEquals(expectedPage1, Lists.newArrayList(page.getValues()));

        // fetch page 2
        page = dao.findByUserId(6, page.getNextPageRequest());

        // verify page 2
        assertNotNull(page);
        assertTrue(page.getIsLastPage());
        assertEquals(1, page.getSize());
        assertEquals(expectedPage2, Lists.newArrayList(page.getValues()));
    }

    @Test
    public void testFindByUserIdNoPermissions() {
        PageRequest pageRequest = PageUtils.newRequest(0, 10);
        Page<InternalGrantedPermission> page = dao.findByUserId(99999, pageRequest);

        assertNotNull(page);
        assertTrue(page.getIsLastPage());
        assertEquals(0, page.getSize());
    }

    @Test
    public void testFindGroupsWithPermission() {
        PageRequest pageRequest = PageUtils.newRequest(0, 10);
        assertGroups(dao.findGroups(Permission.SYS_ADMIN, pageRequest), "stash-admins");
        assertGroups(dao.findGroups(Permission.ADMIN, pageRequest), "stash-admins");
        assertGroups(dao.findGroups(Permission.PROJECT_CREATE, pageRequest), "stash-admins");
        assertGroups(dao.findGroups(Permission.LICENSED_USER, pageRequest), "group-with-global-and-resource-perms", "licensed_group", "stash-admins", "stash-users");
    }

    @Test
    public void testFindUsersWithPermission() {
        PageRequest pageRequest = PageUtils.newRequest(0, 10);
        assertUsers(dao.findUsers(Permission.SYS_ADMIN, pageRequest, ALWAYS_TRUE), 1);
        assertUsers(dao.findUsers(Permission.ADMIN, pageRequest, ALWAYS_TRUE), 2, 1);
        assertUsers(dao.findUsers(Permission.PROJECT_CREATE, pageRequest, ALWAYS_TRUE), 2, 1);
        assertUsers(dao.findUsers(Permission.LICENSED_USER, pageRequest, ALWAYS_TRUE), 2, 1, 9);
    }

    @Test
    public void testIsGrantedForGroupWithoutPermissionOrWithOnlyLicensedPermission() {
        loadProjectAndRepositories();
        Table<Permission, Object, Boolean> tests = createTestTable(false);
        // project 2: default permission: PROJECT_WRITE
        tests.put(Permission.PROJECT_WRITE, project2, true);
        tests.put(Permission.PROJECT_READ, project2, true);
        tests.put(Permission.PROJECT_VIEW, project2, true);
        // project 3: default permission: PROJECT_READ
        tests.put(Permission.PROJECT_READ, project3, true);
        tests.put(Permission.PROJECT_VIEW, project3, true);
        // any project permission
        tests.put(Permission.PROJECT_WRITE, NULL, true);
        tests.put(Permission.PROJECT_READ, NULL, true);
        tests.put(Permission.PROJECT_VIEW, NULL, true);
        // repo 3 and 4: inherits PROJECT_WRITE from project 2
        tests.put(Permission.REPO_WRITE, repository3, true);
        tests.put(Permission.REPO_READ, repository3, true);
        tests.put(Permission.REPO_WRITE, repository4, true);
        tests.put(Permission.REPO_READ, repository4, true);
        // any repository permission
        tests.put(Permission.REPO_WRITE, NULL, true);
        tests.put(Permission.REPO_READ, NULL, true);

        testIsGranted("nogroup", tests);

        tests.put(Permission.LICENSED_USER, NULL, true);
        testIsGranted("licensed_group", tests);
        testIsGranted("stash-users", tests);
    }

    @Test
    public void testIsGrantedForGroupWithGlobalAndProjectPermissions() {
        loadProjectAndRepositories();
        Table<Permission, Object, Boolean> tests = createTestTable(false);
        // global permissions
        tests.put(Permission.LICENSED_USER, NULL, true);
        // project 2: default permission: PROJECT_WRITE
        tests.put(Permission.PROJECT_WRITE, project2, true);
        tests.put(Permission.PROJECT_READ, project2, true);
        tests.put(Permission.PROJECT_VIEW, project2, true);
        // project 3: has PROJECT_READ
        tests.put(Permission.PROJECT_READ, project3, true);
        tests.put(Permission.PROJECT_VIEW, project3, true);
        // any project permission
        tests.put(Permission.PROJECT_WRITE, NULL, true);
        tests.put(Permission.PROJECT_READ, NULL, true);
        tests.put(Permission.PROJECT_VIEW, NULL, true);
        // repo 3: has REPO_READ + inherits PROJECT_WRITE from project 2
        tests.put(Permission.REPO_WRITE, repository3, true);
        tests.put(Permission.REPO_READ, repository3, true);
        // repo 4: inherits PROJECT_WRITE from project 2
        tests.put(Permission.REPO_WRITE, repository4, true);
        tests.put(Permission.REPO_READ, repository4, true);
        // any repository permission
        tests.put(Permission.REPO_WRITE, NULL, true);
        tests.put(Permission.REPO_READ, NULL, true);

        testIsGranted("group-with-global-and-resource-perms", tests);
    }

    @Test
    public void testIsGrantedForGroupWithDeveloperGroup() {
        loadProjectAndRepositories();
        Table<Permission, Object, Boolean> tests = createTestTable(false);
        // project 1: repository permission on repository 2 gives them PROJECT_VIEW
        tests.put(Permission.PROJECT_VIEW, project1, true);
        // project 2: PROJECT_ADMIN + default permission: PROJECT_WRITE
        tests.put(Permission.PROJECT_ADMIN, project2, true);
        tests.put(Permission.PROJECT_WRITE, project2, true);
        tests.put(Permission.PROJECT_READ, project2, true);
        tests.put(Permission.PROJECT_VIEW, project2, true);
        // project 3: default permission: PROJECT_READ
        tests.put(Permission.PROJECT_READ, project3, true);
        tests.put(Permission.PROJECT_VIEW, project3, true);
        // any project permission
        tests.put(Permission.PROJECT_ADMIN, NULL, true);
        tests.put(Permission.PROJECT_WRITE, NULL, true);
        tests.put(Permission.PROJECT_READ, NULL, true);
        tests.put(Permission.PROJECT_VIEW, NULL, true);
        // repo 2: has REPO_ADMIN
        tests.put(Permission.REPO_ADMIN, repository2, true);
        tests.put(Permission.REPO_WRITE, repository2, true);
        tests.put(Permission.REPO_READ, repository2, true);
        // repo 3 and 4: has PROJECT_ADMIN on project 2 + inherits PROJECT_WRITE from same project
        tests.put(Permission.REPO_ADMIN, repository3, true);
        tests.put(Permission.REPO_WRITE, repository3, true);
        tests.put(Permission.REPO_READ, repository3, true);
        tests.put(Permission.REPO_ADMIN, repository4, true);
        tests.put(Permission.REPO_WRITE, repository4, true);
        tests.put(Permission.REPO_READ, repository4, true);
        // any repository permission
        tests.put(Permission.REPO_ADMIN, NULL, true);
        tests.put(Permission.REPO_WRITE, NULL, true);
        tests.put(Permission.REPO_READ, NULL, true);

        testIsGranted("developers", tests);
    }

    @Test
    public void testIsGrantedForAdmins() throws Exception {
        loadProjectAndRepositories();
        Table<Permission, Object, Boolean> tests = createTestTable(true);

        testIsGranted("stash-admins", tests); // stash-admins is a sysadmin group
        testIsGranted("stash-ADMINS", tests); // test should be case insensitive
        testIsGranted(1, tests); // user 1 is a sysadmin

        tests.put(Permission.SYS_ADMIN, NULL, false);
        testIsGranted(2, tests); // user 2 is an admin
    }

    @Test
    public void testIsGrantedForUserWithoutPermissions() {
        loadProjectAndRepositories();
        Table<Permission, Object, Boolean> tests = createTestTable(false);
        // project 2: default permission: PROJECT_WRITE
        tests.put(Permission.PROJECT_WRITE, project2, true);
        tests.put(Permission.PROJECT_READ, project2, true);
        tests.put(Permission.PROJECT_VIEW, project2, true);
        // project 3: default permission: PROJECT_READ
        tests.put(Permission.PROJECT_READ, project3, true);
        tests.put(Permission.PROJECT_VIEW, project3, true);
        // any project permission
        tests.put(Permission.PROJECT_WRITE, NULL, true);
        tests.put(Permission.PROJECT_READ, NULL, true);
        tests.put(Permission.PROJECT_VIEW, NULL, true);
        // repo 3 and 4: inherits PROJECT_WRITE from project 2
        tests.put(Permission.REPO_WRITE, repository3, true);
        tests.put(Permission.REPO_READ, repository3, true);
        tests.put(Permission.REPO_WRITE, repository4, true);
        tests.put(Permission.REPO_READ, repository4, true);
        // any repository permission
        tests.put(Permission.REPO_WRITE, NULL, true);
        tests.put(Permission.REPO_READ, NULL, true);

        // nonexistent user
        testIsGranted(-42, tests);
    }

    @Test // user 3 has only a admin project permission (excluding defaults), no repo permission
    public void testIsGrantedForUser3() {
        loadProjectAndRepositories();
        Table<Permission, Object, Boolean> tests = createTestTable(false);
        // project 1: has PROJECT_ADMIN
        tests.put(Permission.PROJECT_ADMIN, project1, true);
        tests.put(Permission.PROJECT_WRITE, project1, true);
        tests.put(Permission.PROJECT_READ, project1, true);
        tests.put(Permission.PROJECT_VIEW, project1, true);
        // project 2: default permission: PROJECT_WRITE
        tests.put(Permission.PROJECT_WRITE, project2, true);
        tests.put(Permission.PROJECT_READ, project2, true);
        tests.put(Permission.PROJECT_VIEW, project2, true);
        // project 3: default permission: PROJECT_READ
        tests.put(Permission.PROJECT_READ, project3, true);
        tests.put(Permission.PROJECT_VIEW, project3, true);
        // any project permission
        tests.put(Permission.PROJECT_ADMIN, NULL, true);
        tests.put(Permission.PROJECT_WRITE, NULL, true);
        tests.put(Permission.PROJECT_READ, NULL, true);
        tests.put(Permission.PROJECT_VIEW, NULL, true);
        // repo 1 and 2: inherits PROJECT_ADMIN from project 1
        tests.put(Permission.REPO_ADMIN, repository1, true);
        tests.put(Permission.REPO_WRITE, repository1, true);
        tests.put(Permission.REPO_READ, repository1, true);
        tests.put(Permission.REPO_ADMIN, repository2, true);
        tests.put(Permission.REPO_WRITE, repository2, true);
        tests.put(Permission.REPO_READ, repository2, true);
        // repo 3 and 4: inherits PROJECT_WRITE from project 2
        tests.put(Permission.REPO_WRITE, repository3, true);
        tests.put(Permission.REPO_READ, repository3, true);
        tests.put(Permission.REPO_WRITE, repository4, true);
        tests.put(Permission.REPO_READ, repository4, true);
        // any repository permission
        tests.put(Permission.REPO_ADMIN, NULL, true);
        tests.put(Permission.REPO_WRITE, NULL, true);
        tests.put(Permission.REPO_READ, NULL, true);

        testIsGranted(3, tests);
    }

    @Test // user 5 has only a write project permission (excluding defaults), no repo permission
    public void testIsGrantedForUser5() {
        loadProjectAndRepositories();
        Table<Permission, Object, Boolean> tests = createTestTable(false);
        // project 1: has PROJECT_WRITE
        tests.put(Permission.PROJECT_WRITE, project1, true);
        tests.put(Permission.PROJECT_READ, project1, true);
        tests.put(Permission.PROJECT_VIEW, project1, true);
        // project 2: default permission: PROJECT_WRITE
        tests.put(Permission.PROJECT_WRITE, project2, true);
        tests.put(Permission.PROJECT_READ, project2, true);
        tests.put(Permission.PROJECT_VIEW, project2, true);
        // project 3: default permission: PROJECT_READ
        tests.put(Permission.PROJECT_READ, project3, true);
        tests.put(Permission.PROJECT_VIEW, project3, true);
        // any project permission
        tests.put(Permission.PROJECT_WRITE, NULL, true);
        tests.put(Permission.PROJECT_READ, NULL, true);
        tests.put(Permission.PROJECT_VIEW, NULL, true);
        // repo 1 and 2: inherits PROJECT_WRITE from project 1
        tests.put(Permission.REPO_WRITE, repository1, true);
        tests.put(Permission.REPO_READ, repository1, true);
        tests.put(Permission.REPO_WRITE, repository2, true);
        tests.put(Permission.REPO_READ, repository2, true);
        // repo 3 and 4: inherits PROJECT_WRITE from project 2
        tests.put(Permission.REPO_WRITE, repository3, true);
        tests.put(Permission.REPO_READ, repository3, true);
        tests.put(Permission.REPO_WRITE, repository4, true);
        tests.put(Permission.REPO_READ, repository4, true);
        // any repository permission
        tests.put(Permission.REPO_WRITE, NULL, true);
        tests.put(Permission.REPO_READ, NULL, true);

        testIsGranted(5, tests);
    }

    @Test // user 7 has only one read project permission (excluding defaults), 'overridden' by one write repo permission
    public void testIsGrantedForUser7() {
        loadProjectAndRepositories();
        Table<Permission, Object, Boolean> tests = createTestTable(false);
        // project 1: has PROJECT_READ
        tests.put(Permission.PROJECT_READ, project1, true);
        tests.put(Permission.PROJECT_VIEW, project1, true);
        // project 2: default permission: PROJECT_WRITE
        tests.put(Permission.PROJECT_WRITE, project2, true);
        tests.put(Permission.PROJECT_READ, project2, true);
        tests.put(Permission.PROJECT_VIEW, project2, true);
        // project 3: default permission: PROJECT_READ
        tests.put(Permission.PROJECT_READ, project3, true);
        tests.put(Permission.PROJECT_VIEW, project3, true);
        // any project permission
        tests.put(Permission.PROJECT_WRITE, NULL, true);
        tests.put(Permission.PROJECT_READ, NULL, true);
        tests.put(Permission.PROJECT_VIEW, NULL, true);
        // repo 1: has REPO_WRITE + inherits PROJECT_READ from project 1
        tests.put(Permission.REPO_WRITE, repository1, true);
        tests.put(Permission.REPO_READ, repository1, true);
        // repo 2: inherits PROJECT_READ from project 1
        tests.put(Permission.REPO_READ, repository2, true);
        // repo 3 and 4: inherits PROJECT_WRITE from project 2
        tests.put(Permission.REPO_WRITE, repository3, true);
        tests.put(Permission.REPO_READ, repository3, true);
        tests.put(Permission.REPO_WRITE, repository4, true);
        tests.put(Permission.REPO_READ, repository4, true);
        // any repository permission
        tests.put(Permission.REPO_WRITE, NULL, true);
        tests.put(Permission.REPO_READ, NULL, true);

        testIsGranted(7, tests);
    }

    @Test // user 11 has only no project permission (excluding defaults), but one read repo permission
    public void testIsGrantedForUser11() {
        loadProjectAndRepositories();
        Table<Permission, Object, Boolean> tests = createTestTable(false);
        // project 2: default permission: PROJECT_WRITE
        tests.put(Permission.PROJECT_WRITE, project2, true);
        tests.put(Permission.PROJECT_READ, project2, true);
        tests.put(Permission.PROJECT_VIEW, project2, true);
        // project 3: default permission: PROJECT_READ
        tests.put(Permission.PROJECT_READ, project3, true);
        tests.put(Permission.PROJECT_VIEW, project3, true);
        // project 4: repository permission on repository 5 gives them PROJECT_VIEW
        tests.put(Permission.PROJECT_VIEW, project4, true);
        // any project permission
        tests.put(Permission.PROJECT_WRITE, NULL, true);
        tests.put(Permission.PROJECT_READ, NULL, true);
        tests.put(Permission.PROJECT_VIEW, NULL, true);
        // repo 3: has REPO_WRITE and inherits PROJECT_WRITE from project 2
        tests.put(Permission.REPO_WRITE, repository3, true);
        tests.put(Permission.REPO_READ, repository3, true);
        // repo 4: inherits PROJECT_WRITE from project 2
        tests.put(Permission.REPO_WRITE, repository4, true);
        tests.put(Permission.REPO_READ, repository4, true);
        // repo 5: direct REPO_WRITE
        tests.put(Permission.REPO_WRITE, repository5, true);
        tests.put(Permission.REPO_READ, repository5, true);
        // any repository permission
        tests.put(Permission.REPO_WRITE, NULL, true);
        tests.put(Permission.REPO_READ, NULL, true);

        testIsGranted(11, tests);
    }

    private void loadProjectAndRepositories() {
        project1 = projectDao.getById(1);
        project2 = projectDao.getById(2);
        project3 = projectDao.getById(3);
        project4 = projectDao.getById(4);
        repository1 = repositoryDao.getById(1);
        repository2 = repositoryDao.getById(2);
        repository3 = repositoryDao.getById(3);
        repository4 = repositoryDao.getById(4);
        repository5 = repositoryDao.getById(5);
    }

    private Table<Permission, Object, Boolean> createTestTable(boolean defaultValue) {
        Table<Permission, Object, Boolean> tests = HashBasedTable.create();
        for (Permission permission : Permission.values()) {
            if (permission.isResource(Project.class)) {
                for (Project project : new Project[]{project1, project2, project3, project4}) {
                    tests.put(permission, project, defaultValue);
                }
            } else if (permission.isResource(Repository.class)) {
                for (Repository repository : new Repository[]{repository1, repository2, repository3, repository4, repository5}) {
                    tests.put(permission, repository, defaultValue);
                }
            }
            tests.put(permission, NULL, defaultValue);
        }
        return tests;
    }

    private void testIsGranted(int userId, Table<Permission, Object, Boolean> tests) {
        assertEquals("there are missing combinations:", 42, tests.size()); // 4 global perms + 4 project perms x (4 projects + 1 null) + 3 repo perms x (5 repositories + 1 null)
        for (Table.Cell<Permission, Object, Boolean> test : tests.cellSet()) {
            Permission permission = test.getRowKey();
            Object resource = test.getColumnKey() == NULL ? null : test.getColumnKey();
            boolean isGranted = test.getValue();

            UserPermissionCriteria criteria = new UserPermissionCriteria.Builder(userId).permission(permission).resource(resource).build();
            assertEquals(String.format("for user: %d, permission: %s and resource: %s", userId, permission, resource),
                    isGranted, dao.isGrantedToUser(criteria));
        }
    }

    private void testIsGranted(String group, Table<Permission, Object, Boolean> tests) {
        assertEquals("there are missing combinations:", 42, tests.size());
        for (Table.Cell<Permission, Object, Boolean> test : tests.cellSet()) {
            Permission permission = test.getRowKey();
            Object resource = test.getColumnKey() == NULL ? null : test.getColumnKey();
            boolean isGranted = test.getValue();

            GroupPermissionCriteria criteria = new GroupPermissionCriteria.Builder(group).permission(permission).resource(resource).build();
            assertEquals(String.format("for group: %s, permission: %s and resource: %s", group, permission, resource),
                    isGranted, dao.isGrantedToGroup(criteria));
        }
    }

    // guava's tables don't accept null
    private static Object NULL = new Object();

}
