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

import com.atlassian.bitbucket.commit.*;
import com.atlassian.bitbucket.commit.graph.CommitGraphNode;
import com.atlassian.bitbucket.commit.graph.TraversalCallback;
import com.atlassian.bitbucket.commit.graph.TraversalContext;
import com.atlassian.bitbucket.commit.graph.TraversalSummary;
import com.atlassian.bitbucket.content.*;
import com.atlassian.bitbucket.repository.*;
import com.atlassian.bitbucket.scm.*;
import com.atlassian.bitbucket.scm.annotation.Repo;
import com.atlassian.bitbucket.scm.git.GitAgent;
import com.atlassian.bitbucket.scm.git.command.GitCommandBuilderFactory;
import com.atlassian.bitbucket.scm.git.command.GitCommandBuilderSupport;
import com.atlassian.bitbucket.scm.git.command.GitCommandFactory;
import com.atlassian.bitbucket.user.SimplePerson;
import com.atlassian.bitbucket.util.Page;
import com.atlassian.bitbucket.util.PageUtils;
import com.atlassian.bitbucket.util.Version;
import com.atlassian.stash.internal.scm.git.command.foreachref.HeadsForEachRefOutputHandler;
import com.atlassian.stash.internal.scm.git.command.revlist.AbstractCommitRevListOutputHandler;
import com.atlassian.stash.internal.scm.git.util.ConfigurableGitScmConfig;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Iterables;
import com.google.common.io.Files;
import org.hamcrest.collection.IsIterableContainingInAnyOrder;
import org.hamcrest.collection.IsIterableContainingInOrder;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import org.mockito.ArgumentCaptor;
import org.mockito.InOrder;

import javax.annotation.Resource;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.stream.Stream;

import static com.atlassian.bitbucket.hamcrest.RepositoryMatchers.*;
import static com.atlassian.bitbucket.scm.git.command.GitCommandBuilderSupport.ENV_AUTHOR_DATE;
import static com.atlassian.bitbucket.scm.git.command.GitCommandBuilderSupport.ENV_COMMITTER_DATE;
import static com.atlassian.stash.internal.scm.git.GitFactoriesTestHelper.isBareRepository;
import static org.hamcrest.Matchers.emptyCollectionOf;
import static org.hamcrest.Matchers.hasSize;
import static org.mockito.Mockito.*;

@SuppressWarnings({"deprecation", "unchecked", "ConstantConditions"})
public class DefaultGitCommandFactoryTest extends AbstractConfiguredGitTest {

    @Rule
    public TemporaryFolder folder = new TemporaryFolder();

    @Resource
    private GitAgent agent;
    @Resource
    private GitCommandBuilderFactory builderFactory;
    @Resource
    private ConfigurableGitScmConfig config;
    @Resource
    private GitCommandFactory commandFactory;

    @Repo("classpath:git/merge.zip")
    private Repository merge;
    @Repo("classpath:git/merge-commits.zip")
    private Repository mergeCommits;
    @Repo("classpath:git/qa-resources.zip")
    private Repository qaResources;
    @Repo
    private Repository repository;

    @Test
    public void testBranches() throws Exception {
        BranchesCommandParameters params = new BranchesCommandParameters.Builder().build();
        Page<Branch> page = commandFactory.branches(repository, params, PageUtils.newRequest(0, 100)).call();
        assertNotNull(page);
        assertThat(page.getValues(), IsIterableContainingInAnyOrder.containsInAnyOrder(
                branch("basic_branching", "d6edcbf924697ab811a867421dab60d954ccad99", false),
                branch("branch_mod_merge", "6053a1eaa1c009dd11092d09a72f3c41af1b59ad", false),
                branch("master", "0a943a29376f2336b78312d99e65da17048951db", true),
                branch("out_of_order_branch", "2e10dd2d1d5eea9291b296e78312e8a703964a95", false)));
    }

    @Test
    public void testBranchesInAlphabeticalOrder() throws Exception {
        BranchesCommandParameters params = new BranchesCommandParameters.Builder().order(RefOrder.ALPHABETICAL).build();
        Page<Branch> page = commandFactory.branches(repository, params, PageUtils.newRequest(0, 100)).call();
        assertNotNull(page);
        assertThat(page.getValues(), IsIterableContainingInOrder.contains(
                branch("basic_branching", "d6edcbf924697ab811a867421dab60d954ccad99", false),
                branch("branch_mod_merge", "6053a1eaa1c009dd11092d09a72f3c41af1b59ad", false),
                branch("master", "0a943a29376f2336b78312d99e65da17048951db", true),
                branch("out_of_order_branch", "2e10dd2d1d5eea9291b296e78312e8a703964a95", false)));
    }

    @Test
    public void testBranchesInModificationOrder() throws Exception {
        BranchesCommandParameters params = new BranchesCommandParameters.Builder().order(RefOrder.MODIFICATION).build();
        Page<Branch> page = commandFactory.branches(repository, params, PageUtils.newRequest(0, 100)).call();
        assertNotNull(page);
        assertThat(page.getValues(), IsIterableContainingInOrder.contains(
                branch("master", "0a943a29376f2336b78312d99e65da17048951db", true),
                branch("branch_mod_merge", "6053a1eaa1c009dd11092d09a72f3c41af1b59ad", false),
                branch("basic_branching", "d6edcbf924697ab811a867421dab60d954ccad99", false),
                branch("out_of_order_branch", "2e10dd2d1d5eea9291b296e78312e8a703964a95", false)));
    }

    @Test
    public void testBranchesWithFilter() throws Exception {
        BranchesCommandParameters params = new BranchesCommandParameters.Builder().filterText("branch").build();
        Page<Branch> page = commandFactory.branches(repository, params, PageUtils.newRequest(0, 100)).call();
        assertNotNull(page);
        assertThat(page.getValues(), IsIterableContainingInAnyOrder.containsInAnyOrder(
                branch("basic_branching", "d6edcbf924697ab811a867421dab60d954ccad99", false),
                branch("branch_mod_merge", "6053a1eaa1c009dd11092d09a72f3c41af1b59ad", false),
                branch("out_of_order_branch", "2e10dd2d1d5eea9291b296e78312e8a703964a95", false)));
    }

    @Test
    public void testChanges() {
        Page<Change> page = commandFactory.changes(repository,
                new ChangesCommandParameters.Builder()
                        .untilId("6053a1eaa1c009dd11092d09a72f3c41af1b59ad")
                        .sinceId("042fb0cc198e7320b59a025689b45a2cb52f4684")
                        .build(),
                PageUtils.newRequest(0, 25))
                .call();
        assertNotNull(page);
        assertTrue(page.getIsLastPage());
        assertEquals(1, page.getSize());

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

    //STASHDEV-1208: Verifies displaying the diff-tree as a big ADD event for root changes (regressed by STASHDEV-441)
    @Test
    public void testChangesForRootCommit() {
        Command<Page<Change>> command = commandFactory.changes(repository,
                new ChangesCommandParameters.Builder()
                        .untilId("409a2180fd95b744df8d817e770b7f8c853f78dc")
                        .build(),
                PageUtils.newRequest(0, 25));
        Page<Change> page = command.call();
        assertNotNull(page);
        assertTrue(page.getIsLastPage());
        assertEquals(1, page.getSize());

        Change change = Iterables.getOnlyElement(page.getValues());
        assertEquals("30bda1ba1e8e99781b4fafcc450da84f97a29630", change.getContentId());
        assertEquals("0000000000000000000000000000000000000000", change.getFromContentId());
        assertEquals("out_of_order/file.txt", change.getPath().toString());
        assertEquals(-1, change.getPercentUnchanged());
        assertNull(change.getSrcPath());
        assertSame(ChangeType.ADD, change.getType());
    }

    @Test
    public void testChangesWithCopyAndFindHarder() throws Exception {
        runDiffWithStrategy("copies+", () -> {
            Page<Change> page = commandFactory.changes(repository,
                    new ChangesCommandParameters.Builder()
                            .sinceId("e0a3f6d8c804c375f49d7609328111e8dc75a4bc")
                            .untilId("0a943a29376f2336b78312d99e65da17048951db")
                            .build(),
                    PageUtils.newRequest(0, 25))
                    .call();
            assertNotNull(page);
            assertTrue(page.getIsLastPage());
            assertEquals(1, page.getSize());

            //This copy is only detected by --find-copies-harder since C.zip is not modified in the same commit
            Change change = Iterables.getOnlyElement(page.getValues());
            assertEquals("binary/D.zip", change.getPath().toString());
            assertEquals("binary/C.zip", change.getSrcPath().toString());
            assertEquals(100, change.getPercentUnchanged());
            assertSame(ChangeType.COPY, change.getType());
        });
    }

    @Test
    public void testChangesWithRename() throws Exception {
        runDiffWithStrategy("renames", () -> {
            Page<Change> page = commandFactory.changes(repository,
                    new ChangesCommandParameters.Builder()
                            .sinceId("9c05f43f859375e392d90d23a13717c16d0fdcda")
                            .untilId("2d8897c9ac29ce42c3442cf80ac977057045e7f6")
                            .build(),
                    PageUtils.newRequest(0, 25))
                    .call();
            assertNotNull(page);
            assertTrue(page.getIsLastPage());
            assertEquals(1, page.getSize());

            Change change = Iterables.getOnlyElement(page.getValues());
            assertEquals("binary/B.zip", change.getPath().toString());
            assertEquals("binary/A.zip", change.getSrcPath().toString());
            assertEquals(100, change.getPercentUnchanged());
            assertSame(ChangeType.MOVE, change.getType());
        });
    }

    @Test
    public void testChangesWithRenamesOff() throws Exception {
        runDiffWithStrategy("off", () -> {
            Page<Change> page = commandFactory.changes(repository,
                    new ChangesCommandParameters.Builder()
                            .sinceId("9c05f43f859375e392d90d23a13717c16d0fdcda")
                            .untilId("2d8897c9ac29ce42c3442cf80ac977057045e7f6")
                            .build(),
                    PageUtils.newRequest(0, 25))
                    .call();
            assertNotNull(page);
            assertTrue(page.getIsLastPage());
            assertEquals(2, page.getSize());

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

            Change first = changes.get(0);
            assertEquals("binary/A.zip", first.getPath().toString());
            assertNull(first.getSrcPath());
            assertSame(ChangeType.DELETE, first.getType());

            Change second = changes.get(1);
            assertEquals("binary/B.zip", second.getPath().toString());
            assertNull(second.getSrcPath());
            assertSame(ChangeType.ADD, second.getType());
        });
    }

    @Test
    public void testCommit() throws Exception {
        Commit commit = commandFactory.commit(repository,
                new CommitCommandParameters.Builder().commitId("0a943a29376f2336b78312d99e65da17048951db").build()).call();
        assertNotNull(commit);
        assertEquals(repository, commit.getRepository());
        assertEquals("0a943a29376f2336b78312d99e65da17048951db", commit.getId());
        assertEquals("Anna Buttfield", commit.getAuthor().getName());
        assertEquals("abuttfield@cascade.sydney.atlassian.com", commit.getAuthor().getEmailAddress());
        assertEquals("Copy C.zip as D.zip", commit.getMessage());
        assertEquals(1, commit.getParents().size());
        assertEquals("e0a3f6d8c804c375f49d7609328111e8dc75a4bc", commit.getParents().iterator().next().getId());

        commit = commandFactory.commit(repository,
                new CommitCommandParameters.Builder().commitId("409a2180fd95b744df8d817e770b7f8c853f78dc").path("out_of_order").build()).call();
        assertNotNull(commit);
        assertEquals(repository, commit.getRepository());
        assertEquals("409a2180fd95b744df8d817e770b7f8c853f78dc", commit.getId());
        assertEquals("Michael Heemskerk", commit.getAuthor().getName());
        assertEquals("mheemskerk@atlassian.com", commit.getAuthor().getEmailAddress());
        assertEquals("out of order commits scenario, initial commit", commit.getMessage());
        assertThat(commit.getParents(), emptyCollectionOf(MinimalCommit.class));
    }

    @Test
    public void testCommitsPagedExcludeMerges() {
        Page<Commit> page = commandFactory.commits(mergeCommits, new CommitsCommandParameters.Builder()
                .merges(CommitListMergeFilter.EXCLUDE)
                .build(), PageUtils.newRequest(0, 10)).call();
        assertNotNull(page);

        List<Commit> commits = ImmutableList.copyOf(page.getValues());
        assertNotNull(commits);
        assertEquals(7, commits.size());

        assertEquals("8dd91d1f6d846ec80045699d7ad1ee85395e22ef", commits.get(0).getId());
        assertEquals("2061acf612f3b1f4ac6d50e99688f41fbbb6c59c", commits.get(1).getId());
        assertEquals("88221b3b195a998a4bf3aecbdb02f9efd69103ae", commits.get(2).getId());
        assertEquals("2d8b41bf8c4a216177b5303f845c226232f454f0", commits.get(3).getId());
        assertEquals("af7ad1f16d32ad11f44bbad1236808d405eb48f9", commits.get(4).getId());
        assertEquals("29646867dfdedb2b139c13b1f383d8f6134a3624", commits.get(5).getId());
        assertEquals("274e479b517365266ed43bf83678b28c2862f2b5", commits.get(6).getId());
    }

    @Test
    public void testCommitsPagedIncludeMerges() {
        Page<Commit> page = commandFactory.commits(mergeCommits, new CommitsCommandParameters.Builder()
                .merges(CommitListMergeFilter.INCLUDE)
                .build(), PageUtils.newRequest(0, 20)).call();
        assertNotNull(page);

        List<Commit> commits = ImmutableList.copyOf(page.getValues());
        assertNotNull(commits);
        assertEquals(12, commits.size());

        assertEquals("e1c0ebbac1079a14b14fb05e67339ffbcca1894c", commits.get(0).getId());
        assertEquals("8dd91d1f6d846ec80045699d7ad1ee85395e22ef", commits.get(1).getId());
        assertEquals("2061acf612f3b1f4ac6d50e99688f41fbbb6c59c", commits.get(2).getId());
        assertEquals("d62cf48ad69b3b6957ab4d5ffdb76a24a2e2d1e6", commits.get(3).getId());
        assertEquals("6d7f1f78419d3dda0e5374d4d6b630e8fc7902ac", commits.get(4).getId());
        assertEquals("4856ffa5289244f02c017100d0d24d92d8df7888", commits.get(5).getId());
        assertEquals("d73fb0b2b117b3f92f9b0160d413b9d8299e2034", commits.get(6).getId());
        assertEquals("88221b3b195a998a4bf3aecbdb02f9efd69103ae", commits.get(7).getId());
        assertEquals("2d8b41bf8c4a216177b5303f845c226232f454f0", commits.get(8).getId());
        assertEquals("af7ad1f16d32ad11f44bbad1236808d405eb48f9", commits.get(9).getId());
        assertEquals("29646867dfdedb2b139c13b1f383d8f6134a3624", commits.get(10).getId());
        assertEquals("274e479b517365266ed43bf83678b28c2862f2b5", commits.get(11).getId());
    }

    @Test
    public void testCommitsPagedOnlyMerges() {
        Page<Commit> page = commandFactory.commits(mergeCommits, new CommitsCommandParameters.Builder()
                .merges(CommitListMergeFilter.ONLY)
                .build(), PageUtils.newRequest(0, 10)).call();
        assertNotNull(page);

        List<Commit> commits = ImmutableList.copyOf(page.getValues());
        assertNotNull(commits);
        assertEquals(5, commits.size());

        assertEquals("e1c0ebbac1079a14b14fb05e67339ffbcca1894c", commits.get(0).getId());
        assertEquals("d62cf48ad69b3b6957ab4d5ffdb76a24a2e2d1e6", commits.get(1).getId());
        assertEquals("6d7f1f78419d3dda0e5374d4d6b630e8fc7902ac", commits.get(2).getId());
        assertEquals("4856ffa5289244f02c017100d0d24d92d8df7888", commits.get(3).getId());
        assertEquals("d73fb0b2b117b3f92f9b0160d413b9d8299e2034", commits.get(4).getId());
    }

    //Verifies that out-of-order commits, generally caused in the wild by out-of-sync clocks on user computers
    //and servers, produce the correct output
    @Test
    public void testCommitByIdWithOutOfOrderDates() throws Exception {
        runCommitWithOutOfOrderDates(false);
    }

    //Verifies that out-of-order commits, generally caused in the wild by out-of-sync clocks on user computers
    //and servers, produce the correct output
    @Test
    public void testCommitByNameWithOutOfOrderDates() throws Exception {
        runCommitWithOutOfOrderDates(true);
    }

    @Test
    public void testCommitsPaged() throws Exception {
        Page<Commit> page = commandFactory.commits(repository, new CommitsCommandParameters.Builder()
                .exclude("e0a3f6d8c804c375f49d7609328111e8dc75a4bc")
                .include("0a943a29376f2336b78312d99e65da17048951db")
                .build(), PageUtils.newRequest(0, 100)).call();
        assertNotNull(page);
        assertEquals(1, page.getSize());

        Commit commit = page.getValues().iterator().next();
        assertEquals(repository, commit.getRepository());
        assertEquals("0a943a29376f2336b78312d99e65da17048951db", commit.getId());
        assertEquals("Anna Buttfield", commit.getAuthor().getName());
        assertEquals("abuttfield@cascade.sydney.atlassian.com", commit.getAuthor().getEmailAddress());
        assertEquals("Copy C.zip as D.zip", commit.getMessage());
        assertEquals(1, commit.getParents().size());
        assertEquals("e0a3f6d8c804c375f49d7609328111e8dc75a4bc", commit.getParents().iterator().next().getId());
    }

    @Test
    public void testCommitsPagedFollowingMultipleCopiesFromUnicodePath() throws Exception {
        Page<Commit> page = commandFactory.commits(qaResources, new CommitsCommandParameters.Builder()
                .followRenames(true)
                .path("国際交流基金海外運営専門員.txt")
                .build(), PageUtils.newRequest(0, 10)).call();
        assertNotNull(page);

        List<Commit> commits = ImmutableList.copyOf(page.getValues());

        //In 7c85f8acb2282e3ed108c46b59fd5daa78bf17db, which first shipped in Git 2.0.0, diffcore-rename.c was
        //updated and the algorithm used by find_exact_renames was changed. That change manifests here, with
        //versions before 2.0.0 finding 3 files in the history for 国際交流基金海外運営専門員.txt and 2.0.0+ finding
        //5. As this is the behavior of Git itself changing, both sets of results must be considered "correct".
        //
        //This changed again in Git 2.8.2 (see commit ca4e3ca029def86b7b29451d4ba77ef76574949e), reverting the
        //above mentioned change in behaviour.
        Version version = config.getVersion();
        if (version.compareTo(new Version(2)) < 0 || version.compareTo(new Version(2, 8, 1)) > 0) {
            assertEquals(3, commits.size());
            assertEquals(3, page.getSize());
            assertTrue(page.getIsLastPage());

            Commit commit = commits.get(0);
            assertEquals("d86c2faf589df293bf8f891c8f116a454fb59881", commit.getId());
            assertFollowProperties(commit, ChangeType.COPY, "国際交流基金海外運営専門員.txt",
                    "SourceView/%E2 t%EAst f%EDl%E9.txt", 98);

            commit = commits.get(1);
            assertEquals("2d44e0593487b151006b80cc0ab300e3f114b91e", commit.getId());
            assertFollowProperties(commit, ChangeType.COPY, "SourceView/%E2 t%EAst f%EDl%E9.txt",
                    "SourceView/intlstrings_20001lines.txt", 100);

            commit = commits.get(2);
            assertEquals("7f26ba7c44d354e81d9ab7bf67239e8a83c050b2", commit.getId());
            assertFollowProperties(commit, ChangeType.ADD, "SourceView/intlstrings_20001lines.txt");
        } else {
            assertEquals(5, commits.size());
            assertEquals(5, page.getSize());
            assertTrue(page.getIsLastPage());

            Commit commit = commits.get(0);
            assertEquals("d86c2faf589df293bf8f891c8f116a454fb59881", commit.getId());
            assertFollowProperties(commit, ChangeType.COPY, "国際交流基金海外運営専門員.txt",
                    "SourceView/%E2 t%EAst f%EDl%E9.txt", 98);

            commit = commits.get(1);
            assertEquals("2d44e0593487b151006b80cc0ab300e3f114b91e", commit.getId());
            assertFollowProperties(commit, ChangeType.COPY, "SourceView/%E2 t%EAst f%EDl%E9.txt",
                    "SourceView/âtêstfílé.txt", 100);

            commit = commits.get(2);
            assertEquals("a40809802903fc5c0016355ab63667f7608b98f9", commit.getId());
            assertFollowProperties(commit, ChangeType.COPY, "SourceView/âtêstfílé.txt",
                    "SourceView/â têst fílé.txt", 100);

            commit = commits.get(3);
            assertEquals("148d906c73c89f2ec78cfc20ea0832f5b5bbd492", commit.getId());
            assertFollowProperties(commit, ChangeType.COPY, "SourceView/â têst fílé.txt",
                    "SourceView/intlstrings_20001lines.txt", 100);

            commit = commits.get(4);
            assertEquals("7f26ba7c44d354e81d9ab7bf67239e8a83c050b2", commit.getId());
            assertFollowProperties(commit, ChangeType.ADD, "SourceView/intlstrings_20001lines.txt");
        }
    }

    @Test
    public void testCommitsPagedFollowingRename() throws Exception {
        Page<Commit> page = commandFactory.commits(repository, new CommitsCommandParameters.Builder()
                .followRenames(true)
                .path("binary/B.zip")
                .build(), PageUtils.newRequest(0, 100)).call();
        assertNotNull(page);
        assertEquals(2, page.getSize());

        List<Commit> commits = ImmutableList.copyOf(page.getValues());
        assertEquals(2, commits.size());

        Commit commit = commits.get(0);
        assertEquals(repository, commit.getRepository());
        assertEquals("2d8897c9ac29ce42c3442cf80ac977057045e7f6", commit.getId());
        assertEquals("Anna Buttfield", commit.getAuthor().getName());
        assertEquals("abuttfield@cascade.sydney.atlassian.com", commit.getAuthor().getEmailAddress());
        assertEquals("Rename A.zip to B.zip", commit.getMessage());
        assertEquals(1, commit.getParents().size());
        assertEquals("9c05f43f859375e392d90d23a13717c16d0fdcda", commit.getParents().iterator().next().getId());
        assertFollowProperties(commit, ChangeType.MOVE, "binary/B.zip", "binary/A.zip", 100);

        commit = commits.get(1);
        assertEquals(repository, commit.getRepository());
        assertEquals("9c05f43f859375e392d90d23a13717c16d0fdcda", commit.getId());
        assertEquals("Anna Buttfield", commit.getAuthor().getName());
        assertEquals("abuttfield@cascade.sydney.atlassian.com", commit.getAuthor().getEmailAddress());
        assertEquals("Add binary file A.zip", commit.getMessage());
        assertEquals(1, commit.getParents().size());
        assertEquals("6053a1eaa1c009dd11092d09a72f3c41af1b59ad", commit.getParents().iterator().next().getId());
        assertFollowProperties(commit, ChangeType.ADD, "binary/A.zip");
    }

    @Test(expected = NoSuchCommitException.class)
    public void testCommitsPagedFollowingRenameBadCommit() throws Exception {
        commandFactory.commits(repository, new CommitsCommandParameters.Builder()
                .followRenames(true)
                .include("bad")
                .path("binary/B.zip")
                .build(), PageUtils.newRequest(0, 100)).call();
    }

    @Test
    public void testCommitsPagedFollowingRenameBadCommitIgnoreMissing() throws Exception {
        Page<Commit> page = commandFactory.commits(repository, new CommitsCommandParameters.Builder()
                .followRenames(true)
                .ignoreMissing(true)
                .include("bad")
                .path("binary/B.zip")
                .build(), PageUtils.newRequest(0, 100)).call();
        assertNotNull(page);
        assertEquals(2, page.getSize());

        List<Commit> commits = ImmutableList.copyOf(page.getValues());
        assertEquals(2, commits.size());

        Commit commit = commits.get(0);
        assertEquals(repository, commit.getRepository());
        assertEquals("2d8897c9ac29ce42c3442cf80ac977057045e7f6", commit.getId());
        assertEquals("Anna Buttfield", commit.getAuthor().getName());
        assertEquals("abuttfield@cascade.sydney.atlassian.com", commit.getAuthor().getEmailAddress());
        assertEquals("Rename A.zip to B.zip", commit.getMessage());
        assertEquals(1, commit.getParents().size());
        assertEquals("9c05f43f859375e392d90d23a13717c16d0fdcda", commit.getParents().iterator().next().getId());
        assertFollowProperties(commit, ChangeType.MOVE, "binary/B.zip", "binary/A.zip", 100);

        commit = commits.get(1);
        assertEquals(repository, commit.getRepository());
        assertEquals("9c05f43f859375e392d90d23a13717c16d0fdcda", commit.getId());
        assertEquals("Anna Buttfield", commit.getAuthor().getName());
        assertEquals("abuttfield@cascade.sydney.atlassian.com", commit.getAuthor().getEmailAddress());
        assertEquals("Add binary file A.zip", commit.getMessage());
        assertEquals(1, commit.getParents().size());
        assertEquals("6053a1eaa1c009dd11092d09a72f3c41af1b59ad", commit.getParents().iterator().next().getId());
        assertFollowProperties(commit, ChangeType.ADD, "binary/A.zip");
    }

    @Test
    public void testCommitsStreamed() throws Exception {
        CommitCallback callback = mock(CommitCallback.class);
        when(callback.onCommit(isNotNull(Commit.class))).thenReturn(true); //Return true to not get canceled

        commandFactory.commits(repository, new CommitsCommandParameters.Builder()
                .exclude("e0a3f6d8c804c375f49d7609328111e8dc75a4bc")
                .include("0a943a29376f2336b78312d99e65da17048951db")
                .build(), callback).call();

        ArgumentCaptor<Commit> commitCaptor = ArgumentCaptor.forClass(Commit.class);
        ArgumentCaptor<CommitSummary> summaryCaptor = ArgumentCaptor.forClass(CommitSummary.class);

        InOrder ordered = inOrder(callback);
        ordered.verify(callback).onStart(isNotNull(CommitContext.class));
        ordered.verify(callback).onCommit(commitCaptor.capture());
        ordered.verify(callback).onEnd(summaryCaptor.capture());
        ordered.verifyNoMoreInteractions();

        Commit commit = commitCaptor.getValue();
        assertEquals(repository, commit.getRepository());
        assertEquals("0a943a29376f2336b78312d99e65da17048951db", commit.getId());
        assertEquals("Anna Buttfield", commit.getAuthor().getName());
        assertEquals("abuttfield@cascade.sydney.atlassian.com", commit.getAuthor().getEmailAddress());
        assertEquals("Copy C.zip as D.zip", commit.getMessage());
        assertEquals(1, commit.getParents().size());
        assertEquals("e0a3f6d8c804c375f49d7609328111e8dc75a4bc", commit.getParents().iterator().next().getId());

        CommitSummary summary = summaryCaptor.getValue();
        assertNotNull(summary);
        assertSame(CommandResult.SUCCEEDED, summary.getResult());
    }

    @Test
    public void testCommitsStreamedFollowingCopy() throws Exception {
        CommitCallback callback = mock(CommitCallback.class);
        when(callback.onCommit(isNotNull(Commit.class))).thenReturn(true); //Return true to not get canceled

        commandFactory.commits(repository, new CommitsCommandParameters.Builder()
                .followRenames(true)
                .path("binary/D.zip")
                .build(), callback).call();

        ArgumentCaptor<Commit> commitCaptor = ArgumentCaptor.forClass(Commit.class);
        ArgumentCaptor<CommitSummary> summaryCaptor = ArgumentCaptor.forClass(CommitSummary.class);

        InOrder ordered = inOrder(callback);
        ordered.verify(callback).onStart(isNotNull(CommitContext.class));
        ordered.verify(callback, times(2)).onCommit(commitCaptor.capture());
        ordered.verify(callback).onEnd(summaryCaptor.capture());
        ordered.verifyNoMoreInteractions();

        List<Commit> commits = commitCaptor.getAllValues();
        assertEquals(2, commits.size());

        Commit commit = commits.get(0);
        assertEquals(repository, commit.getRepository());
        assertEquals("0a943a29376f2336b78312d99e65da17048951db", commit.getId());
        assertEquals("Anna Buttfield", commit.getAuthor().getName());
        assertEquals("abuttfield@cascade.sydney.atlassian.com", commit.getAuthor().getEmailAddress());
        assertEquals("Copy C.zip as D.zip", commit.getMessage());
        assertEquals(1, commit.getParents().size());
        assertEquals("e0a3f6d8c804c375f49d7609328111e8dc75a4bc", commit.getParents().iterator().next().getId());
        assertFollowProperties(commit, ChangeType.COPY, "binary/D.zip", "binary/C.zip", 100);

        commit = commits.get(1);
        assertEquals(repository, commit.getRepository());
        assertEquals("e0a3f6d8c804c375f49d7609328111e8dc75a4bc", commit.getId());
        assertEquals("Anna Buttfield", commit.getAuthor().getName());
        assertEquals("abuttfield@cascade.sydney.atlassian.com", commit.getAuthor().getEmailAddress());
        assertEquals("Add binary file C.zip", commit.getMessage());
        assertEquals(1, commit.getParents().size());
        assertEquals("2d8897c9ac29ce42c3442cf80ac977057045e7f6", commit.getParents().iterator().next().getId());
        assertFollowProperties(commit, ChangeType.ADD, "binary/C.zip");

        CommitSummary summary = summaryCaptor.getValue();
        assertNotNull(summary);
        assertSame(CommandResult.SUCCEEDED, summary.getResult());
    }

    @Test
    public void testCommitsStreamedOnCancel() throws Exception {
        CommitCallback callback = mock(CommitCallback.class); //onCommit will return false by default
        commandFactory.commits(repository, new CommitsCommandParameters.Builder()
                .include("0a943a29376f2336b78312d99e65da17048951db").exclude("e0a3f6d8c804c375f49d7609328111e8dc75a4bc")
                .build(), callback).call();

        ArgumentCaptor<Commit> commitCaptor = ArgumentCaptor.forClass(Commit.class);
        ArgumentCaptor<CommitSummary> summaryCaptor = ArgumentCaptor.forClass(CommitSummary.class);

        InOrder ordered = inOrder(callback);
        ordered.verify(callback).onStart(isNotNull(CommitContext.class));
        ordered.verify(callback).onCommit(commitCaptor.capture());
        ordered.verify(callback).onEnd(summaryCaptor.capture());
        ordered.verifyNoMoreInteractions();

        Commit commit = commitCaptor.getValue();
        assertEquals(repository, commit.getRepository());
        assertEquals("0a943a29376f2336b78312d99e65da17048951db", commit.getId());
        assertEquals("Anna Buttfield", commit.getAuthor().getName());
        assertEquals("abuttfield@cascade.sydney.atlassian.com", commit.getAuthor().getEmailAddress());
        assertEquals("Copy C.zip as D.zip", commit.getMessage());
        assertEquals(1, commit.getParents().size());
        assertEquals("e0a3f6d8c804c375f49d7609328111e8dc75a4bc", commit.getParents().iterator().next().getId());

        CommitSummary summary = summaryCaptor.getValue();
        assertNotNull(summary);
        assertSame(CommandResult.CANCELED, summary.getResult());
    }

    @Test
    public void testCommitsWithBadRefAndIgnoreMissing() throws Exception {
        assertCommits(true, "bad"); //no exception
    }

    @Test
    public void testCommitsWithBadRefAndIgnoreMissingGiveEmptyPage() throws Exception {
        Page<Commit> page = commandFactory.commits(repository, new CommitsCommandParameters.Builder()
                .ignoreMissing(true)
                .include("bad")
                .build(), PageUtils.newRequest(0, 100)).call();
        assertEquals(0, page.getSize());
    }

    @Test(expected = NoSuchCommitException.class)
    public void testCommitsWithBadRefAndAutoIgnoreMissing() throws Exception {
        assertCommits(null, "bad");
    }

    @Test(expected = NoSuchCommitException.class)
    public void testCommitsWithBadRefAndNotIgnoreMissing() throws Exception {
        assertCommits(false, "bad");
    }

    @Test(expected = NoSuchCommitException.class)
    public void testCommitsWithBadRefs() throws Exception {
        assertCommits(null, "bad", "worse"); //triggers --ignore-missing
    }

    @Test
    public void testCommitsWithBadAndGoodRef() throws Exception {
        assertCommits(null, "bad", "e0a3f6d8c804c375f49d7609328111e8dc75a4bc");
    }

    @Test
    public void testCommitsWithBadAndGoodRefWithExcludesIgnoreMissing() throws Exception {
        Page<Commit> page = commandFactory.commits(repository, new CommitsCommandParameters.Builder()
                .ignoreMissing(true)
                .include("e0a3f6d8c804c375f49d7609328111e8dc75a4bc")
                .exclude("bad")
                .build(), PageUtils.newRequest(0, 100)).call();
        assertEquals(19, page.getSize());
    }

    @Test
    public void testCommitsWithBadAndGoodRefWithExcludes() throws Exception {
        Page<Commit> page = commandFactory.commits(repository, new CommitsCommandParameters.Builder()
                .ignoreMissing(null)
                .include("e0a3f6d8c804c375f49d7609328111e8dc75a4bc")
                .exclude("bad")
                .build(), PageUtils.newRequest(0, 100)).call();
        assertEquals(19, page.getSize());
    }

    @Test(expected = NoSuchCommitException.class)
    public void testCommitsWithBadAndGoodRefWithExcludesNoIgnoreMissing() throws Exception {
        commandFactory.commits(repository, new CommitsCommandParameters.Builder()
                .ignoreMissing(false)
                .include("e0a3f6d8c804c375f49d7609328111e8dc75a4bc")
                .exclude("bad")
                .build(), PageUtils.newRequest(0, 100)).call();
    }

    @Test
    public void testCommitsWithBadAndGoodRefs() throws Exception {
        assertCommits(null, "0a943a29376f2336b78312d99e65da17048951db", "bad", "e0a3f6d8c804c375f49d7609328111e8dc75a4bc", "worse");
    }

    @Test
    public void testCreate() throws Exception {
        Repository repo = mock(Repository.class);
        when(repo.getHierarchyId()).thenReturn("create");
        when(repo.getId()).thenReturn(10000);
        when(repo.getProject()).thenReturn(repository.getProject());
        when(repo.getSlug()).thenReturn("create-repo");

        config.setRepositoryDir(repo, folder.getRoot());

        commandFactory.create(repo).call();
        assertTrue(isBareRepository(folder.getRoot()));
    }

    @Test
    public void testDefaultBranch() throws Exception {
        Branch branch = commandFactory.defaultBranch(repository).call();
        assertEquals(agent.resolveHead(repository), branch);
    }

    @Test
    public void testDiffWithFindCopiesHarder() throws Exception {
        runDiffWithStrategy("copy+", () -> {
            DiffCommandParameters params = new DiffCommandParameters.Builder()
                    .sinceId("e0a3f6d8c804c375f49d7609328111e8dc75a4bc")
                    .untilId("0a943a29376f2336b78312d99e65da17048951db")
                    .maxLineLength(25)
                    .maxLines(100)
                    .build();
            DiffContentCallback callback = mock(DiffContentCallback.class);

            commandFactory.diff(repository, params, callback).call();

            //This copy is only detected by --find-copies-harder since C.zip is not modified in the same commit
            InOrder ordered = inOrder(callback);
            ordered.verify(callback).onDiffStart(eq(new SimplePath("binary/C.zip")),
                    eq(new SimplePath("binary/D.zip")));
            ordered.verify(callback).onDiffEnd(eq(false));
            ordered.verify(callback).onEnd(isNotNull(DiffSummary.class));
            ordered.verifyNoMoreInteractions();
        });
    }

    @Test
    public void testDiffWithFindRenames() throws Exception {
        runDiffWithStrategy("rename", () -> {
            DiffCommandParameters params = new DiffCommandParameters.Builder()
                    .sinceId("9c05f43f859375e392d90d23a13717c16d0fdcda")
                    .untilId("2d8897c9ac29ce42c3442cf80ac977057045e7f6")
                    .maxLineLength(25)
                    .maxLines(100)
                    .build();
            DiffContentCallback callback = mock(DiffContentCallback.class);

            commandFactory.diff(repository, params, callback).call();

            InOrder ordered = inOrder(callback);
            ordered.verify(callback).onDiffStart(eq(new SimplePath("binary/A.zip")),
                    eq(new SimplePath("binary/B.zip")));
            ordered.verify(callback).onDiffEnd(eq(false));
            ordered.verify(callback).onEnd(isNotNull(DiffSummary.class));
            ordered.verifyNoMoreInteractions();
        });
    }

    @Test
    public void testDiffWithRenamesOff() throws Exception {
        runDiffWithStrategy("off", () -> {
            DiffCommandParameters params = new DiffCommandParameters.Builder()
                    .sinceId("9c05f43f859375e392d90d23a13717c16d0fdcda")
                    .untilId("2d8897c9ac29ce42c3442cf80ac977057045e7f6")
                    .maxLineLength(25)
                    .maxLines(100)
                    .build();
            DiffContentCallback callback = mock(DiffContentCallback.class);

            commandFactory.diff(repository, params, callback).call();

            //With rename detection turned off, the diff should contain a binary delete and a binary add
            InOrder ordered = inOrder(callback);
            ordered.verify(callback).onBinary(eq(new SimplePath("binary/A.zip")), isNull(Path.class));
            ordered.verify(callback).onBinary(isNull(Path.class), eq(new SimplePath("binary/B.zip")));
            ordered.verify(callback).onEnd(isNotNull(DiffSummary.class));
            ordered.verifyNoMoreInteractions();
        });
    }

    @Test
    public void testDiffWithWhitespaceIgnored() {
        // We don't care about the parsing, just that the whitespace command doesn't blow up
        commandFactory.diff(repository, new DiffCommandParameters.Builder()
                .untilId("0a943a29376f2336b78312d99e65da17048951db")
                .whitespace(DiffWhitespace.IGNORE_ALL)
                .build(), mock(DiffContentCallback.class)).call();
    }

    @Test
    public void testDirectory() throws Exception {
        ContentTreeCallback callback = mock(ContentTreeCallback.class);
        when(callback.onTreeNode(isA(Directory.class))).thenReturn(true, true, false); //Cancel after three

        //Listing master: on the sanity repository will not collapse any entries, but it will still exercise
        //the majority of the DirectoryCommand processing
        commandFactory.directory(repository, new DirectoryCommandParameters.Builder()
                .commitId("master")
                .build(), callback, PageUtils.newRequest(0, 500))
                .call();

        ArgumentCaptor<ContentTreeNode> nodeCaptor = ArgumentCaptor.forClass(ContentTreeNode.class);

        InOrder ordered = inOrder(callback);
        ordered.verify(callback).onStart(isNotNull(ContentTreeContext.class));
        ordered.verify(callback, times(3)).onTreeNode(nodeCaptor.capture());
        ordered.verify(callback).onEnd(isNotNull(ContentTreeSummary.class));

        List<ContentTreeNode> nodes = nodeCaptor.getAllValues();
        assertEquals(3, nodes.size());
        assertEquals("add_file", nodes.get(0).getPath().toString());
        assertEquals("basic_branching", nodes.get(1).getPath().toString());
        assertEquals("binary", nodes.get(2).getPath().toString());
        for (ContentTreeNode node : nodes) {
            assertSame("All entries should have directory type", ContentTreeNode.Type.DIRECTORY, node.getType());
            assertTrue("All entries should be directories", node instanceof Directory);
        }
    }

    @Test
    public void testDirectoryCollapsesEntries() throws Exception {
        ContentTreeCallback callback = mock(ContentTreeCallback.class);
        when(callback.onTreeNode(isA(Directory.class))).thenReturn(true);

        commandFactory.directory(qaResources, new DirectoryCommandParameters.Builder()
                .commitId("master")
                .path("Calculator/src")
                .build(), callback, PageUtils.newRequest(0, 500))
                .call();

        ArgumentCaptor<ContentTreeNode> nodeCaptor = ArgumentCaptor.forClass(ContentTreeNode.class);

        InOrder ordered = inOrder(callback);
        ordered.verify(callback).onStart(isNotNull(ContentTreeContext.class));
        ordered.verify(callback, times(2)).onTreeNode(nodeCaptor.capture());
        ordered.verify(callback).onEnd(isNotNull(ContentTreeSummary.class));

        List<ContentTreeNode> nodes = nodeCaptor.getAllValues();
        assertEquals(2, nodes.size());
        assertEquals("main/java/org/farmas", nodes.get(0).getPath().toString());
        assertEquals("test/java/org", nodes.get(1).getPath().toString());
        for (ContentTreeNode node : nodes) {
            assertSame("All entries should have directory type", ContentTreeNode.Type.DIRECTORY, node.getType());
            assertTrue("All entries should be directories", node instanceof Directory);
        }
    }

    @Test
    public void testDirectoryCollapsesUnicodeEntries() throws Exception {
        ContentTreeCallback callback = mock(ContentTreeCallback.class);
        when(callback.onTreeNode(isA(Directory.class))).thenReturn(true);

        commandFactory.directory(qaResources, new DirectoryCommandParameters.Builder()
                .commitId("collapse-unicode")
                .path("国際交流基金海外運営専門員/国際交流基金海外運営専門員/国際交流基金海外運営専門員")
                .build(), callback, PageUtils.newRequest(0, 500))
                .call();

        ArgumentCaptor<ContentTreeNode> nodeCaptor = ArgumentCaptor.forClass(ContentTreeNode.class);

        InOrder ordered = inOrder(callback);
        ordered.verify(callback).onStart(isNotNull(ContentTreeContext.class));
        ordered.verify(callback, times(2)).onTreeNode(nodeCaptor.capture());
        ordered.verify(callback).onEnd(isNotNull(ContentTreeSummary.class));

        List<ContentTreeNode> nodes = nodeCaptor.getAllValues();
        assertEquals(2, nodes.size());

        ContentTreeNode directory = nodes.get(0);
        assertEquals("国/際/交流/基金海外/運営専門員", directory.getPath().toString());
        assertSame(ContentTreeNode.Type.DIRECTORY, directory.getType());
        assertTrue(directory instanceof Directory);

        ContentTreeNode file = nodes.get(1);
        assertEquals("国際交流基金海外運.txt", file.getPath().toString());
        assertSame(ContentTreeNode.Type.FILE, file.getType());
        assertTrue(com.atlassian.bitbucket.content.File.class.isInstance(file)); //instanceof doesn't work with FQNs
    }

    @Test
    public void testFileV2() throws Exception {
        FileContentCallback callback = mockFileContentCallback(FileContentCallback.class);
        FileCommandParameters parameters = new FileCommandParameters.Builder()
                .commitId("master")
                .maxLineLength(15)
                .path("modification/mod_file.txt")
                .build();

        commandFactory.file(repository, parameters, callback, PageUtils.newRequest(0, 10)).call();

        InOrder ordered = inOrder(callback);
        ordered.verify(callback).onStart(isNotNull(FileContext.class));
        ordered.verify(callback).onLine(eq(1), eq("this is a test "), eq(true));
        ordered.verify(callback).onLine(eq(2), eq("another line"), eq(false));
        ordered.verify(callback).onEnd(isNotNull(FileSummary.class));
        ordered.verifyNoMoreInteractions();
    }

    @Test
    public void testFilePaging() throws Exception {
        FileContentCallback callback = mockFileContentCallback(FileContentCallback.class);
        FileCommandParameters parameters = new FileCommandParameters.Builder()
                .commitId("master")
                .maxLineLength(15)
                .path("modification/mod_file.txt")
                .build();

        ArgumentCaptor<FileSummary> summaryCaptor = ArgumentCaptor.forClass(FileSummary.class);

        commandFactory.file(repository, parameters, callback, PageUtils.newRequest(0, 1)).call();

        InOrder ordered = inOrder(callback);
        ordered.verify(callback).onStart(isNotNull(FileContext.class));
        ordered.verify(callback).onLine(eq(1), eq("this is a test "), eq(true));
        ordered.verify(callback).onEnd(summaryCaptor.capture());
        ordered.verifyNoMoreInteractions();

        FileSummary summary = summaryCaptor.getValue();
        assertEquals(1, summary.getPageRequest().getLimit());
        assertEquals(0, summary.getPageRequest().getStart());
        assertEquals(1, summary.getSize());
        assertFalse(summary.isLastPage());

        callback = mockFileContentCallback(FileContentCallback.class);
        summaryCaptor.getAllValues().clear();

        commandFactory.file(repository, parameters, callback, PageUtils.newRequest(1, 1)).call();

        ordered = inOrder(callback);
        ordered.verify(callback).onStart(isNotNull(FileContext.class));
        ordered.verify(callback).onLine(eq(2), eq("another line"), eq(false));
        ordered.verify(callback).onEnd(summaryCaptor.capture());
        ordered.verifyNoMoreInteractions();

        summary = summaryCaptor.getValue();
        assertEquals(1, summary.getPageRequest().getLimit());
        assertEquals(1, summary.getPageRequest().getStart());
        assertEquals(1, summary.getSize());
        assertEquals(1, summary.getSize());
        assertTrue(summary.isLastPage());
    }

    @SuppressWarnings("unchecked")
    @Test
    public void testFileWithBlame() throws Exception {
        FileContentCallback callback = mockFileContentCallback(FileContentCallback.class);
        FileCommandParameters parameters = new FileCommandParameters.Builder()
                .annotate(true)
                .commitId("master")
                .maxLineLength(15)
                .path("modification/mod_file.txt")
                .build();

        commandFactory.file(repository, parameters, callback, PageUtils.newRequest(0, 10)).call();

        InOrder ordered = inOrder(callback);
        ordered.verify(callback).onStart(isNotNull(FileContext.class));
        ordered.verify(callback).onLine(eq(1), eq("this is a test "), eq(true));
        ordered.verify(callback).onLine(eq(2), eq("another line"), eq(false));
        ordered.verify(callback).offerBlame((List) argThat(hasSize(2))); //Exact blame tested in BlameCommandTest
        ordered.verify(callback).onEnd(isNotNull(FileSummary.class));
        ordered.verifyNoMoreInteractions();
    }

    @SuppressWarnings("unchecked")
    @Test
    public void testFileWithBlamePaging() throws Exception {
        FileContentCallback callback = mockFileContentCallback(FileContentCallback.class);
        FileCommandParameters parameters = new FileCommandParameters.Builder()
                .annotate(true)
                .commitId("master")
                .maxLineLength(15)
                .path("modification/mod_file.txt")
                .build();

        ArgumentCaptor<FileSummary> summaryCaptor = ArgumentCaptor.forClass(FileSummary.class);

        commandFactory.file(repository, parameters, callback, PageUtils.newRequest(0, 1)).call();

        InOrder ordered = inOrder(callback);
        ordered.verify(callback).onStart(isNotNull(FileContext.class));
        ordered.verify(callback).onLine(eq(1), eq("this is a test "), eq(true));
        ordered.verify(callback).offerBlame((List) argThat(hasSize(1)));
        ordered.verify(callback).onEnd(summaryCaptor.capture());
        ordered.verifyNoMoreInteractions();

        FileSummary summary = summaryCaptor.getValue();
        assertEquals(1, summary.getPageRequest().getLimit());
        assertEquals(0, summary.getPageRequest().getStart());
        assertEquals(1, summary.getSize());
        assertFalse(summary.isLastPage());
        assertSame(CommandResult.SUCCEEDED, summary.getResult());

        callback = mockFileContentCallback(FileContentCallback.class);
        summaryCaptor.getAllValues().clear();

        commandFactory.file(repository, parameters, callback, PageUtils.newRequest(1, 1)).call();

        ordered = inOrder(callback);
        ordered.verify(callback).onStart(isNotNull(FileContext.class));
        ordered.verify(callback).onLine(eq(2), eq("another line"), eq(false));
        ordered.verify(callback).offerBlame((List) argThat(hasSize(1)));
        ordered.verify(callback).onEnd(summaryCaptor.capture());
        ordered.verifyNoMoreInteractions();

        summary = summaryCaptor.getValue();
        assertEquals(1, summary.getPageRequest().getLimit());
        assertEquals(1, summary.getPageRequest().getStart());
        assertEquals(1, summary.getSize());
        assertTrue(summary.isLastPage());
        assertSame(CommandResult.SUCCEEDED, summary.getResult());
    }

    @Test
    public void testHeads() throws Exception {
        RefCallback callback = mock(RefCallback.class);
        when(callback.onRef(any(Ref.class))).thenReturn(true);

        commandFactory.heads(repository, callback).call();

        ArgumentCaptor<Ref> refCaptor = ArgumentCaptor.forClass(Ref.class);
        ArgumentCaptor<RefSummary> summaryCaptor = ArgumentCaptor.forClass(RefSummary.class);

        InOrder ordered = inOrder(callback);
        ordered.verify(callback).onStart(same(HeadsForEachRefOutputHandler.EMPTY_CONTEXT));
        ordered.verify(callback, times(7)).onRef(refCaptor.capture());
        ordered.verify(callback).onEnd(summaryCaptor.capture());
        ordered.verifyNoMoreInteractions();

        List<Ref> refs = refCaptor.getAllValues();
        assertThat(refs, IsIterableContainingInAnyOrder.containsInAnyOrder(
                ref("basic_branching", "d6edcbf924697ab811a867421dab60d954ccad99"),
                ref("branch_mod_merge", "6053a1eaa1c009dd11092d09a72f3c41af1b59ad"),
                ref("master", "0a943a29376f2336b78312d99e65da17048951db"),
                ref("out_of_order_branch", "2e10dd2d1d5eea9291b296e78312e8a703964a95"),
                ref("backdated_annotated_tag", "57150c54c38d6570b2fd5e6d6cfc19476de44e84"),
                ref("retagged_signed_tag", "12ebe2a58367347cd39f19f5a72f3cbec7b8f9a9"),
                ref("signed_tag", "0a943a29376f2336b78312d99e65da17048951db")));

        RefSummary summary = summaryCaptor.getValue();
        assertNotNull(summary);
        assertSame(CommandResult.SUCCEEDED, summary.getResult());
    }

    @Test
    public void testRawFile() throws Exception {
        final ByteArrayOutputStream stream = new ByteArrayOutputStream();
        commandFactory.rawFile(repository,
                new RawFileCommandParameters.Builder().commitId("042fb0cc198e7320b59a025689b45a2cb52f4684")
                        .path("branch_mod_merge/file.txt").build(),
                contentType -> {
                    assertEquals("text/plain", contentType);
                    return stream;
                }).call();
        assertEquals("branch, mod and merge scenario\n", stream.toString());
    }

    @Test
    public void testResolveRef() throws Exception {
        for (String refId : Arrays.asList("master", "refs/heads/master")) {
            Ref ref = commandFactory.resolveRef(repository, new ResolveRefCommandParameters.Builder(refId).build())
                    .call();
            assertNotNull(refId + " did not resolve", ref);
            assertEquals("0a943a29376f2336b78312d99e65da17048951db", ref.getLatestCommit());
            assertEquals("refs/heads/master", ref.getId());
            assertEquals("master", ref.getDisplayId());
        }
    }

    @Test
    public void testResolveRefDeprecated() throws Exception {
        for (String refId : Arrays.asList("master", "refs/heads/master")) {
            Ref ref = commandFactory.resolveRef(repository, refId).call();
            assertNotNull(refId + " did not resolve", ref);
            assertEquals("0a943a29376f2336b78312d99e65da17048951db", ref.getLatestCommit());
            assertEquals("refs/heads/master", ref.getId());
            assertEquals("master", ref.getDisplayId());
        }
    }

    @Test
    public void testResolveRefForBranch() throws Exception {
        //Note: 0a943a29376f2336b78312d99e65da17048951db without specifying a type will resolve to signed_tag,
        //      not master, so this verifies both resolving a branch by its tip commit and that tags won't be
        //      resolved by hash when BRANCH is explicitly specified
        Stream.of("master", "refs/heads/master")
                .forEach(branchId -> {
                    Ref ref = commandFactory.resolveRef(repository, new ResolveRefCommandParameters.Builder(branchId)
                            .type(StandardRefType.BRANCH)
                            .build())
                            .call();
                    assertNotNull(branchId + " did not resolve", ref);
                    assertEquals("0a943a29376f2336b78312d99e65da17048951db", ref.getLatestCommit());
                    assertEquals("refs/heads/master", ref.getId());
                    assertEquals("master", ref.getDisplayId());
                });

        // Tags should not resolve when specifically requesting a branch
        Stream.of("signed_tag", "refs/tags/signed_tag")
                .forEach(tagId ->
                        assertNull(commandFactory.resolveRef(repository, new ResolveRefCommandParameters.Builder(tagId)
                                .type(StandardRefType.BRANCH)
                                .build())
                                .call()));
    }

    @Test
    public void testResolveRefForTag() throws Exception {
        Stream.of("signed_tag", "refs/tags/signed_tag")
                .forEach(tagId -> {
                    Tag tag = (Tag) commandFactory.resolveRef(repository, new ResolveRefCommandParameters.Builder(tagId)
                            .type(StandardRefType.TAG)
                            .build())
                            .call();
                    assertNotNull(tagId + " did not resolve", tag);
                    assertEquals("refs/tags/signed_tag", tag.getId());
                    assertEquals("signed_tag", tag.getDisplayId());
                    assertEquals("12ebe2a58367347cd39f19f5a72f3cbec7b8f9a9", tag.getHash());
                    assertEquals("0a943a29376f2336b78312d99e65da17048951db", tag.getLatestCommit());
                });

        ///Branches should not resolve when specifically requesting a tag
        Stream.of("master", "refs/heads/master")
                .forEach(branchId ->
                        assertNull(commandFactory.resolveRef(repository, new ResolveRefCommandParameters.Builder(branchId)
                                .type(StandardRefType.TAG)
                                .build())
                                .call()));
    }

    @Test
    public void testResolveRefs() throws Exception {
        Map<String, Ref> refs = commandFactory.resolveRefs(repository, new ResolveRefsCommandParameters.Builder()
                .branchIds("master", "heads/out_of_order_branch")
                .branchId("signed_tag") // should not result in hits since it's provided as a branchId
                .tagId("retagged_signed_tag")
                .refId("57150c54c") // is the commit backdated_annotated_tag is referencing
                .build())
                .call();

        assertThat(refs.values(), IsIterableContainingInAnyOrder.containsInAnyOrder(
                ref("master", "0a943a29376f2336b78312d99e65da17048951db"),
                ref("out_of_order_branch", "2e10dd2d1d5eea9291b296e78312e8a703964a95"),
                ref("backdated_annotated_tag", "57150c54c38d6570b2fd5e6d6cfc19476de44e84"),
                ref("retagged_signed_tag", "12ebe2a58367347cd39f19f5a72f3cbec7b8f9a9")));
    }

    @Test
    public void testTags() throws Exception {
        Page<Tag> page = commandFactory.tags(repository, new TagsCommandParameters.Builder().build(), PageUtils.newRequest(0, 100)).call();
        assertNotNull(page);
        assertThat(page.getValues(), IsIterableContainingInAnyOrder.containsInAnyOrder(
                tag("backdated_annotated_tag", "57150c54c38d6570b2fd5e6d6cfc19476de44e84", "80aa5be5ecf39660f798858482254f7a2ae9110e"),
                tag("retagged_signed_tag", "12ebe2a58367347cd39f19f5a72f3cbec7b8f9a9", "9b4e781dea0769888fe270e06ad76675f73851b1"),
                tag("signed_tag", "0a943a29376f2336b78312d99e65da17048951db", "12ebe2a58367347cd39f19f5a72f3cbec7b8f9a9")));
    }

    @Test
    public void testTagsInAlphabeticalOrder() throws Exception {
        Page<Tag> page = commandFactory.tags(repository, new TagsCommandParameters.Builder().order(RefOrder.ALPHABETICAL).build(), PageUtils.newRequest(0, 100)).call();
        assertNotNull(page);
        assertThat(page.getValues(), IsIterableContainingInOrder.contains(
                tag("backdated_annotated_tag", "57150c54c38d6570b2fd5e6d6cfc19476de44e84", "80aa5be5ecf39660f798858482254f7a2ae9110e"),
                tag("retagged_signed_tag", "12ebe2a58367347cd39f19f5a72f3cbec7b8f9a9", "9b4e781dea0769888fe270e06ad76675f73851b1"),
                tag("signed_tag", "0a943a29376f2336b78312d99e65da17048951db", "12ebe2a58367347cd39f19f5a72f3cbec7b8f9a9")));
    }

    @Test
    public void testTagsInModificationOrder() {
        TagsCommandParameters params = new TagsCommandParameters.Builder().order(RefOrder.MODIFICATION).build();
        Page<Tag> page = commandFactory.tags(repository, params, PageUtils.newRequest(0, 100)).call();
        assertNotNull(page);
        assertThat(page.getValues(), IsIterableContainingInOrder.contains(
                tag("retagged_signed_tag", "12ebe2a58367347cd39f19f5a72f3cbec7b8f9a9", "9b4e781dea0769888fe270e06ad76675f73851b1"),
                tag("signed_tag", "0a943a29376f2336b78312d99e65da17048951db", "12ebe2a58367347cd39f19f5a72f3cbec7b8f9a9"),
                tag("backdated_annotated_tag", "57150c54c38d6570b2fd5e6d6cfc19476de44e84", "80aa5be5ecf39660f798858482254f7a2ae9110e")));
    }

    @Test
    public void testTagsWithFilter() {
        TagsCommandParameters params = new TagsCommandParameters.Builder().filterText("signed").build();
        Page<Tag> page = commandFactory.tags(repository, params, PageUtils.newRequest(0, 100)).call();
        assertNotNull(page);
        assertThat(page.getValues(), IsIterableContainingInAnyOrder.containsInAnyOrder(
                tag("retagged_signed_tag", "12ebe2a58367347cd39f19f5a72f3cbec7b8f9a9", "9b4e781dea0769888fe270e06ad76675f73851b1"),
                tag("signed_tag", "0a943a29376f2336b78312d99e65da17048951db", "12ebe2a58367347cd39f19f5a72f3cbec7b8f9a9")));
    }

    @Test
    public void testTraverse() {
        TraversalCallback callback = mock(TraversalCallback.class);

        commandFactory.traverseCommits(repository, callback).call();

        ArgumentCaptor<CommitGraphNode> nodeCaptor = ArgumentCaptor.forClass(CommitGraphNode.class);

        InOrder ordered = inOrder(callback);
        ordered.verify(callback).onStart(isNotNull(TraversalContext.class));
        ordered.verify(callback, times(21)).onNode(nodeCaptor.capture());
        ordered.verify(callback).onEnd(isNotNull(TraversalSummary.class));

        List<CommitGraphNode> nodes = nodeCaptor.getAllValues();
        assertEquals(21, nodes.size());
        assertCommitGraphNode(nodes.get(0), "0a943a29376f2336b78312d99e65da17048951db", "e0a3f6d8c804c375f49d7609328111e8dc75a4bc");
        assertCommitGraphNode(nodes.get(1), "e0a3f6d8c804c375f49d7609328111e8dc75a4bc", "2d8897c9ac29ce42c3442cf80ac977057045e7f6");
        assertCommitGraphNode(nodes.get(2), "2d8897c9ac29ce42c3442cf80ac977057045e7f6", "9c05f43f859375e392d90d23a13717c16d0fdcda");
        assertCommitGraphNode(nodes.get(3), "9c05f43f859375e392d90d23a13717c16d0fdcda", "6053a1eaa1c009dd11092d09a72f3c41af1b59ad");
        assertCommitGraphNode(nodes.get(4), "6053a1eaa1c009dd11092d09a72f3c41af1b59ad", "042fb0cc198e7320b59a025689b45a2cb52f4684");
        assertCommitGraphNode(nodes.get(5), "042fb0cc198e7320b59a025689b45a2cb52f4684", "8fb8538799b2481750eac52716931d895d029b7c");
        assertCommitGraphNode(nodes.get(6), "d6edcbf924697ab811a867421dab60d954ccad99", "8fb8538799b2481750eac52716931d895d029b7c");
        assertCommitGraphNode(nodes.get(7), "8fb8538799b2481750eac52716931d895d029b7c", "53979b3304bece75a3852a3d250dbaed64d45430");
        assertCommitGraphNode(nodes.get(8), "53979b3304bece75a3852a3d250dbaed64d45430", "ed69be6064dcef2d0b08b35192f581613e48411b");
        assertCommitGraphNode(nodes.get(9), "ed69be6064dcef2d0b08b35192f581613e48411b", "05c78271ed4d7a158fe6789f6b27e3a47631faba");
        assertCommitGraphNode(nodes.get(10), "05c78271ed4d7a158fe6789f6b27e3a47631faba", "57150c54c38d6570b2fd5e6d6cfc19476de44e84");
        assertCommitGraphNode(nodes.get(11), "57150c54c38d6570b2fd5e6d6cfc19476de44e84", "9fbc34a0d905950131d73f352abe68520c6db2a3");
        assertCommitGraphNode(nodes.get(12), "9fbc34a0d905950131d73f352abe68520c6db2a3", "6d8e9736357657738d15cd29069f21bf44553e19");
        assertCommitGraphNode(nodes.get(13), "6d8e9736357657738d15cd29069f21bf44553e19", "4f9290ae1a9fcde7acd56664a22cc65eb76540f3");
        assertCommitGraphNode(nodes.get(14), "4f9290ae1a9fcde7acd56664a22cc65eb76540f3", "8a71bdc3c67e49729db5f17f0477cee857fd7ae7");
        assertCommitGraphNode(nodes.get(15), "8a71bdc3c67e49729db5f17f0477cee857fd7ae7", "bf3346620ec9161977fc23dee1240a7991e64ca1");
        assertCommitGraphNode(nodes.get(16), "bf3346620ec9161977fc23dee1240a7991e64ca1", "e26cab95a50f6e99d2c46c498a23cee307cc8e27");
        assertCommitGraphNode(nodes.get(17), "e26cab95a50f6e99d2c46c498a23cee307cc8e27", "2e10dd2d1d5eea9291b296e78312e8a703964a95");
        assertCommitGraphNode(nodes.get(18), "2e10dd2d1d5eea9291b296e78312e8a703964a95", "c2608f5dff150e2b26b4b68d9e22369581b39b0c");
        assertCommitGraphNode(nodes.get(19), "c2608f5dff150e2b26b4b68d9e22369581b39b0c", "409a2180fd95b744df8d817e770b7f8c853f78dc");
        assertCommitGraphNode(nodes.get(20), "409a2180fd95b744df8d817e770b7f8c853f78dc");
    }

    @Test
    public void testType() {
        Command<ContentTreeNode.Type> command = commandFactory.type(repository,
                new TypeCommandParameters.Builder().commitId("master").path("modification").build());
        assertSame(ContentTreeNode.Type.DIRECTORY, command.call());
    }

    private static void assertCommitGraphNode(CommitGraphNode node, String commitId, String... parentIds) {
        assertEquals(commitId, node.getCommit().getId());

        Collection<MinimalCommit> parents = node.getParents();
        assertNotNull(parents);
        if (parentIds == null || parentIds.length == 0) {
            assertTrue(parents.isEmpty());
        } else {
            assertEquals("Expected " + parentIds.length + " parents for " + commitId, parentIds.length, parents.size());
            for (int i = 0; i < parentIds.length; ++i) {
                assertEquals(commitId + "^" + i, parentIds[i], Iterables.get(parents, i).getId());
            }
        }
    }

    private static void assertFollowProperties(Commit commit, ChangeType changeType, String path) {
        assertFollowProperties(commit, changeType, path, null, -1);
    }

    private static void assertFollowProperties(Commit commit, ChangeType changeType, String path,
                                               String srcPath, int percentUnchanged) {
        Map<?, ?> change = commit.getProperties().getAs(AbstractCommitRevListOutputHandler.NAME_STATUS_CHANGE, Map.class);
        assertNotNull("No \"change\" data is associated with the commit", change);
        assertFalse(change.isEmpty());
        assertSame(changeType, change.get("type"));
        assertEquals(path, change.get("path"));
        if (changeType == ChangeType.COPY || changeType == ChangeType.MOVE) {
            assertEquals(srcPath, change.get("srcPath"));
            assertEquals(percentUnchanged, change.get("percentUnchanged"));
        }
    }

    private void assertCommits(Boolean ignoreMissing, String commit, String... commits) {
        CommitCallback callback = mock(CommitCallback.class);
        commandFactory.commits(repository, new CommitsCommandParameters.Builder()
                .ignoreMissing(ignoreMissing)
                .include(commit, commits)
                .build(), callback).call();
    }

    private static <T extends FileContentCallback> T mockFileContentCallback(Class<T> callbackClass)
            throws IOException {
        T callback = mock(callbackClass);
        doReturn(true).when(callback).onLine(anyInt(), anyString(), anyBoolean());

        return callback;
    }

    private void runCommitWithOutOfOrderDates(boolean retrieveByName) throws IOException {
        SimplePerson author = new SimplePerson("Bryan Turner", "bturner@atlassian.com");
        CommandOutputHandler<Void> outputHandler = new GitFactoriesTestHelper.NoOpHandler();

        Repository repository = mock(Repository.class);
        config.setRepositoryDir(repository, folder.getRoot());

        builderFactory.builder().init()
                .directory(folder.getRoot().getAbsolutePath())
                .build()
                .call();

        Files.write("Hello, World!", new File(folder.getRoot(), "file.txt"), StandardCharsets.UTF_8);
        builderFactory.builder(repository).add()
                .path("file.txt")
                .build()
                .call();
        builderFactory.builder(repository).commit()
                .author(author)
                .message("Initial commit")
                .withEnvironment(ENV_AUTHOR_DATE, "2015-03-10 10:00:00 +1100")
                .withEnvironment(ENV_COMMITTER_DATE, "2015-03-10 10:00:00 +1100")
                .build()
                .call();

        Files.append("Goodbye, World!", new File(folder.getRoot(), "file.txt"), StandardCharsets.UTF_8);
        builderFactory.builder(repository).command("commit") //Manual command because we want to use --all
                .argument("--all")
                .argument("-m")
                .argument("A commit on master")
                .withEnvironment(ENV_AUTHOR_DATE, "2015-03-10 11:30:00 +1100")
                .withEnvironment(GitCommandBuilderSupport.ENV_AUTHOR_EMAIL, author.getEmailAddress())
                .withEnvironment(GitCommandBuilderSupport.ENV_AUTHOR_NAME, author.getName())
                .withEnvironment(ENV_COMMITTER_DATE, "2015-03-10 11:30:00 +1100")
                .withEnvironment(GitCommandBuilderSupport.ENV_COMMITTER_EMAIL, author.getEmailAddress())
                .withEnvironment(GitCommandBuilderSupport.ENV_COMMITTER_NAME, author.getName())
                .build(outputHandler)
                .call();

        Files.write("This is another file, so we can do a clean merge.", new File(folder.getRoot(), "junk.txt"), StandardCharsets.UTF_8);
        builderFactory.builder(repository).command("checkout")
                .argument("-b")
                .argument("branch")
                .argument("master^") //Move up one commit so that we're not fast-forward
                .build(new GitFactoriesTestHelper.NoOpHandler())
                .call();
        builderFactory.builder(repository).add()
                .path("junk.txt")
                .build()
                .call();
        builderFactory.builder(repository).commit()
                .author(author)
                .message("A commit on branch")
                .withEnvironment(ENV_AUTHOR_DATE, "2015-03-10 11:00:00 +1100")
                .withEnvironment(ENV_COMMITTER_DATE, "2015-03-10 11:00:00 +1100")
                .build()
                .call();

        builderFactory.builder(repository).command("checkout")
                .argument("master")
                .build(outputHandler)
                .call();
        builderFactory.builder(repository).merge()
                .normal()
                .author(author)
                .commit("branch")
                .message("Merging branch into master")
                .withEnvironment(ENV_AUTHOR_DATE, "2015-03-10 10:30:00 +1100")
                .withEnvironment(ENV_COMMITTER_DATE, "2015-03-10 10:30:00 +1100")
                .build()
                .call();

        //Note that because file names and contents as well as all author and committer details, including
        //timestamps, are fixed, each time this test runs it creates the same commits, right down to their
        //SHA-1s. That means we know in advance the commit ID we should end up with
        Commit merge = commandFactory.commit(repository, new CommitCommandParameters.Builder()
                .commitId(retrieveByName ? "master" : "733ba6f49752b003de7d5193a12ed855dbfdc156")
                .build())
                .call();
        assertNotNull(merge);
        assertEquals("733ba6f49752b003de7d5193a12ed855dbfdc156", merge.getId());

        Collection<MinimalCommit> parents = merge.getParents();
        assertEquals(2, parents.size());

        MinimalCommit firstParent = Iterables.get(parents, 0);
        assertEquals("6638e3efa4f57be70e5a17c08e0bbc155b9942ab", firstParent.getId());
        assertTrue(firstParent instanceof Commit);  //Verify that the full details were retrieved

        MinimalCommit secondParent = Iterables.get(parents, 1);
        assertEquals("4050ab17a6e3ddb06c97801a3977c8a514660a47", secondParent.getId());
        assertTrue(secondParent instanceof Commit); //Verify that the full details were retrieved
    }

    private <E extends Exception> void runDiffWithStrategy(String mode, ConfiguredTest<E> test) throws E {
        runWithConfig(DefaultGitRepositoryConfig.PROP_DIFF_RENAMES, mode, test);
    }
}
