package com.atlassian.stash.internal.repository.sync.dao;

import com.atlassian.activeobjects.external.ActiveObjects;
import com.atlassian.activeobjects.test.TestActiveObjects;
import com.atlassian.bitbucket.ao.AbstractAoDaoTest;
import com.atlassian.bitbucket.project.Project;
import com.atlassian.bitbucket.repository.MinimalRef;
import com.atlassian.bitbucket.repository.RefChange;
import com.atlassian.bitbucket.repository.Repository;
import com.atlassian.bitbucket.repository.sync.RefSyncStatus;
import com.atlassian.bitbucket.repository.sync.RejectedRef;
import com.atlassian.bitbucket.repository.sync.RejectedRefState;
import com.atlassian.bitbucket.scm.git.GitRefPattern;
import com.atlassian.stash.internal.repository.sync.RefSyncResult;
import com.atlassian.stash.internal.repository.sync.SimpleRejectedRef;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import net.java.ao.test.jdbc.Data;
import org.junit.Before;
import org.junit.Test;

import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Set;
import java.util.concurrent.TimeUnit;

import static com.atlassian.bitbucket.repository.RefTestUtils.minimalBranch;
import static com.atlassian.bitbucket.repository.RefTestUtils.minimalTag;
import static com.atlassian.stash.internal.repository.sync.SimpleRejectedRef.isTag;
import static org.junit.Assert.*;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;

@Data(RefSyncDatabaseUpdater.class)
public class AoRefSyncDaoTest extends AbstractAoDaoTest {

    private ActiveObjects ao;
    private AoRefSyncDao dao;

    public AoRefSyncDaoTest() {
        super(AoRefSyncConfig.class, AoRejectedRef.class);
    }

    @Before
    public void setup() {
        ao = new TestActiveObjects(entityManager);
        dao = new AoRefSyncDao(ao);
    }

    @Test
    public void testCleanup() {
        Repository one = repositoryFor(1);
        assertTrue(dao.isEnabled(one.getId()));

        dao.cleanup(one.getId());

        assertFalse(dao.isEnabled(one.getId()));
        assertNull(ao.get(AoRefSyncConfig.class, 1));
        assertEquals(0, ao.find(AoRejectedRef.class, AoRejectedRef.REPOSITORY_ID_COLUMN + " = ?", 1).length);
    }

    @Test
    public void testCleanupWithUnconfiguredRepository() {
        Repository three = repositoryFor(3);
        assertNull("Repository 3 has been explicitly configured", ao.get(AoRefSyncConfig.class, 3));

        dao.cleanup(three.getId());
    }

    @Test
    public void testGetRef() {
        assertNull(dao.getRef(repositoryFor(3).getId(), "refs/heads/master")); //No config or refs for repository 3
        assertNull(dao.getRef(repositoryFor(2).getId(), "refs/heads/master")); //Repository 2 is configured but has no refs

        Repository one = repositoryFor(1);
        Integer oneId = one.getId();
        assertRef(dao.getRef(oneId, "refs/tags/release-1.0.0"), "refs/tags/release-1.0.0", RejectedRefState.AHEAD);
        assertRef(dao.getRef(oneId, "refs/heads/master"), "refs/heads/master", RejectedRefState.DIVERGED);
        assertRef(dao.getRef(oneId, "refs/heads/feature"), "refs/heads/feature", RejectedRefState.ORPHANED);
    }

    @Test
    public void testGetStatus() {
        assertNull("Repository 3 should not be configured", dao.getStatus(repositoryFor(3).getId()));

        RefSyncStatus status = dao.getStatus(repositoryFor(1).getId());
        assertNotNull("Repository 1 should be configured", status);
        assertTrue(status.isEnabled());
        assertEquals(RefSyncDatabaseUpdater.LAST_SYNC_ON_ONE, status.getLastSync());
        assertRef(Iterables.getOnlyElement(status.getAheadRefs()), "refs/tags/release-1.0.0", RejectedRefState.AHEAD);
        assertRef(Iterables.getOnlyElement(status.getDivergedRefs()), "refs/heads/master", RejectedRefState.DIVERGED);
        assertRef(Iterables.getOnlyElement(status.getOrphanedRefs()), "refs/heads/feature", RejectedRefState.ORPHANED);

        status = dao.getStatus(repositoryFor(2).getId());
        assertNotNull("Repository 2 should be configured", status);
        assertFalse(status.isEnabled());
        assertEquals(RefSyncDatabaseUpdater.LAST_SYNC_ON_TWO, status.getLastSync());
        assertTrue(status.getAheadRefs().isEmpty());
        assertTrue(status.getDivergedRefs().isEmpty());
        assertTrue(status.getOrphanedRefs().isEmpty());
    }

    @Test
    public void testIsEnabled() {
        assertTrue(dao.isEnabled(repositoryFor(1).getId()));  //Explicitly true
        assertFalse(dao.isEnabled(repositoryFor(2).getId())); //Explicitly false
        assertFalse(dao.isEnabled(repositoryFor(3).getId())); //Does not exist
    }

    @Test
    public void testRemoveRef() {
        assertEquals(0, dao.removeRef(repositoryFor(3).getId(), "refs/heads/master")); //No config or refs for repository 3
        assertEquals(0, dao.removeRef(repositoryFor(2).getId(), "refs/heads/master")); //Repository 2 is configured but has no refs
        assertEquals(1, dao.removeRef(repositoryFor(1).getId(), "refs/heads/master"));
    }

    @Test
    public void testSetDisabled() {
        Repository two = repositoryFor(2);

        assertFalse("The default state for repository 2 is no longer disabled", dao.isEnabled(two.getId()));
        dao.setEnabled(two.getId(), true);
        assertTrue(dao.isEnabled(two.getId()));
    }

    @Test
    public void testSetEnabled() {
        Repository one = repositoryFor(1);

        assertTrue("The default state for repository 1 is no longer enabled", dao.isEnabled(one.getId()));
        dao.setEnabled(one.getId(), false);
        assertFalse(dao.isEnabled(one.getId()));
        assertNull(dao.getRef(one.getId(), "refs/heads/master")); //All RejectedRefs should have been deleted
    }

    @Test
    public void testSetEnabledCreatesNewIfNecessary() {
        Repository three = repositoryFor(3);
        assertNull("Repository 3 has been explicitly configured", ao.get(AoRefSyncConfig.class, 3));

        dao.setEnabled(three.getId(), true);
        assertTrue(dao.isEnabled(three.getId()));
    }

    @Test
    public void testSetEnabledDoesNotCreateNewIfEnabledIsFalse() {
        Repository three = repositoryFor(3);
        assertNull("Repository 3 has been explicitly configured", ao.get(AoRefSyncConfig.class, 3));

        dao.setEnabled(three.getId(), false);
        assertFalse(dao.isEnabled(three.getId()));
        assertNull("A row should not have been needlessly created for repository 3", ao.get(AoRefSyncConfig.class, 3));
    }

    @Test
    public void testSetStatus() {
        Repository one = repositoryFor(1);

        //The results here should:
        //- Remove the AHEAD RejectedRef for release-1.0.0
        //- Ignore the update to refs/heads/master; the state is the same
        //- Update refs/heads/feature from ORPHANED to DIVERGED
        //- Add a new RejectedRef for release-2.0.0
        RefSyncResult result = new RefSyncResult.Builder()
                .isSynchronized()
                .changedRef(refChangeFor("refs/tags/release-1.0.0"))
                .rejectedRef(rejectedRefFor(minimalBranch("feature"), RejectedRefState.DIVERGED))
                .rejectedRef(rejectedRefFor(minimalBranch("master"), RejectedRefState.DIVERGED))
                .rejectedRef(rejectedRefFor(minimalTag("release-2.0.0"), RejectedRefState.AHEAD))
                .build();

        long start = System.currentTimeMillis();
        dao.setStatus(one.getId(), result);

        RefSyncStatus status = dao.getStatus(one.getId());
        assertNotNull(status);
        assertTrue(status.isEnabled()); //Should not have changed
        assertTrue(status.getLastSync().getTime() - start < TimeUnit.SECONDS.toMillis(3)); //Should have updated
        assertTrue(status.getOrphanedRefs().isEmpty()); //refs/heads/feature is now DIVERGED
        assertRef(Iterables.getOnlyElement(status.getAheadRefs()), "refs/tags/release-2.0.0", RejectedRefState.AHEAD);

        List<RejectedRef> diverged = sortRefs(status.getDivergedRefs());
        assertEquals(2, diverged.size());
        assertRef(diverged.get(0), "refs/heads/feature", RejectedRefState.DIVERGED);
        assertRef(diverged.get(1), "refs/heads/master", RejectedRefState.DIVERGED);
    }

    @Test
    public void testSetStatusDoesNotUpdateTimestampIfNotSynchronized() {
        Repository one = repositoryFor(1);
        RefSyncResult result = new RefSyncResult.Builder()
                .rejectedRef(rejectedRefFor(minimalTag("release-2.0.0"), RejectedRefState.AHEAD))
                .build();

        dao.setStatus(one.getId(), result);

        RefSyncStatus status = dao.getStatus(one.getId());
        assertNotNull(status);
        assertTrue(status.isEnabled());
        assertEquals(RefSyncDatabaseUpdater.LAST_SYNC_ON_ONE, status.getLastSync()); //Not synced, so not updated
        //The existing RejectedRefs should not be removed, so this verifies they're still present
        assertRef(Iterables.getOnlyElement(status.getDivergedRefs()), "refs/heads/master", RejectedRefState.DIVERGED);
        assertRef(Iterables.getOnlyElement(status.getOrphanedRefs()), "refs/heads/feature", RejectedRefState.ORPHANED);

        List<RejectedRef> ahead = sortRefs(status.getAheadRefs());
        assertEquals(2, ahead.size());
        assertRef(ahead.get(0), "refs/tags/release-1.0.0", RejectedRefState.AHEAD);
        //The newly-rejected release-2.0.0 tag should have been added to the AHEAD set
        assertRef(ahead.get(1), "refs/tags/release-2.0.0", RejectedRefState.AHEAD);
    }

    @Test(expected = IllegalStateException.class)
    public void testSetStatusWithUnconfiguredRepository() {
        Project project = mock(Project.class);
        when(project.getKey()).thenReturn("KEY");

        Repository three = repositoryFor(3);
        when(three.getProject()).thenReturn(project);
        when(three.getSlug()).thenReturn("slug");

        dao.setStatus(three.getId(), new RefSyncResult.Builder().build());
    }

    @Test
    public void testUpdateRef() {
        //Repository 3 is not configured and has no refs
        assertFalse(dao.updateRef(repositoryFor(3).getId(), "refs/heads/master", RejectedRefState.DIVERGED));

        //Repository 2 is configured but disabled and has no refs
        assertFalse(dao.updateRef(repositoryFor(2).getId(), "refs/heads/master", RejectedRefState.DIVERGED));

        Repository one = repositoryFor(1);
        //This update matches the existing state, so it does nothing
        assertFalse(dao.updateRef(one.getId(), "refs/heads/master", RejectedRefState.DIVERGED));
        //This update changes the ref from ORPHANED to DIVERGED
        assertTrue(dao.updateRef(one.getId(), "refs/heads/feature", RejectedRefState.DIVERGED));
    }

    private static void assertRef(RejectedRef ref, String id, RejectedRefState state) {
        assertNotNull(ref);
        assertEquals(displayIdFor(id), ref.getDisplayId());
        assertEquals(id, ref.getId());
        assertEquals(isTag(id), ref.isTag());
        assertSame(state, ref.getState());
    }

    private static String displayIdFor(String id) {
        return isTag(id) ? GitRefPattern.TAGS.unqualify(id) : GitRefPattern.HEADS.unqualify(id);
    }

    private static RejectedRef rejectedRefFor(MinimalRef ref, RejectedRefState state) {
        return new SimpleRejectedRef.Builder()
                .displayId(ref.getDisplayId())
                .id(ref.getId())
                .type(ref.getType())
                .state(state)
                .build();
    }

    private static RefChange refChangeFor(String id) {
        //This is mocked because SimpleRefChange needs a lot of data the DAO doesn't care about
        RefChange change = mock(RefChange.class);
        MinimalRef ref = mock(MinimalRef.class);
        when(change.getRef()).thenReturn(ref);
        when(ref.getId()).thenReturn(id);

        return change;
    }

    private static Repository repositoryFor(int id) {
        Repository repository = mock(Repository.class);
        when(repository.getId()).thenReturn(id);

        return repository;
    }

    private static List<RejectedRef> sortRefs(Set<RejectedRef> refs) {
        List<RejectedRef> diverged = Lists.newArrayList(refs);
        Collections.sort(diverged, new Comparator<RejectedRef>() {

            @Override
            public int compare(RejectedRef lhs, RejectedRef rhs) {
                //Ensure the refs are sorted by their IDs, just to avoid database-ordering vagaries
                return lhs.getId().compareTo(rhs.getId());
            }
        });
        return diverged;
    }

}
