package com.atlassian.stash.internal.content;

import com.atlassian.bitbucket.annotation.Dao;
import com.atlassian.bitbucket.annotation.DataSet;
import com.atlassian.bitbucket.idx.IndexSearchRequest;
import com.atlassian.stash.internal.AbstractDaoTest;
import com.atlassian.stash.internal.DataSets;
import com.atlassian.stash.internal.idx.InternalIndexedCommit;
import com.atlassian.stash.internal.repository.HibernateRepositoryDao;
import com.atlassian.stash.internal.repository.InternalRepository;
import com.atlassian.stash.internal.repository.RepositoryDao;
import com.atlassian.bitbucket.property.PropertyMap;
import com.atlassian.bitbucket.repository.Repository;
import com.atlassian.bitbucket.util.Page;
import com.atlassian.bitbucket.util.PageRequest;
import com.atlassian.bitbucket.util.PageUtils;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import org.hamcrest.collection.IsIterableContainingInOrder;
import org.junit.Before;
import org.junit.Test;

import java.util.*;

import static com.atlassian.bitbucket.hamcrest.PropertyMapMatchers.hasEntry;
import static org.hamcrest.Matchers.*;

@DataSet({DataSets.PROJECTS, DataSets.REPOSITORIES, DataSets.INDEXED_COMMITS})
public class HibernateIndexedCommitDaoTest extends AbstractDaoTest {

    @Dao
    private HibernateIndexedCommitDao dao;
    @Dao(HibernateRepositoryDao.class)
    private RepositoryDao repositoryDao;

    private InternalIndexedCommit commit_aef1234;
    private InternalIndexedCommit commit_4321fae;
    private InternalIndexedCommit commit_98765ee;
    
    private InternalRepository repo1;
    private InternalRepository repo2;
    private InternalRepository repo3;
    
    @Before
    public void setup() {
        // this is the test data that is defined in idx_commits
        commit_aef1234 = dao.getById("aef1234");
        commit_98765ee = dao.getById("98765ee");
        commit_4321fae = dao.getById("4321fae");
        
        repo1 = repositoryDao.getById(1);
        repo2 = repositoryDao.getById(2);
        repo3 = repositoryDao.getById(3);
    }

    @Test
    public void testAddMembership() throws Exception {
        assertTrue(dao.addMembership(commit_98765ee, repo3));
        flush(); // make sure the addition is actually performed in the DB
        clear();

        assertTrue(dao.isIndexed(commit_98765ee.getId(), repo3));

        // add the same membership again, should have no effect
        assertFalse(dao.addMembership(commit_98765ee, repo3));
        assertTrue(dao.isIndexed(commit_98765ee.getId(), repo3));
    }

    @Test
    public void testAddProperty() throws Exception {
        assertTrue(dao.addProperty(commit_98765ee, "test", "value"));
        flush(); // make sure the addition is actually performed in the DB
        clear();

        Set<String> values = dao.getPropertyValues(commit_98765ee.getId(), "test");
        assertEquals(1, values.size());
        assertEquals("value", values.iterator().next());
    }

    @Test
    public void testCjkPropertyCrud() throws Exception {
        String attributeName = "\u5718\u96c6";
        String value = "\u96c6\u5718";

        // insert cjk attr name & value
        assertTrue(dao.addProperty(commit_98765ee, attributeName, value));
        flush();
        clear();

        // look up by commit and cjk name
        Set<String> values = dao.getPropertyValues(commit_98765ee.getId(), attributeName);
        assertEquals(1, values.size());
        assertEquals(value, values.iterator().next());

        // search by cjk name & value
        Page<InternalIndexedCommit> commits = dao.findByProperty(attributeName, value, true, PageUtils.newRequest(0, 100));
        assertEquals(1, commits.getSize());
        assertEquals(commit_98765ee, Iterables.getFirst(commits.getValues(), null));

        // remove
        assertTrue(dao.removeProperty(commit_98765ee.getId(), attributeName, value));
        flush();
        clear();

        // check the attributes were removed
        assertTrue(dao.getPropertyValues(commit_98765ee.getId(), attributeName).isEmpty());
    }

    @Test
    public void testDeleteCascades() {
        // when an InternalIndexedCommit is deleted, all memberships and attributes should be removed
        dao.delete(commit_aef1234);
        flush();
        clear();

        // aef1234 was member of repo 1, 2 and 3
        assertFalse(dao.isIndexed(commit_aef1234.getId(), repo1));
        assertFalse(dao.isIndexed(commit_aef1234.getId(), repo2));
        assertFalse(dao.isIndexed(commit_aef1234.getId(), repo3));

        // aef1234 had 2 jira keys associated with it: CAV-1234 and CAV-2345
        assertTrue(dao.getPropertyValues(commit_aef1234.getId(), "jira").isEmpty());
    }

    @Test
    public void testDeleteDeletesOrphans() {
        // 4321fae is only member of repo 3
        dao.deleteMembership(commit_4321fae.getId(), repo3);

        // flush to make sure Hibernate cleans up the relationships / cascades
        flush();
        clear();

        // the commit should have been deleted
        InternalIndexedCommit commit = dao.getById(commit_4321fae.getId());
        assertNull(commit);

        // its attributes should be history too
        assertTrue(dao.getPropertyValues(commit_4321fae.getId(), "jira").isEmpty());
        assertTrue(dao.getPropertyValues(commit_4321fae.getId(), "bamboo").isEmpty());
    }

    @Test
    public void testDeleteAllMemberships() throws Exception {
        dao.deleteAllMemberships(repo3);

        // flush to make sure Hibernate cleans up the relationships / cascades
        flush();
        clear();

        // verify all the memberships to repo3 were deleted
        assertFalse(dao.isIndexed(commit_aef1234.getId(), repo3));
        assertFalse(dao.isIndexed(commit_4321fae.getId(), repo3));
        assertEquals(0, dao.findAll(PageUtils.newRequest(0, 10),
                commit -> commit.getRepositories().contains(repo3)).getSize());
    }

    @Test
    public void testDeleteMembership() throws Exception {
        assertTrue(dao.isIndexed(commit_aef1234.getId(), repo1));
        assertTrue(dao.deleteMembership(commit_aef1234.getId(), repo1));
        flush(); // make sure the delete is actually performed in the DB
        clear();

        assertFalse(dao.isIndexed(commit_aef1234.getId(), repo1));

        // delete it again, should not result in errors
        assertFalse(dao.deleteMembership(commit_aef1234.getId(), repo1));
    }

    @Test
    public void testFindAll() throws Exception {
        Page<InternalIndexedCommit> all = dao.findAll(PageUtils.newRequest(0, 10));
        assertEquals(3, all.getSize());
        assertThat(all.getValues(), hasItems(commit_aef1234, commit_4321fae, commit_98765ee));
    }

    @Test
    public void testFindByProperty() throws Exception {
        PageRequest pageRequest = PageUtils.newRequest(0, 100);
        Page<InternalIndexedCommit> commits = dao.findByProperty("jira", "CAV-1234", true, pageRequest);

        // 2 matches defined in idx_commits.xml
        assertEquals(2, commits.getSize());

        // verify that it is sorted by date desc
        assertEquals(ImmutableList.of(commit_aef1234, commit_4321fae), ImmutableList.copyOf(commits.getValues()));

        // add another commit before and after the current commits to verify correct sort order
        Date past = new GregorianCalendar(2000, 1, 2, 3, 4, 5).getTime();
        Date future = new GregorianCalendar(2020, 1, 2, 3, 4, 5).getTime();
        InternalIndexedCommit before = new InternalIndexedCommit("before", past);
        InternalIndexedCommit after = new InternalIndexedCommit("after", future);
        dao.create(after);
        dao.create(before);

        // associate both commits with jira:CAV-1234
        dao.addProperty(before, "jira", "CAV-1234");
        dao.addProperty(after, "jira", "CAV-1234");

        // now verify the sort order
        commits = dao.findByProperty("jira", "CAV-1234", true, pageRequest);
        assertEquals(4, commits.getSize());
        assertEquals(ImmutableList.of(after, commit_aef1234, commit_4321fae, before),
                ImmutableList.copyOf(commits.getValues()));
    }

    @Test
    public void testFindByPropertyCaseSensitivity() throws Exception {
        // check case insensitive
        Page<InternalIndexedCommit> commits = dao.findByProperty("jira", "cav-1234", true, PageUtils.newRequest(0, 100));
        assertEquals(0, commits.getSize());

        commits = dao.findByProperty("jira", "cav-1234", false, PageUtils.newRequest(0, 100));
        assertEquals(2, commits.getSize());
    }

    @Test
    public void testFindByPropertyPaging() throws Exception {
        // verify page 1
        Page<InternalIndexedCommit> commits = dao.findByProperty("jira", "CAV-1234", true, PageUtils.newRequest(0, 1));

        assertEquals(1, commits.getSize());
        assertFalse(commits.getIsLastPage());
        assertEquals(ImmutableList.of(commit_aef1234), ImmutableList.copyOf(commits.getValues()));

        // verify page 2
        commits = dao.findByProperty("jira", "CAV-1234", true, PageUtils.newRequest(1, 1));

        assertEquals(1, commits.getSize());
        assertTrue(commits.getIsLastPage());
        assertEquals(ImmutableList.of(commit_4321fae), ImmutableList.copyOf(commits.getValues()));

        // verify empty page 3
        commits = dao.findByProperty("jira", "CAV-1234", true, PageUtils.newRequest(2, 1));

        assertEquals(0, commits.getSize());
        assertTrue(commits.getIsLastPage());
    }

    @Test
    public void testGetPropertiesForMultipleCommits() {
        Map<String, PropertyMap> properties = dao.getProperties(
                ImmutableSet.of(commit_4321fae.getId(), commit_98765ee.getId(), commit_aef1234.getId(), "no-match"),
                ImmutableSet.of("jira", "bamboo", "other"));

        assertNotNull(properties);
        assertEquals(2, properties.size()); // no entries for commit_98765ee and 'no-match' because they don't have any of the req. attributes
        assertThat(properties.get(commit_aef1234.getId()), hasEntry("jira", ImmutableSet.of("CAV-1234", "CAV-2345")));
        assertThat(properties.get(commit_4321fae.getId()), allOf(
                hasEntry("jira", ImmutableSet.of("CAV-1234")),
                hasEntry("bamboo", ImmutableSet.of("CAV-MASTER-1234:FAIL"))
        ));
    }

    @Test
    public void testGetPropertiesForMultipleCommitsWithMultipleNames() {
        Map<String, PropertyMap> properties = dao.getProperties(
                ImmutableSet.of(commit_4321fae.getId(), commit_98765ee.getId(), "no-match"),
                ImmutableSet.of("jira", "other"));

        assertNotNull(properties);
        assertEquals(1, properties.size()); // no entries for commit_98765ee and 'no-match' because they don't have any of the req. attributes
        assertThat(properties.get(commit_4321fae.getId()), hasEntry("jira", ImmutableSet.of("CAV-1234")));

    }

    @Test
    public void testGetPropertiesForMultipleCommitsWithNoCommitIds() {
        Map<String, PropertyMap> properties = dao.getProperties(
                Collections.<String>emptySet(),
                ImmutableSet.of("jira", "bamboo", "no-match"));
        assertNotNull(properties);
        assertTrue(properties.isEmpty());
    }

    @Test
    public void testGetPropertiesForMultipleCommitsWithNoNames() {
        Map<String, PropertyMap> properties = dao.getProperties(
                ImmutableSet.of(commit_4321fae.getId(), commit_98765ee.getId(), "no-match"),
                Collections.<String>emptySet());

        assertNotNull(properties);
        assertTrue(properties.isEmpty());
    }

    @Test
    public void testGetPropertiesForSingleCommit() {
        // simple case; only a single property type
        PropertyMap properties = dao.getProperties(commit_aef1234.getId(), ImmutableSet.of("jira", "bamboo", "random stuff"));

        assertNotNull(properties);
        assertEquals(1, properties.size()); // only 'jira' attributes
        assertEquals(ImmutableSet.of("CAV-1234", "CAV-2345"), properties.get("jira"));
    }

    @Test
    public void testGetPropertiesForSingleCommitWithMultipleNames() {
        // slightly more complicated case; 2 types of attributes
        PropertyMap properties = dao.getProperties(commit_4321fae.getId(), ImmutableSet.of("jira", "bamboo", "random stuff"));

        assertNotNull(properties);
        assertEquals(2, properties.size()); // 'jira' and 'bamboo' attributes
        assertEquals(ImmutableSet.of("CAV-1234"), properties.get("jira"));
        assertEquals(ImmutableSet.of("CAV-MASTER-1234:FAIL"), properties.get("bamboo"));
    }

    @Test
    public void testGetPropertiesForSingleCommitWithNoMatchingNames() {
        PropertyMap properties = dao.getProperties(commit_4321fae.getId(),
                ImmutableSet.of("some", "attributenames", "that", "don't", "exist"));
        assertNotNull(properties);
        assertTrue(properties.isEmpty());
    }

    @Test
    public void testGetPropertiesForSingleCommitWithNoNames() {
        PropertyMap properties = dao.getProperties(commit_4321fae.getId(), Collections.<String>emptySet());
        assertNotNull(properties);
        assertTrue(properties.isEmpty());
    }

    @Test
    public void testGetPropertiesForSingleCommitWithOneName() {
        // now request just one of the attributes
        PropertyMap properties = dao.getProperties(commit_4321fae.getId(), ImmutableSet.of("jira"));

        assertNotNull(properties);
        assertEquals(1, properties.size()); // only 'jira' should be returned this time
        assertEquals(ImmutableSet.of("CAV-1234"), properties.get("jira"));
    }

    @Test
    public void testIsMemberOf() throws Exception {
        assertTrue(dao.isIndexed(commit_aef1234.getId(), repo1));
        assertTrue(dao.isIndexed(commit_aef1234.getId(), repo2));
        assertTrue(dao.isIndexed(commit_4321fae.getId(), repo3));
        assertFalse(dao.isIndexed(commit_98765ee.getId(), repo1));
        assertFalse(dao.isIndexed(commit_98765ee.getId(), repo2));
    }

    @Test
    public void testMembershipsAreOrderedByRepositoryId() throws Exception {
        InternalIndexedCommit commit = dao.create(new InternalIndexedCommit("abcdef123", new Date()));
        try {
            assertTrue(dao.addMembership(commit, repo3));
            flush(); // make sure the memberships are added in the DB in the requested order
            assertTrue(dao.addMembership(commit, repo1));
            flush();
            assertTrue(dao.addMembership(commit, repo2));
            flush();
            clear();

            InternalIndexedCommit result = dao.getById(commit.getId());
            assertNotNull(result);
            assertThat(result.getId(), equalTo(commit.getId()));
            assertThat(result.getRepositories(), IsIterableContainingInOrder.<Repository>contains(repo1, repo2, repo3));
        } finally {
            dao.deleteMembership(commit.getId(), repo1);
            dao.deleteMembership(commit.getId(), repo2);
            dao.deleteMembership(commit.getId(), repo3);
            dao.deleteById(commit.getId());
        }
    }

    @Test
    public void testRemoveProperty() throws Exception {
        assertEquals(ImmutableSet.of("CAV-1234", "CAV-2345"), dao.getPropertyValues(commit_aef1234.getId(), "jira"));

        assertTrue(dao.removeProperty(commit_aef1234.getId(), "jira", "CAV-1234"));
        assertEquals(ImmutableSet.of("CAV-2345"), dao.getPropertyValues(commit_aef1234.getId(), "jira"));

        // delete the same attribute again
        assertFalse(dao.removeProperty(commit_aef1234.getId(), "jira", "CAV-1234"));
        assertEquals(ImmutableSet.of("CAV-2345"), dao.getPropertyValues(commit_aef1234.getId(), "jira"));

        assertTrue(dao.removeProperty(commit_aef1234.getId(), "jira", "CAV-2345"));
        assertTrue(dao.getPropertyValues(commit_aef1234.getId(), "jira").isEmpty());
    }

    @Test
    public void testSearchWithNoCriteria() throws Exception {
        IndexSearchRequest searchRequest = new IndexSearchRequest.Builder().build();
        Page<InternalIndexedCommit> page = dao.search(searchRequest, PageUtils.newRequest(0, 10));
        assertThat(page.getSize(), greaterThanOrEqualTo(3));
        assertThat(page.getValues(), hasItems(equalTo(commit_aef1234), equalTo(commit_4321fae), equalTo(commit_98765ee)));
    }

    @Test
    public void testSearchWithPrefixes() throws Exception {
        int[] prefixSize = {2, 3, 5};
        for (int i : prefixSize) {
            IndexSearchRequest searchRequest = new IndexSearchRequest.Builder().prefix(commit_4321fae.getId().substring(0, i)).build();
            Page<InternalIndexedCommit> page = dao.search(searchRequest, PageUtils.newRequest(0, 10));
            assertEquals(1, page.getSize());
            assertTrue(page.getIsLastPage());
            assertThat(page.getValues(), hasItem(equalTo(commit_4321fae)));
        }
    }

    @Test(expected = UnsupportedOperationException.class)
    public void testUpdate() throws Exception {
        InternalIndexedCommit commit = dao.getById(commit_4321fae.getId());
        assertNotNull(commit);
        dao.update(commit);
    }
}
