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

import com.atlassian.activeobjects.test.TestActiveObjects;
import com.atlassian.bitbucket.ao.AbstractAoDaoTest;
import com.atlassian.bitbucket.repository.ref.restriction.RefRestrictionType;
import com.atlassian.bitbucket.util.Chainable;
import com.atlassian.bitbucket.util.Page;
import com.atlassian.bitbucket.util.PageUtils;
import com.atlassian.stash.internal.repository.ref.restriction.SimpleRef;
import com.atlassian.stash.internal.repository.ref.restriction.provider.BranchMatcher;
import com.atlassian.stash.internal.repository.ref.restriction.provider.BranchRestrictionType;
import com.atlassian.stash.internal.repository.ref.restriction.provider.PatternMatcher;
import com.atlassian.stash.internal.repository.ref.restriction.provider.PatternRestrictionType;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import com.google.common.collect.Sets;
import net.java.ao.test.jdbc.Data;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;

import java.sql.SQLException;
import java.util.Collections;
import java.util.Set;
import java.util.stream.Collectors;

import static com.atlassian.stash.internal.repository.ref.restriction.dao.RefRestrictionDatabaseUpdater.*;
import static org.junit.Assert.*;

@Data(RefRestrictionDatabaseUpdater.class)
public class AoRefRestrictionDaoTest extends AbstractAoDaoTest {

    private AoRefRestrictionDao dao;

    public AoRefRestrictionDaoTest() {
        super(AoAccessGrant.class, AoRefRestriction.class);
    }

    @Before
    public void setUp() {
        dao = new AoRefRestrictionDao(new TestActiveObjects(entityManager));
    }

    @Test
    public void testCount() {
        assertEquals(count(1, RefRestrictionDatabaseUpdater.TEST_RESTRICTIONS.values()), dao.count(1));
        assertEquals(count(2, RefRestrictionDatabaseUpdater.TEST_RESTRICTIONS.values()), dao.count(2));
        assertEquals(0, dao.count(12345));
    }

    @Test
    public void testCreate() {
        String patternString = "refs/heads/release/**";
        AoRefRestriction restriction = dao.create(1, RefRestrictionType.NO_DELETES, pattern(patternString),
                Lists.newArrayList(), Lists.newArrayList());

        assertNotNull(restriction.getId());
        assertEquals((Integer) 1, restriction.getRepositoryId());
        assertEquals(PatternRestrictionType.getInstance().getId(), restriction.getMatcherType());
        assertEquals(patternString, restriction.getMatcherId());
        assertEquals(0, restriction.getAccessGrants().length);

        AoRefRestriction fetched = dao.getById(restriction.getId());
        assertNotNull(fetched);
        assertEquals((Integer) 1, fetched.getRepositoryId());
        assertEquals(PatternRestrictionType.getInstance().getId(), fetched.getMatcherType());
        assertEquals(patternString, fetched.getMatcherId());
        assertEquals(0, restriction.getAccessGrants().length);
    }

    @Test
    public void testCreateWithGrants() {
        String patternString = "refs/heads/release/**";
        AoRefRestriction restriction = dao.create(1, RefRestrictionType.READ_ONLY, pattern(patternString),
                Lists.newArrayList("group1", "group2"), Lists.newArrayList(7, 8, 11));

        assertNotNull(restriction.getId());
        assertEquals((Integer) 1, restriction.getRepositoryId());
        assertEquals(PatternRestrictionType.getInstance().getId(), restriction.getMatcherType());
        assertEquals(patternString, restriction.getMatcherId());
        assertEquals(5, restriction.getAccessGrants().length);
        assertContainsGrant(restriction.getAccessGrants(), "group1");
        assertContainsGrant(restriction.getAccessGrants(), "group2");
        assertContainsGrant(restriction.getAccessGrants(), 7);
        assertContainsGrant(restriction.getAccessGrants(), 8);
        assertContainsGrant(restriction.getAccessGrants(), 11);

        AoRefRestriction fetched = dao.getById(restriction.getId());
        assertNotNull(fetched);
        assertEquals((Integer) 1, fetched.getRepositoryId());
        assertEquals(PatternRestrictionType.getInstance().getId(), fetched.getMatcherType());
        assertEquals(patternString, fetched.getMatcherId());
        assertEquals(5, restriction.getAccessGrants().length);
    }

    @Test(expected = IllegalStateException.class)
    public void testCreateExisting() {
        AoRefRestriction restriction = dao.create(1, RefRestrictionType.PULL_REQUEST_ONLY,
                branch(RefRestrictionDatabaseUpdater.BR_1_0), Lists.newArrayList(), Lists.newArrayList());

        assertNotNull(restriction);
        assertEquals((Integer) 2, restriction.getId());
    }

    @Test
    public void testCreateWithGroupAccess() {
        String groupName = "amazing-devs";
        AoRefRestriction restriction = dao.create(1, RefRestrictionType.NO_DELETES, branch("refs/heads/monster"),
                Lists.newArrayList(groupName), Lists.newArrayList());

        AoAccessGrant grant = restriction.getAccessGrants()[0];
        assertNotNull(grant);
        assertNull(grant.getUserId());
        assertEquals(groupName, grant.getGroup());
        assertEquals(restriction, grant.getRestriction());
    }

    @Test
    public void testCreateWithUserAccess() {
        Integer userId = 47;
        AoRefRestriction restriction = dao.create(1, RefRestrictionType.NO_DELETES, branch("refs/heads/monster"),
                Lists.newArrayList(), Lists.newArrayList(userId));

        AoAccessGrant grant = restriction.getAccessGrants()[0];
        assertNotNull(grant);
        assertNull(grant.getGroup());
        assertEquals(userId, grant.getUserId());
        assertEquals(restriction, grant.getRestriction());
    }

    @Test
    public void testDelete() throws SQLException {
        AoRefRestriction restriction = dao.getById(1);
        assertNotNull(restriction);
        int grantCount = entityManager.count(AoAccessGrant.class);
        dao.delete(restriction);

        assertNull(dao.getById(1));
        // restriction 1 has got 2 access grants associated with it
        assertEquals(grantCount - 2, entityManager.count(AoAccessGrant.class));
        assertEquals(count(1, RefRestrictionDatabaseUpdater.TEST_RESTRICTIONS.values()) - 1, dao.count(1));
    }

    @Test
    public void testDeleteForGroup() {
        int accesses1Before = dao.findGrantsByRestrictionId(1).length;
        int accesses2Before = dao.findGrantsByRestrictionId(2).length;

        dao.deleteForGroup(GROUP_1);

        // verify that no restrictions have been deleted
        assertEquals(count(1, RefRestrictionDatabaseUpdater.TEST_RESTRICTIONS.values()), dao.count(1));
        assertEquals(count(2, RefRestrictionDatabaseUpdater.TEST_RESTRICTIONS.values()), dao.count(2));

        AoAccessGrant[] access1 = dao.findGrantsByRestrictionId(1);
        AoAccessGrant[] access2 = dao.findGrantsByRestrictionId(2);
        assertEquals(accesses1Before - 1, access1.length);
        assertEquals(accesses2Before - 1, access2.length);
        for (AoAccessGrant access : access1) {
            Assert.assertNotEquals(GROUP_1, access.getGroup());
        }
        for (AoAccessGrant access : access2) {
            Assert.assertNotEquals(GROUP_1, access.getGroup());
        }
    }

    @Test
    public void testDeleteForGroupNoAccesses() {
        int before = dao.findGrantsByRestrictionId(1).length;
        dao.deleteForGroup("some-random-group-without-accesses");
        assertEquals(before, dao.findGrantsByRestrictionId(1).length);
    }

    @Test
    public void testDeleteForRepository() {
        dao.deleteForRepository(2);

        assertEquals(count(1, RefRestrictionDatabaseUpdater.TEST_RESTRICTIONS.values()), dao.count(1));
        assertEquals(0, dao.count(2));
    }

    @Test
    public void testDeleteForUser() {
        int accesses1Before = dao.findGrantsByRestrictionId(1).length;
        int accesses2Before = dao.findGrantsByRestrictionId(2).length;

        dao.deleteForUser(RefRestrictionDatabaseUpdater.USER_1);

        // verify that no restrictions have been deleted
        assertEquals(count(1, RefRestrictionDatabaseUpdater.TEST_RESTRICTIONS.values()), dao.count(1));
        assertEquals(count(2, RefRestrictionDatabaseUpdater.TEST_RESTRICTIONS.values()), dao.count(2));

        AoAccessGrant[] access1 = dao.findGrantsByRestrictionId(1);
        AoAccessGrant[] access2 = dao.findGrantsByRestrictionId(2);
        assertEquals(accesses1Before - 1, access1.length);
        assertEquals(accesses2Before - 1, access2.length);
        for (AoAccessGrant access : access1) {
            assertNotEquals((Integer) RefRestrictionDatabaseUpdater.USER_1, access.getUserId());
        }
        for (AoAccessGrant access : access2) {
            assertNotEquals((Integer) RefRestrictionDatabaseUpdater.USER_1, access.getUserId());
        }
    }

    @Test
    public void testDeleteForUserNoAccesses() {
        int before = dao.findGrantsByRestrictionId(1).length;
        dao.deleteForUser(999123);
        assertEquals(before, dao.findGrantsByRestrictionId(1).length);
    }

    //-- find(..) tests --//

    @Test
    public void testFind() {
        Page<AoRefRestriction> restrictions = dao.find(1, null, null, null, PageUtils.newRequest(0, 500));

        // Expected: 2, 3, 1
        assertPage(3, true, restrictions);
        assertArrayEquals(new Integer[] {2, 3, 1}, getRestrictionIds(restrictions));
    }

    @Test
    public void testFindByMatcher() {
        Page<AoRefRestriction> restrictions = dao.find(2, null, pattern(RefRestrictionDatabaseUpdater.PATTERN1), null, PageUtils.newRequest(0, 500));

        // Expected: 6, 5
        assertPage(2, true, restrictions);
        assertArrayEquals(new Integer[] {6, 5}, getRestrictionIds(restrictions));
    }

    @Test
    public void testFindByBranchMatcherType() {
        Page<AoRefRestriction> restrictions = dao.find(2, null, null, Collections.singleton(BranchRestrictionType.getInstance()),
                PageUtils.newRequest(0, 500));

        // Expected: 4
        assertPage(1, true, restrictions);
        assertArrayEquals(new Integer[] {4}, getRestrictionIds(restrictions));
    }

    @Test
    public void testFindByPatternMatcherType() {
        Page<AoRefRestriction> restrictions = dao.find(2, null, null, Collections.singleton(PatternRestrictionType.getInstance()),
                PageUtils.newRequest(0, 500));

        // Expected: 5, 6
        assertPage(2, true, restrictions);
        assertArrayEquals(new Integer[] {6, 5}, getRestrictionIds(restrictions));
    }

    @Test
    public void testFindByType() {
        Page<AoRefRestriction> restrictions = dao.find(1, Collections.singleton(RefRestrictionType.READ_ONLY),
                null, null, PageUtils.newRequest(0, 500));

        // Expected: 3, 1
        assertPage(2, true, restrictions);
        assertArrayEquals(new Integer[]{3, 1}, getRestrictionIds(restrictions));
    }

    @Test
    public void testGetByIdMiss() {
        assertNull(dao.getById(99));
    }

    @Test
    public void testGetById() {
        AoRefRestriction restriction = dao.getById(2);

        assertNotNull(restriction);
        assertEquals((Integer) 1, restriction.getRepositoryId());
        assertEquals(BranchRestrictionType.getInstance().getId(), restriction.getMatcherType());
        Assert.assertEquals(RefRestrictionDatabaseUpdater.BR_1_0, restriction.getMatcherId());

        restriction = dao.getById(5);

        assertNotNull(restriction);
        assertEquals((Integer) 2, restriction.getRepositoryId());
        assertEquals(PatternRestrictionType.getInstance().getId(), restriction.getMatcherType());
        Assert.assertEquals(RefRestrictionDatabaseUpdater.PATTERN1, restriction.getMatcherId());
    }

    @Test
    public void testSetAccessGrants() throws SQLException {
        // restriction 2 has access grants for USER_1, USER_2, GROUP_1, GROUP_2, GROUP_3
        AoRefRestriction restriction = dao.getById(2);

        int grantCount = entityManager.count(AoAccessGrant.class);
        AoRefRestriction updated = dao.setAccessGrants(restriction,
                Lists.newArrayList(GROUP_1, "new-group"), Lists.newArrayList(USER_1, 99));

        assertEquals(4, updated.getAccessGrants().length);
        assertContainsGrant(updated.getAccessGrants(), GROUP_1);
        assertContainsGrant(updated.getAccessGrants(), "new-group");
        assertContainsGrant(updated.getAccessGrants(), USER_1);
        assertContainsGrant(updated.getAccessGrants(), 99);

        assertEquals(grantCount - 1, entityManager.count(AoAccessGrant.class));
    }

    @Test
    public void testSetAccessGrantsNoMoreGrants() throws SQLException {
        // restriction 2 has access grants for USER_1, USER_2, GROUP_1, GROUP_2, GROUP_3
        AoRefRestriction restriction = dao.getById(2);
        int grantCount = entityManager.count(AoAccessGrant.class);
        AoRefRestriction updated = dao.setAccessGrants(restriction,
                Lists.newArrayList(), Lists.newArrayList());

        assertEquals(0, updated.getAccessGrants().length);
        assertEquals(grantCount - 5, entityManager.count(AoAccessGrant.class));
    }

    @Test
    public void testSetAccessGrantsNoChange() throws SQLException {
        // restriction 2 has access grants for USER_1, USER_2, GROUP_1, GROUP_2, GROUP_3
        AoRefRestriction restriction = dao.getById(2);
        Set<Integer> grantIds = Sets.newHashSet(restriction.getAccessGrants())
                .stream().map(AoAccessGrant::getId).collect(Collectors.toSet());

        int grantCount = entityManager.count(AoAccessGrant.class);
        AoRefRestriction updated = dao.setAccessGrants(restriction,
                Lists.newArrayList(GROUP_1, GROUP_2, GROUP_3), Lists.newArrayList(USER_1, USER_2));

        assertEquals(5, updated.getAccessGrants().length);
        assertContainsGrant(updated.getAccessGrants(), GROUP_1);
        assertContainsGrant(updated.getAccessGrants(), GROUP_2);
        assertContainsGrant(updated.getAccessGrants(), GROUP_3);
        assertContainsGrant(updated.getAccessGrants(), USER_1);
        assertContainsGrant(updated.getAccessGrants(), USER_2);

        assertEquals(grantCount, entityManager.count(AoAccessGrant.class));
        Set<Integer> newIds = Sets.newHashSet(restriction.getAccessGrants())
                .stream().map(AoAccessGrant::getId).collect(Collectors.toSet());
        assertEquals(grantIds, newIds);

        AoRefRestriction fetched = dao.getById(2);
        Set<Integer> fetchedIds = Sets.newHashSet(fetched.getAccessGrants())
                .stream().map(AoAccessGrant::getId).collect(Collectors.toSet());
        assertEquals(grantIds, fetchedIds);
    }

    private void assertContainsGrant(AoAccessGrant[] grants, String group) {
        for (AoAccessGrant grant : grants) {
            if (group.equals(grant.getGroup())) {
                return;
            }
        }

        fail("No access grant found for group " + group);
    }

    private void assertContainsGrant(AoAccessGrant[] grants, Integer userId) {
        for (AoAccessGrant grant : grants) {
            if (userId.equals(grant.getUserId())) {
                return;
            }
        }

        fail("No access grant found for user " + userId);
    }

    private void assertPage(int expectedSize, boolean lastPage, Page<?> page) {
        assertNotNull(page);
        assertEquals(expectedSize, page.getSize());
        assertEquals(lastPage, page.getIsLastPage());
    }

    private int count(final int repoId, Iterable<RefRestrictionDatabaseUpdater.TestRefRestriction> restrictions) {
        return Iterables.size(Iterables.filter(restrictions, restriction -> restriction.getRepositoryId() == repoId));
    }

    private Integer[] getRestrictionIds(Page<AoRefRestriction> page) {
        return Chainable.chain(page.getValues()).transform(AoRefRestriction::getId).toList().toArray(new Integer[]{});
    }

    private BranchMatcher branch(String refId) {
        return new BranchMatcher(refId, true, () -> null);
    }

    private PatternMatcher pattern(String pattern) {
        return new PatternMatcher(pattern, true);
    }
}