package com.atlassian.stash.internal.scm.git.command.diff;

import com.atlassian.bitbucket.commit.Changeset;
import com.atlassian.bitbucket.commit.Commit;
import com.atlassian.bitbucket.commit.MinimalCommit;
import com.atlassian.bitbucket.commit.NoSuchCommitException;
import com.atlassian.bitbucket.content.*;
import com.atlassian.stash.internal.scm.git.AbstractGitTest;
import com.atlassian.bitbucket.repository.Repository;
import com.atlassian.bitbucket.scm.ChangesCommandParameters;
import com.atlassian.bitbucket.scm.ChangesetsCommandParameters;
import com.atlassian.bitbucket.scm.Command;
import com.atlassian.bitbucket.scm.CommandResult;
import com.atlassian.bitbucket.scm.annotation.Repo;
import com.atlassian.bitbucket.scm.git.command.GitCommandBuilderFactory;
import com.atlassian.bitbucket.scm.git.command.GitCommandFactory;
import com.atlassian.bitbucket.scm.git.GitUtils;
import com.atlassian.bitbucket.scm.git.command.diff.GitDiffRenames;
import com.atlassian.bitbucket.scm.git.command.diff.GitDiffTreeBuilder;
import com.atlassian.bitbucket.util.Page;
import com.atlassian.bitbucket.util.PageUtils;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Iterables;
import org.apache.commons.lang3.StringUtils;
import org.junit.Test;
import org.mockito.ArgumentCaptor;
import org.mockito.InOrder;
import org.springframework.beans.factory.annotation.Autowired;

import java.util.List;

import static org.mockito.Mockito.*;

@SuppressWarnings("ConstantConditions")
public class DiffTreeCommandTest extends AbstractGitTest {

    static final String NULL_TREE = "4b825dc642cb6eb9a060e54bf8d69288fbee4904";

    @Autowired
    private GitCommandBuilderFactory builderFactory;
    @Autowired
    private GitCommandFactory commandFactory;
    @Repo("classpath:git/diff-tree.zip")
    private Repository diffTree;
    @Repo
    private Repository repository;

    @Test
    public void testAgainstNullTree() throws Exception {
        Page<Change> page = runDiffTreeForChanges("57150c54c38d6570b2fd5e6d6cfc19476de44e84", NULL_TREE);
        assertNotNull(page);
        assertTrue(page.getIsLastPage());
        assertEquals(4, page.getSize());

        // verify that all files are additions (all the files at the commit are additions compared to the null tree)
        for (Change change : page.getValues()) {
            assertSame(ChangeType.ADD, change.getType());
            assertSame(ContentTreeNode.Type.FILE, change.getNodeType());
            assertEquals(-1, change.getPercentUnchanged());
            assertNull(change.getSrcExecutable());
            assertNull(change.getSrcPath());
        }

        List<Change> changes = ImmutableList.copyOf(page.getValues());

        Change first = changes.get(0);
        assertEquals("493021b1c9e0ecfebcd5ebce646a44138bc60b39", first.getContentId());
        assertEquals(GitUtils.NULL_SHA1, first.getFromContentId());
        assertEquals("add_file/add_file.txt", first.getPath().toString());
        assertFalse(first.getExecutable());
        Change second = changes.get(1);
        assertEquals("b1d99e581f0bb8138b8997f9268294e41faee05b", second.getContentId());
        assertEquals(GitUtils.NULL_SHA1, second.getFromContentId());
        assertEquals("modification/mod_file.txt", second.getPath().toString());
        assertFalse(second.getExecutable());
        Change third = changes.get(2);
        assertEquals("f49abb7d6c366e6f83470e36439bc162234d7963", third.getContentId());
        assertEquals(GitUtils.NULL_SHA1, third.getFromContentId());
        assertEquals("mv_file/moved_file.txt", third.getPath().toString());
        assertFalse(third.getExecutable());
        Change fourth = changes.get(3);
        assertEquals("2e6b03cbbe90d3c9afcfc98967a5a71b4c9cff1a", fourth.getContentId());
        assertEquals(GitUtils.NULL_SHA1, fourth.getFromContentId());
        assertEquals("out_of_order/file.txt", fourth.getPath().toString());
        assertFalse(fourth.getExecutable());
    }

    @Test
    public void testForAdd() {
        Page<Change> page = runDiffTreeForChanges("05c78271ed4d7a158fe6789f6b27e3a47631faba",
                "57150c54c38d6570b2fd5e6d6cfc19476de44e84");
        assertNotNull(page);
        assertTrue(page.getIsLastPage());
        assertEquals(2, page.getSize());

        List<Change> changes = ImmutableList.copyOf(page.getValues());

        //Note: The first change here is for a symbolic link. Symbolic links are _always_ executable, because the
        //      real permission is only known at the target
        Change first = changes.get(0);
        assertEquals("4cbb553f3f4ac2ee7b01ff6c951d6bf583c39c15", first.getContentId());
        assertEquals(GitUtils.NULL_SHA1, first.getFromContentId());
        assertEquals("symlink/link.txt", first.getPath().toString());
        assertEquals(-1, first.getPercentUnchanged());
        assertNull(first.getSrcExecutable());
        assertNull(first.getSrcPath());
        assertSame(ChangeType.ADD, first.getType());
        assertTrue(first.getExecutable());

        Change second = changes.get(1);
        assertEquals("57a3592bb2cf8e76e2faef7660945a2fe0e24360", second.getContentId());
        assertEquals(GitUtils.NULL_SHA1, second.getFromContentId());
        assertEquals("symlink/target.txt", second.getPath().toString());
        assertEquals(-1, second.getPercentUnchanged());
        assertFalse(second.getExecutable());
        assertNull(second.getSrcExecutable());
        assertNull(second.getSrcPath());
        assertSame(ChangeType.ADD, second.getType());
    }

    @Test
    public void testForCopy() {
        Page<Change> page = runDiffTreeForChanges("0a943a29376f2336b78312d99e65da17048951db",
                "e0a3f6d8c804c375f49d7609328111e8dc75a4bc", true);
        assertNotNull(page);
        assertTrue(page.getIsLastPage());
        assertEquals(1, page.getSize());

        Change change = page.getValues().iterator().next();
        assertEquals("72555d02a139700be930a6734b57c5fab84512b9", change.getContentId());
        assertEquals("72555d02a139700be930a6734b57c5fab84512b9", change.getFromContentId());
        assertEquals("binary/D.zip", change.getPath().toString());
        assertEquals("binary/C.zip", change.getSrcPath().toString());
        assertEquals(100, change.getPercentUnchanged());
        assertFalse(change.getExecutable());
        assertFalse(change.getSrcExecutable());
        assertSame(ChangeType.COPY, change.getType());
    }

    @Test
    public void testForDelete() {
        Page<Change> page = runDiffTreeForChanges("6d8e9736357657738d15cd29069f21bf44553e19",
                "4f9290ae1a9fcde7acd56664a22cc65eb76540f3");
        assertNotNull(page);
        assertTrue(page.getIsLastPage());
        assertEquals(1, page.getSize());

        Change change = page.getValues().iterator().next();
        assertEquals("bdea48ee65c869eb0b86b1283069d76cce0a7254", change.getContentId());
        assertEquals("bdea48ee65c869eb0b86b1283069d76cce0a7254", change.getFromContentId());
        assertEquals("del_file/del_file.txt", change.getPath().toString());
        assertEquals(-1, change.getPercentUnchanged());
        assertFalse(change.getSrcExecutable());
        assertNull(change.getExecutable());
        assertNull(change.getSrcPath());
        assertSame(ChangeType.DELETE, change.getType());
    }

    @Test
    public void testForEmptyDiffToFirstParent() {
        Command<Page<Changeset>> command = commandFactory.changesets(diffTree,
                new ChangesetsCommandParameters.Builder()
                        .commitId("62abb7aa35f8928baa23be2e6cf5cbf1ca43dfdb")
                        .maxChangesPerCommit(5).build(),
                PageUtils.newRequest(0, 10));

        Page<Changeset> page = command.call();
        assertNotNull(page);
        assertEquals(1, page.getSize());

        Changeset changeset = Iterables.getFirst(page.getValues(), null);
        assertNotNull(changeset);

        MinimalCommit fromCommit = changeset.getFromCommit();
        assertNotNull(fromCommit);
        assertEquals("f17a3325dfb2701a1f2c261b3b90252041af7537", fromCommit.getId());

        Commit toCommit = changeset.getToCommit();
        assertEquals("62abb7aa35f8928baa23be2e6cf5cbf1ca43dfdb", toCommit.getId());
        assertEquals("Bryan Turner", toCommit.getAuthor().getName());
        assertEquals(2, toCommit.getParents().size());

        Page<? extends Change> changes = changeset.getChanges();
        assertNotNull(changes);
        assertEquals(0, changes.getSize()); //Diff to the second parent is not empty, diff to the first is
    }

    @Test
    public void testForEmptyDiffToFirstParentPaged() throws Exception {
        Page<Change> page = commandFactory.changes(diffTree,
                new ChangesCommandParameters.Builder()
                        .maxChanges(5)
                        .untilId("62abb7aa35f8928baa23be2e6cf5cbf1ca43dfdb")
                        .build(),
                PageUtils.newRequest(0, 10)).call();

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

    @Test
    public void testForEmptyDiffToFirstParentStreamed() throws Exception {
        ChangeCallback callback = mock(ChangeCallback.class);

        commandFactory.changes(diffTree,
                new ChangesCommandParameters.Builder()
                        .maxChanges(5)
                        .untilId("62abb7aa35f8928baa23be2e6cf5cbf1ca43dfdb")
                        .build(),
                callback).call();

        InOrder ordered = inOrder(callback);
        ordered.verify(callback).onStart(any(ChangeContext.class));
        ordered.verify(callback).onEnd(any(ChangeSummary.class));
        ordered.verifyNoMoreInteractions();
    }

    @Test
    public void testForEmptyDiffToFirstParentStreamedV2() throws Exception {
        ChangeCallback callback = mock(ChangeCallback.class);

        commandFactory.changes(diffTree,
                new ChangesCommandParameters.Builder()
                        .maxChanges(5)
                        .untilId("62abb7aa35f8928baa23be2e6cf5cbf1ca43dfdb")
                        .build(),
                callback).call();

        ArgumentCaptor<ChangeContext> contextCaptor = ArgumentCaptor.forClass(ChangeContext.class);
        ArgumentCaptor<ChangeSummary> summaryCaptor = ArgumentCaptor.forClass(ChangeSummary.class);

        InOrder ordered = inOrder(callback);
        ordered.verify(callback).onStart(contextCaptor.capture());
        ordered.verify(callback).onEnd(summaryCaptor.capture());
        ordered.verifyNoMoreInteractions();

        ChangeContext context = contextCaptor.getValue();
        assertNotNull(context);
        assertEquals("62abb7aa35f8928baa23be2e6cf5cbf1ca43dfdb", context.getToHash());
        assertEquals(5, context.getMaxChanges());
        assertNull(context.getFromHash());

        ChangeSummary summary = summaryCaptor.getValue();
        assertNotNull(summary);
        assertFalse(summary.isTruncated());
        assertSame(CommandResult.SUCCEEDED, summary.getResult());
    }

    @Test
    public void testForModify() {
        Page<Change> page = runDiffTreeForChanges("6053a1eaa1c009dd11092d09a72f3c41af1b59ad",
                "042fb0cc198e7320b59a025689b45a2cb52f4684");
        assertNotNull(page);
        assertTrue(page.getIsLastPage());
        assertEquals(1, page.getSize());

        Change change = page.getValues().iterator().next();
        assertEquals("668076c308990933c2df66efb87b946704252486", change.getContentId());
        assertEquals("8e66897a7bfcbb4dbc3be09fbfd0c300e2f2844b", change.getFromContentId());
        assertEquals("branch_mod_merge/file.txt", change.getPath().toString());
        assertEquals(-1, change.getPercentUnchanged());
        assertFalse(change.getExecutable());
        assertFalse(change.getSrcExecutable());
        assertNull(change.getSrcPath());
        assertSame(ChangeType.MODIFY, change.getType());
    }

    @Test
    public void testForMultipleDiffTrees() {
        Command<Page<Changeset>> command = commandFactory.changesets(repository,
                new ChangesetsCommandParameters.Builder()
                        .commitIds("57150c54c38d6570b2fd5e6d6cfc19476de44e84",
                                "6053a1eaa1c009dd11092d09a72f3c41af1b59ad")
                        .maxChangesPerCommit(5).build(),
                PageUtils.newRequest(0, 10));

        Page<Changeset> page = command.call();
        assertNotNull(page);
        assertEquals(2, page.getSize());

        Changeset changeset = Iterables.getFirst(page.getValues(), null);
        assertNotNull(changeset);

        Commit toCommit = changeset.getToCommit();
        assertEquals("57150c54c38d6570b2fd5e6d6cfc19476de44e84", toCommit.getId());
        assertEquals("57150c54c38", toCommit.getDisplayId());
        assertEquals("Michael Heemskerk", toCommit.getAuthor().getName());

        MinimalCommit fromCommit = changeset.getFromCommit();
        assertNotNull(fromCommit);
        assertEquals("9fbc34a0d905950131d73f352abe68520c6db2a3", fromCommit.getId());
        assertEquals("9fbc34a0d90", fromCommit.getDisplayId());
    }

    @Test
    public void testForRename() {
        Page<Change> page = runDiffTreeForChanges("57150c54c38d6570b2fd5e6d6cfc19476de44e84",
                "9fbc34a0d905950131d73f352abe68520c6db2a3");
        assertNotNull(page);
        assertTrue(page.getIsLastPage());
        assertEquals(1, page.getSize());

        Change change = page.getValues().iterator().next();
        assertEquals("f49abb7d6c366e6f83470e36439bc162234d7963", change.getContentId());
        assertEquals("f49abb7d6c366e6f83470e36439bc162234d7963", change.getFromContentId());
        assertEquals("mv_file/moved_file.txt", change.getPath().toString());
        assertEquals("mv_file/mv_file.txt", change.getSrcPath().toString());
        assertEquals(100, change.getPercentUnchanged());
        assertFalse(change.getExecutable());
        assertFalse(change.getSrcExecutable());
        assertSame(ChangeType.MOVE, change.getType());
    }

    @Test
    public void testForRootCommit() {
        Page<Change> page = runDiffTreeForChanges("409a2180fd95b744df8d817e770b7f8c853f78dc");
        assertNotNull(page);
        assertTrue(page.getIsLastPage());
        assertEquals(1, page.getSize());

        Change change = page.getValues().iterator().next();
        assertEquals("30bda1ba1e8e99781b4fafcc450da84f97a29630", change.getContentId());
        assertEquals(GitUtils.NULL_SHA1, change.getFromContentId());
        assertEquals("out_of_order/file.txt", change.getPath().toString());
        assertEquals(-1, change.getPercentUnchanged());
        assertFalse(change.getExecutable());
        assertNull(change.getSrcExecutable());
        assertNull(change.getSrcPath());
        assertSame(ChangeType.ADD, change.getType());
    }

    @Test(expected = NoSuchCommitException.class)
    public void testNoSuchCommitForChanges() {
        runDiffTreeForChanges("57150c54c38d6570b2fd5e6d6cfc19476de44e83", "9fbc34a0d905950131d73f352abe68520c6db2a3");
    }

    @Test(expected = NoSuchCommitException.class)
    public void testNoSuchCommitForChangesPaged() {
        commandFactory.changes(repository,
                new ChangesCommandParameters.Builder()
                        .maxChanges(100)
                        .sinceId("9fbc34a0d905950131d73f352abe68520c6db2a3")
                        .untilId("57150c54c38d6570b2fd5e6d6cfc19476de44e83")
                        .build(),
                PageUtils.newRequest(0, 10)).call();
    }

    @Test(expected = NoSuchCommitException.class)
    public void testNoSuchCommitForChangesStreamed() {
        ChangeCallback callback = mock(ChangeCallback.class);

        try {
            commandFactory.changes(repository,
                    new ChangesCommandParameters.Builder()
                            .maxChanges(100)
                            .sinceId("9fbc34a0d905950131d73f352abe68520c6db2a3")
                            .untilId("57150c54c38d6570b2fd5e6d6cfc19476de44e83")
                            .build(),
                    callback).call();
        } finally {
            //If the command fails (and throwing NoSuchCommit should fail), ensure the callback is never used
            verifyZeroInteractions(callback);
        }
    }

    @Test(expected = NoSuchCommitException.class)
    public void testNoSuchCommitForChangesStreamedV2() {
        ChangeCallback callback = mock(ChangeCallback.class);

        try {
            commandFactory.changes(repository,
                    new ChangesCommandParameters.Builder()
                            .maxChanges(100)
                            .sinceId("9fbc34a0d905950131d73f352abe68520c6db2a3")
                            .untilId("57150c54c38d6570b2fd5e6d6cfc19476de44e83")
                            .build(),
                    callback).call();
        } finally {
            //If the command fails (and throwing NoSuchCommit should fail), ensure the callback is never used
            verifyZeroInteractions(callback);
        }
    }

    @Test(expected = NoSuchCommitException.class)
    public void testNoSuchCommitForChangesets() {
        commandFactory.changesets(repository,
                new ChangesetsCommandParameters.Builder()
                        .commitId("62abb7aa35f8928baa23be2e6cf5cbf1ca43dfdb")
                        .maxChangesPerCommit(5)
                        .build(),
                PageUtils.newRequest(0, 10)).call();
    }

    @Test
    public void testNoSuchCommitForChangesetsIgnoreMissing() {
        assertEquals(0, commandFactory.changesets(repository,
                new ChangesetsCommandParameters.Builder()
                        .commitId("62abb7aa35f8928baa23be2e6cf5cbf1ca43dfdb")
                        .maxChangesPerCommit(5)
                        .ignoreMissing(true)
                        .build(),
                PageUtils.newRequest(0, 10)).call().getSize());
    }

    @Test(expected = NoSuchCommitException.class)
    public void testNoSuchParent() {
        runDiffTreeForChanges("57150c54c38d6570b2fd5e6d6cfc19476de44e84", "9fbc34a0d905950131d73f352abe68520c6db2a4");
    }

    private GitDiffTreeBuilder createBuilder() {
        return createBuilder(repository);
    }

    private GitDiffTreeBuilder createBuilder(Repository repository) {
        return builderFactory.builder(repository).diffTree();
    }

    private Page<Change> runDiffTreeForChanges(String rev) {
        return runDiffTreeForChanges(rev, null, false);
    }

    private Page<Change> runDiffTreeForChanges(String rev, String ancestor) {
        return runDiffTreeForChanges(rev, ancestor, false);
    }

    private Page<Change> runDiffTreeForChanges(String rev, String ancestor, boolean findCopiesHarder) {
        ChangeDiffTreeOutputHandler outputHandler = new ChangeDiffTreeOutputHandler(PageUtils.newRequest(0, 10));

        GitDiffTreeBuilder builder = createBuilder()
                .renames(findCopiesHarder ? GitDiffRenames.FIND_COPIES_HARDER : GitDiffRenames.FIND_COPIES);
        if (StringUtils.isBlank(ancestor)) {
            builder.always(true)
                    .root(true);
        } else {
            builder.ancestor(ancestor);
        }
        return builder
                .recursive(true)
                .rev(rev)
                .build(outputHandler)
                .call();
    }
}
