package com.atlassian.bitbucket.mesh.git;

import com.atlassian.bitbucket.mesh.AbstractGrpcServiceTest;
import com.atlassian.bitbucket.mesh.git.builder.GitProcessBuilderFactory;
import com.atlassian.bitbucket.mesh.git.builder.log.GitLogBuilder;
import com.atlassian.bitbucket.mesh.git.builder.revlist.AbstractCommitRevListStdioHandler;
import com.atlassian.bitbucket.mesh.git.builder.revlist.CallbackRevListStdioHandler;
import com.atlassian.bitbucket.mesh.git.builder.revlist.SingleCommitRevListStdioHandler;
import com.atlassian.bitbucket.mesh.git.commit.Commit;
import com.atlassian.bitbucket.mesh.git.exception.CommandFailedException;
import com.atlassian.bitbucket.mesh.git.exception.CommitNotFoundException;
import com.atlassian.bitbucket.mesh.grpc.BidirectionalFragmentResponseObserver;
import com.atlassian.bitbucket.mesh.grpc.FragmentResponseObserver;
import com.atlassian.bitbucket.mesh.grpc.RequestFragmentCreator;
import com.atlassian.bitbucket.mesh.process.OsProcess;
import com.atlassian.bitbucket.mesh.process.ProcessStdioHandler;
import com.atlassian.bitbucket.mesh.process.handler.AbstractLineStdioHandler;
import com.atlassian.bitbucket.mesh.process.handler.LineEncoder;
import com.atlassian.bitbucket.mesh.process.handler.StringStdioHandler;
import com.atlassian.bitbucket.mesh.repository.Repository;
import com.atlassian.bitbucket.mesh.rpc.v1.*;
import com.atlassian.bitbucket.mesh.test.HasGitError;
import com.atlassian.bitbucket.mesh.test.annotation.DirtiesRepo;
import com.atlassian.bitbucket.mesh.test.annotation.Repo;
import com.google.common.collect.ImmutableList;
import com.google.protobuf.ByteString;
import io.grpc.Status;
import io.grpc.StatusRuntimeException;
import org.junit.Before;
import org.junit.Test;

import javax.annotation.Nonnull;
import javax.annotation.Resource;
import java.io.ByteArrayOutputStream;
import java.io.OutputStream;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.util.*;
import java.util.stream.Stream;

import static com.atlassian.bitbucket.mesh.git.GitRepositories.MERGE_COMMITS;
import static com.atlassian.bitbucket.mesh.git.GitRepositories.QA_RESOURCES;
import static com.atlassian.bitbucket.mesh.git.GrpcPlumbingServiceTest.assertCommit;
import static com.atlassian.bitbucket.mesh.git.builder.revlist.CommitPumpSettings.UNLIMITED_MESSAGE_LENGTH;
import static com.atlassian.bitbucket.mesh.rpc.util.ByteStringUtils.toByteStringsLazily;
import static java.nio.charset.StandardCharsets.UTF_8;
import static org.hamcrest.Matchers.*;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verifyNoInteractions;

public class GrpcPorcelainServiceTest extends AbstractGrpcServiceTest {

    @Resource
    private GitProcessBuilderFactory builderFactory;
    @Repo(MERGE_COMMITS)
    private Repository mergeCommits;
    @Repo(QA_RESOURCES)
    private Repository qaResources;
    @Repo
    private Repository repository;
    private PorcelainServiceGrpc.PorcelainServiceStub stub;

    @Before
    public void setup() {
        stub = PorcelainServiceGrpc.newStub(managedChannel);
    }

    @Test
    public void testFormatPatch() {
        String patch = streamPatch(createFormatPatchBuilder()
                .setRev(ByteString.copyFromUtf8("6053a1eaa1c009dd11092d09a72f3c41af1b59ad")));

        assertThat(patch, startsWith("From 6053a1eaa1c009dd11092d09a72f3c41af1b59ad Mon Sep 17 00:00:00 2001\n" +
                "From: Michael Heemskerk <mheemskerk@atlassian.com>\n" +
                "Date: Mon, 20 Dec 2010 14:41:15 +1100\n" +
                "Subject: [PATCH] modification on branch_mod_merge\n" +
                "\n" +
                "---\n" +
                " branch_mod_merge/file.txt | 1 +\n" +
                " 1 file changed, 1 insertion(+)\n" +
                "\n" +
                "diff --git a/branch_mod_merge/file.txt b/branch_mod_merge/file.txt\n" +
                "index 8e66897..668076c 100644\n" +
                "--- a/branch_mod_merge/file.txt\n" +
                "+++ b/branch_mod_merge/file.txt\n" +
                "@@ -1 +1,2 @@\n" +
                " branch, mod and merge scenario\n" +
                "+a modification\n" +
                "-- "));
    }

    @Test
    public void testFormatPatchWithAllAncestorsAndRev() {
        String patch = streamPatch(
                createFormatPatchBuilder()
                        .setAllAncestors(true)
                        .setRev(ByteString.copyFromUtf8("6053a1eaa1c")));
        // Patch 1 of 16
        assertThat(patch, startsWith("From 409a2180fd95b744df8d817e770b7f8c853f78dc Mon Sep 17 00:00:00 2001\n" +
                "From: Michael Heemskerk <mheemskerk@atlassian.com>\n" +
                "Date: Sun, 10 Jan 2010 10:10:10 +1100\n" +
                "Subject: [PATCH 01/16] out of order commits scenario, initial commit\n" +
                "\n" +
                "---\n" +
                " out_of_order/file.txt | 1 +\n" +
                " 1 file changed, 1 insertion(+)\n" +
                " create mode 100644 out_of_order/file.txt\n" +
                "\n" +
                "diff --git a/out_of_order/file.txt b/out_of_order/file.txt\n" +
                "new file mode 100644\n" +
                "index 0000000..30bda1b\n" +
                "--- /dev/null\n" +
                "+++ b/out_of_order/file.txt\n" +
                "@@ -0,0 +1 @@\n" +
                "+out of order commits scenario\n" +
                "-- "));
        // Patch 16 of 16
        assertThat(patch, containsString("From 6053a1eaa1c009dd11092d09a72f3c41af1b59ad Mon Sep 17 00:00:00 2001\n" +
                "From: Michael Heemskerk <mheemskerk@atlassian.com>\n" +
                "Date: Mon, 20 Dec 2010 14:41:15 +1100\n" +
                "Subject: [PATCH 16/16] modification on branch_mod_merge\n" +
                "\n" +
                "---\n" +
                " branch_mod_merge/file.txt | 1 +\n" +
                " 1 file changed, 1 insertion(+)\n" +
                "\n" +
                "diff --git a/branch_mod_merge/file.txt b/branch_mod_merge/file.txt\n" +
                "index 8e66897..668076c 100644\n" +
                "--- a/branch_mod_merge/file.txt\n" +
                "+++ b/branch_mod_merge/file.txt\n" +
                "@@ -1 +1,2 @@\n" +
                " branch, mod and merge scenario\n" +
                "+a modification\n" +
                "-- "));
    }

    @Test
    public void testFormatPatchWithAncestorAndRev() {
        String patch = streamPatch(
                createFormatPatchBuilder()
                        .setAncestor(ByteString.copyFromUtf8("8fb8538799b"))
                        .setRev(ByteString.copyFromUtf8("6053a1eaa1c")));
        // Patch 1 of 2
        assertThat(patch, startsWith("From 042fb0cc198e7320b59a025689b45a2cb52f4684 Mon Sep 17 00:00:00 2001\n" +
                "From: Michael Heemskerk <mheemskerk@atlassian.com>\n" +
                "Date: Mon, 20 Dec 2010 14:41:15 +1100\n" +
                "Subject: [PATCH 1/2] branch, mod and merge scenario\n" +
                "\n" +
                "---\n" +
                " branch_mod_merge/file.txt | 1 +\n" +
                " 1 file changed, 1 insertion(+)\n" +
                " create mode 100644 branch_mod_merge/file.txt\n" +
                "\n" +
                "diff --git a/branch_mod_merge/file.txt b/branch_mod_merge/file.txt\n" +
                "new file mode 100644\n" +
                "index 0000000..8e66897\n" +
                "--- /dev/null\n" +
                "+++ b/branch_mod_merge/file.txt\n" +
                "@@ -0,0 +1 @@\n" +
                "+branch, mod and merge scenario\n" +
                "-- "));
        // Patch 2 of 2
        assertThat(patch, containsString("From 6053a1eaa1c009dd11092d09a72f3c41af1b59ad Mon Sep 17 00:00:00 2001\n" +
                "From: Michael Heemskerk <mheemskerk@atlassian.com>\n" +
                "Date: Mon, 20 Dec 2010 14:41:15 +1100\n" +
                "Subject: [PATCH 2/2] modification on branch_mod_merge\n" +
                "\n" +
                "---\n" +
                " branch_mod_merge/file.txt | 1 +\n" +
                " 1 file changed, 1 insertion(+)\n" +
                "\n" +
                "diff --git a/branch_mod_merge/file.txt b/branch_mod_merge/file.txt\n" +
                "index 8e66897..668076c 100644\n" +
                "--- a/branch_mod_merge/file.txt\n" +
                "+++ b/branch_mod_merge/file.txt\n" +
                "@@ -1 +1,2 @@\n" +
                " branch, mod and merge scenario\n" +
                "+a modification\n" +
                "-- "));
    }

    @Test
    public void testFormatPatchWithInvalidRef() {
        OutputStream stream = mock(OutputStream.class);

        StatusRuntimeException thrown = assertThrows(StatusRuntimeException.class,
                () -> streamPatch(createFormatPatchBuilder()
                        .setRev(ByteString.copyFromUtf8("--remote=ssh://127.0.0.1/")), stream));

        assertSame(Status.Code.INVALID_ARGUMENT, thrown.getStatus().getCode());
        assertThat(thrown, HasGitError.invalidCommitish("--remote=ssh://127.0.0.1/"));

        verifyNoInteractions(stream);
    }

    @Test
    public void testFormatPatchWithMissingCommit() {
        OutputStream stream = mock(OutputStream.class);

        CommitNotFoundException thrown = assertThrows(CommitNotFoundException.class,
                () -> streamPatch(createFormatPatchBuilder()
                        .setRev(ByteString.copyFromUtf8("905b4f9708b5238ccecde1749dd02e9cdca34646")), stream));

        assertSame(Status.Code.NOT_FOUND, thrown.getStatus().getCode());
        assertThat(thrown.getMessage(), equalTo("No such commit: 905b4f9708b5238ccecde1749dd02e9cdca34646"));

        verifyNoInteractions(stream);
    }

    /**
     * Ensures {@link GitLogBuilder#rev} can still be used with {@link GitLogBuilder#all}. In this case we're
     * ensuring we can specify an exclude, but there are reasons you might want to include things even if
     * {@code --all} is being passed. One example might be to include one or more unreachable SHAs while also
     * including all refs as well.
     */
    @Test
    public void testLogAllWithRevs() {
        SingleCommitRevListStdioHandler handler =
                new SingleCommitRevListStdioHandler(null, true, false, UNLIMITED_MESSAGE_LENGTH);

        RpcLogRequest.Builder builder = RpcLogRequest.newBuilder()
                .setLogOptions(RpcRevListOptions.newBuilder()
                        .addRevs(ByteString.copyFromUtf8("^master"))
                        .setAll(true)
                        .setFormat(revListToLogFormat(handler)))
                .setRepository(repository.getId());

        Commit commit = log(builder, handler);

        assertNotNull(commit);
        assertEquals("d6edcbf924697ab811a867421dab60d954ccad99", commit.getId());
    }

    @Test
    public void testLogAmbiguousCommit() {
        SingleCommitRevListStdioHandler handler =
                new SingleCommitRevListStdioHandler(null, true, false, UNLIMITED_MESSAGE_LENGTH);

        RpcLogRequest.Builder builder = RpcLogRequest.newBuilder()
                .setLogOptions(RpcRevListOptions.newBuilder()
                        .addRevs(ByteString.copyFromUtf8("FOO")))
                .setRepository(repository.getId());

        CommitNotFoundException thrown = assertThrows(CommitNotFoundException.class, () -> log(builder, handler));
        assertSame(Status.Code.NOT_FOUND, thrown.getStatus().getCode());
    }

    @Test
    public void testLogBranches() {
        List<Commit> commits = new ArrayList<>();
        CallbackRevListStdioHandler handler =
                new CallbackRevListStdioHandler((commit, refChange) -> commits.add(commit),
                        true, UNLIMITED_MESSAGE_LENGTH, Collections.emptyList());

        RpcLogRequest.Builder builder = RpcLogRequest.newBuilder()
                .setLogOptions(RpcRevListOptions.newBuilder()
                        .setBranches(true)
                        .setFormat(revListToLogFormat(handler))
                        .setWalkValue(RpcRevListWalk.WALK_NO.getNumber()))
                .setRepository(repository.getId());

        log(builder, handler);

        assertEquals(4, commits.size());
        assertEquals("0a943a29376f2336b78312d99e65da17048951db", commits.get(0).getId());
        assertEquals("6053a1eaa1c009dd11092d09a72f3c41af1b59ad", commits.get(1).getId());
        assertEquals("d6edcbf924697ab811a867421dab60d954ccad99", commits.get(2).getId());
        assertEquals("2e10dd2d1d5eea9291b296e78312e8a703964a95", commits.get(3).getId());
    }

    @DirtiesRepo
    @Test
    public void testLogBranchesDoesNotIncludeHead() {
        //First, set HEAD to a nonexistent ref. If HEAD is included in the rev-list arguments by the builder,
        //this will cause the command to fail
        builderFactory.builder(repository).symbolicRef()
                .set("HEAD", "refs/heads/nonexistent")
                .build()
                .run();

        RpcLogRequest.Builder builder = RpcLogRequest.newBuilder()
                .setLogOptions(RpcRevListOptions.newBuilder()
                        .setBranches(true)
                        .setFormat("%H"))
                .setRepository(repository.getId());

        Queue<String> commitIds = log(builder, new QueueHandler());

        assertNotNull(commitIds);
        assertTrue(commitIds.contains("d6edcbf924697ab811a867421dab60d954ccad99"));  //basic_branching
        assertTrue(commitIds.contains("6053a1eaa1c009dd11092d09a72f3c41af1b59ad"));  //branch_mod_merge
        assertTrue(commitIds.contains("0a943a29376f2336b78312d99e65da17048951db"));  //master
        assertTrue(commitIds.contains("2e10dd2d1d5eea9291b296e78312e8a703964a95"));  //out_of_order_branch
        assertEquals(21, commitIds.size());
    }

    @Test
    public void testLogCommitsBetween() {
        List<Commit> commits = new ArrayList<>();
        CallbackRevListStdioHandler handler =
                new CallbackRevListStdioHandler((commit, refChange) -> commits.add(commit),
                        true, UNLIMITED_MESSAGE_LENGTH, Collections.emptyList());

        RpcLogRequest.Builder builder = RpcLogRequest.newBuilder()
                .setLogOptions(RpcRevListOptions.newBuilder()
                        .addRevs(ByteString.copyFromUtf8("2d8897c9ac29ce42c3442cf80ac977057045e7f6"))
                        .addRevs(ByteString.copyFromUtf8("^6053a1eaa1c009dd11092d09a72f3c41af1b59ad"))
                        .setFormat(revListToLogFormat(handler))
                        .setIgnoreMissing(true))
                .setRepository(repository.getId());

        log(builder, handler);

        assertEquals(2, commits.size());
        // test 1 commit fully to ensure that output parsing and commit construction is correct
        Commit commit = commits.get(0);
        assertCommit(commit, "Anna Buttfield", "abuttfield@cascade.sydney.atlassian.com",
                1296709182, "2d8897c9ac29ce42c3442cf80ac977057045e7f6",
                "Rename A.zip to B.zip\n", "9c05f43f859375e392d90d23a13717c16d0fdcda");
        // now check the ids on all the others
        assertEquals("9c05f43f859375e392d90d23a13717c16d0fdcda", commits.get(1).getId());
    }

    @Test
    public void testLogCommitsBetweenWithoutMessages() {
        List<Commit> commits = new ArrayList<>();
        CallbackRevListStdioHandler handler =
                new CallbackRevListStdioHandler((commit, refChange) -> commits.add(commit),
                        false, 0, Collections.emptyList());

        RpcLogRequest.Builder builder = RpcLogRequest.newBuilder()
                .setLogOptions(RpcRevListOptions.newBuilder()
                        .addRevs(ByteString.copyFromUtf8("2d8897c9ac29ce42c3442cf80ac977057045e7f6"))
                        .addRevs(ByteString.copyFromUtf8("^6053a1eaa1c009dd11092d09a72f3c41af1b59ad"))
                        .setFormat(revListToLogFormat(handler))
                        .setIgnoreMissing(true))
                .setRepository(repository.getId());

        log(builder, handler);

        assertEquals(2, commits.size());
        // test 1 commit fully to ensure that output parsing and commit construction is correct
        Commit commit = commits.get(0);
        assertCommit(commit, "Anna Buttfield", "abuttfield@cascade.sydney.atlassian.com",
                1296709182, "2d8897c9ac29ce42c3442cf80ac977057045e7f6", null,
                "9c05f43f859375e392d90d23a13717c16d0fdcda");
        // now check the ids on all the others
        assertEquals("9c05f43f859375e392d90d23a13717c16d0fdcda", commits.get(1).getId());
    }

    @Test
    public void testLogCommitsForPath() {
        List<Commit> commits = new ArrayList<>();
        CallbackRevListStdioHandler handler =
                new CallbackRevListStdioHandler((commit, refChange) -> commits.add(commit),
                        true, UNLIMITED_MESSAGE_LENGTH, Collections.emptyList());

        RpcRevListOptions.Builder optionsBuilder = RpcRevListOptions.newBuilder()
                .addPaths(ByteString.copyFromUtf8("modification/mod_file.txt"))
                .addRevs(ByteString.copyFromUtf8("HEAD"))
                .setFormat(handler.getFormat());
        RpcLogRequest.Builder builder = RpcLogRequest.newBuilder()
                .setLogOptions(optionsBuilder)
                .setRepository(repository.getId());

        log(builder, handler);

        assertNotNull(commits);
        assertEquals(2, commits.size());

        assertCommit(commits.get(0), "Michael Heemskerk", "mheemskerk@atlassian.com",
                1292816091, "bf3346620ec9161977fc23dee1240a7991e64ca1",
                "edited modification/mod_file.txt\n", "e26cab95a50f6e99d2c46c498a23cee307cc8e27");
        assertCommit(commits.get(1), "Michael Heemskerk", "mheemskerk@atlassian.com",
                1292816091, "e26cab95a50f6e99d2c46c498a23cee307cc8e27",
                "plain modification, initial add\n", "2e10dd2d1d5eea9291b296e78312e8a703964a95");
    }

    @Test
    public void testLogCommitsPathWithFollowAndSkipThrows() {
        // --skip with --follow leads to unexpected results, so should be avoided. This combination should result in
        // an exception
        List<Commit> commits = new ArrayList<>();
        CallbackRevListStdioHandler handler =
                new CallbackRevListStdioHandler((commit, refChange) -> commits.add(commit),
                        true, UNLIMITED_MESSAGE_LENGTH, Collections.emptyList());

        RpcLogRequest.Builder builder = RpcLogRequest.newBuilder()
                .setLogOptions(RpcRevListOptions.newBuilder()
                        .addPaths(ByteString.copyFromUtf8("modification/mod_file.txt"))
                        .addRevs(ByteString.copyFromUtf8("HEAD"))
                        .setFormat(handler.getFormat())
                        .setSkip(5))
                .setFollow(true)
                .setRepository(repository.getId());

        StatusRuntimeException thrown = assertThrows(StatusRuntimeException.class, () -> log(builder, handler));
        assertSame(Status.Code.INVALID_ARGUMENT, thrown.getStatus().getCode());
    }

    @Test
    public void testLogCommitsForPathWithTrailingWhitespace() {
        List<Commit> commits = new ArrayList<>();
        CallbackRevListStdioHandler handler =
                new CallbackRevListStdioHandler((commit, refChange) -> commits.add(commit),
                        true, UNLIMITED_MESSAGE_LENGTH, Collections.emptyList());

        RpcLogRequest.Builder builder = RpcLogRequest.newBuilder()
                .setLogOptions(RpcRevListOptions.newBuilder()
                        .addPaths(ByteString.copyFromUtf8("spaces/file.txt "))
                        .addRevs(ByteString.copyFromUtf8("HEAD"))
                        .setFormat(handler.getFormat()))
                .setRepository(qaResources.getId());

        log(builder, handler);

        assertNotNull(commits);
        assertEquals(1, commits.size());

        assertCommit(commits.get(0), "Bryan Turner", "bturner@atlassian.com", 1547860858,
                "4eae372719e92c48ee67819a5adfa13cc25b59c7", "BSERV-9123 Add files with whitespace in their names\n",
                "effbbd10c64eeb5caf3dd1724d17a1ed935c7c62");
    }

    @Test
    public void testLogCommitsForPathWithoutMessages() {
        List<Commit> commits = new ArrayList<>();
        CallbackRevListStdioHandler handler =
                new CallbackRevListStdioHandler((commit, refChange) -> commits.add(commit),
                        false, UNLIMITED_MESSAGE_LENGTH, Collections.emptyList());

        RpcRevListOptions.Builder optionsBuilder = RpcRevListOptions.newBuilder()
                .addPaths(ByteString.copyFromUtf8("modification/mod_file.txt"))
                .addRevs(ByteString.copyFromUtf8("HEAD"))
                .setFormat(handler.getFormat());
        RpcLogRequest.Builder builder = RpcLogRequest.newBuilder()
                .setLogOptions(optionsBuilder)
                .setRepository(repository.getId());

        log(builder, handler);

        assertNotNull(commits);
        assertEquals(2, commits.size());

        assertCommit(commits.get(0), "Michael Heemskerk", "mheemskerk@atlassian.com",
                1292816091, "bf3346620ec9161977fc23dee1240a7991e64ca1", null,
                "e26cab95a50f6e99d2c46c498a23cee307cc8e27");
        assertCommit(commits.get(1), "Michael Heemskerk", "mheemskerk@atlassian.com",
                1292816091, "e26cab95a50f6e99d2c46c498a23cee307cc8e27", null,
                "2e10dd2d1d5eea9291b296e78312e8a703964a95");
    }

    @Test
    public void testLogCommitsSince() {
        List<Commit> commits = new ArrayList<>();
        CallbackRevListStdioHandler handler =
                new CallbackRevListStdioHandler((commit, refChange) -> commits.add(commit),
                        true, UNLIMITED_MESSAGE_LENGTH, Collections.emptyList());

        RpcLogRequest.Builder builder = RpcLogRequest.newBuilder()
                .setLogOptions(RpcRevListOptions.newBuilder()
                        .addRevs(ByteString.copyFromUtf8("^6053a1eaa1c009dd11092d09a72f3c41af1b59ad"))
                        .setAll(true)
                        .setIgnoreMissing(true)
                        .setFormat(handler.getFormat()))
                .setRepository(repository.getId());

        log(builder, handler);

        assertEquals(5, commits.size());

        Commit commit = commits.get(0);
        assertCommit(commit, "Anna Buttfield", "abuttfield@cascade.sydney.atlassian.com",
                1296709267, "0a943a29376f2336b78312d99e65da17048951db",
                "Copy C.zip as D.zip\n", "e0a3f6d8c804c375f49d7609328111e8dc75a4bc");

        // now check the ids on all the others
        assertEquals("e0a3f6d8c804c375f49d7609328111e8dc75a4bc", commits.get(1).getId());
        assertEquals("2d8897c9ac29ce42c3442cf80ac977057045e7f6", commits.get(2).getId());
        assertEquals("9c05f43f859375e392d90d23a13717c16d0fdcda", commits.get(3).getId());
        assertEquals("d6edcbf924697ab811a867421dab60d954ccad99", commits.get(4).getId());
    }

    @Test
    public void testLogExcludeMergeCommits() {
        List<Commit> commits = new ArrayList<>();
        CallbackRevListStdioHandler handler =
                new CallbackRevListStdioHandler((commit, refChange) -> commits.add(commit),
                        true, UNLIMITED_MESSAGE_LENGTH, Collections.emptyList());

        RpcLogRequest.Builder builder = RpcLogRequest.newBuilder()
                .setLogOptions(RpcRevListOptions.newBuilder()
                        .setAll(true)
                        .setFormat(handler.getFormat())
                        .setMerges(RpcRevListMerges.MERGES_EXCLUDE))
                .setRepository(mergeCommits.getId());

        log(builder, handler);

        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 testLogFirstCommit() {
        SingleCommitRevListStdioHandler handler =
                new SingleCommitRevListStdioHandler(null, true, false, UNLIMITED_MESSAGE_LENGTH);

        RpcLogRequest.Builder builder = RpcLogRequest.newBuilder()
                .setLogOptions(RpcRevListOptions.newBuilder()
                        .addRevs(ByteString.copyFromUtf8("042fb0cc198e7320b59a025689b45a2cb52f4684"))
                        .setLimit(1)
                        .setFormat(revListToLogFormat(handler)))
                .setRepository(repository.getId());

        Commit commit = log(builder, handler);

        assertNotNull(commit);
        assertCommit(commit, "Michael Heemskerk", "mheemskerk@atlassian.com",
                1292816475, "042fb0cc198e7320b59a025689b45a2cb52f4684",
                "branch, mod and merge scenario\n", "8fb8538799b2481750eac52716931d895d029b7c");
    }

    @Test
    public void testLogFirstCommitForNonexistentCommitId() {
        SingleCommitRevListStdioHandler handler =
                new SingleCommitRevListStdioHandler(null, true, false, UNLIMITED_MESSAGE_LENGTH);

        RpcLogRequest.Builder builder = RpcLogRequest.newBuilder()
                .setLogOptions(RpcRevListOptions.newBuilder()
                        .addRevs(ByteString.copyFromUtf8("042fb0cc198e7320b59a025689b45a2cb52f4683"))
                        .setLimit(1))
                .setRepository(repository.getId());

        CommitNotFoundException thrown = assertThrows(CommitNotFoundException.class, () -> log(builder, handler));
        assertSame(Status.Code.NOT_FOUND, thrown.getStatus().getCode());
    }

    @Test
    public void testLogFirstCommitForNonexistentCommitIdIgnoreMissing() {
        SingleCommitRevListStdioHandler handler =
                new SingleCommitRevListStdioHandler(null, true, false, UNLIMITED_MESSAGE_LENGTH);

        RpcLogRequest.Builder builder = RpcLogRequest.newBuilder()
                .setLogOptions(RpcRevListOptions.newBuilder()
                        .addRevs(ByteString.copyFromUtf8("042fb0cc198e7320b59a025689b45a2cb52f4683"))
                        .setIgnoreMissing(true)
                        .setLimit(1))
                .setRepository(repository.getId());

        assertNull(log(builder, handler));
    }

    @Test
    public void testFirstCommitForNonexistentPath() {
        SingleCommitRevListStdioHandler handler =
                new SingleCommitRevListStdioHandler(null, true, false, UNLIMITED_MESSAGE_LENGTH);

        RpcLogRequest.Builder builder = RpcLogRequest.newBuilder()
                .setLogOptions(RpcRevListOptions.newBuilder()
                        .addPaths(ByteString.copyFromUtf8("modification/mod_file2.txt"))
                        .addRevs(ByteString.copyFromUtf8("HEAD"))
                        .setLimit(1))
                .setRepository(repository.getId());

        assertNull(log(builder, handler));
    }

    @Test
    public void testLogFirstCommitForPath() {
        SingleCommitRevListStdioHandler handler =
                new SingleCommitRevListStdioHandler("modification/mod_file.txt", true,
                        true, UNLIMITED_MESSAGE_LENGTH);

        RpcLogRequest.Builder builder = RpcLogRequest.newBuilder()
                .setLogOptions(RpcRevListOptions.newBuilder()
                        .addPaths(ByteString.copyFromUtf8("modification/mod_file.txt"))
                        .addRevs(ByteString.copyFromUtf8("HEAD"))
                        .setLimit(1)
                        .setFormat(revListToLogFormat(handler)))
                .setRepository(repository.getId());

        Commit commit = log(builder, handler);

        assertNotNull(commit);
        assertCommit(commit, "Michael Heemskerk", "mheemskerk@atlassian.com",
                1292816091, "bf3346620ec9161977fc23dee1240a7991e64ca1",
                "edited modification/mod_file.txt\n", "e26cab95a50f6e99d2c46c498a23cee307cc8e27");
    }

    @Test
    public void testLogFirstCommitForPathWithInvalidCommitId() {
        SingleCommitRevListStdioHandler handler =
                new SingleCommitRevListStdioHandler("modification/mod_file.txt", true,
                        true, UNLIMITED_MESSAGE_LENGTH);

        RpcLogRequest.Builder builder = RpcLogRequest.newBuilder()
                .setLogOptions(RpcRevListOptions.newBuilder()
                        .addPaths(ByteString.copyFromUtf8("modification/mod_file.txt"))
                        .addRevs(ByteString.copyFromUtf8("invalid"))
                        .setLimit(1))
                .setRepository(repository.getId());

        CommitNotFoundException thrown = assertThrows(CommitNotFoundException.class, () -> log(builder, handler));
        assertSame(Status.Code.NOT_FOUND, thrown.getStatus().getCode());
    }

    @Test
    public void testLogFirstCommitForRootCommit() {
        SingleCommitRevListStdioHandler handler =
                new SingleCommitRevListStdioHandler(null, true, false,
                        UNLIMITED_MESSAGE_LENGTH);

        RpcLogRequest.Builder builder = RpcLogRequest.newBuilder()
                .setLogOptions(RpcRevListOptions.newBuilder()
                        .addRevs(ByteString.copyFromUtf8("409a2180fd95b744df8d817e770b7f8c853f78dc"))
                        .setFormat(revListToLogFormat(handler)))
                .setRepository(repository.getId());

        Commit commit = log(builder, handler);

        assertNotNull(commit);
        //When this commit was created, GIT_AUTHOR_DATE was exported to use a date in the past, but
        //GIT_COMMITTER_DATE was not exported. As a result, this commit has distinct timestamps
        assertCommit(commit, "Michael Heemskerk", "mheemskerk@atlassian.com", 1263078610,
                "Michael Heemskerk", "mheemskerk@atlassian.com", 1292812705,
                "409a2180fd95b744df8d817e770b7f8c853f78dc", "out of order commits scenario, initial commit\n");

        Collection<String> parents = commit.getParents();
        assertNotNull(parents);
        assertTrue(parents.isEmpty());
    }

    @Test
    public void testLogFollowingCopy() {
        RpcLogRequest.Builder builder = RpcLogRequest.newBuilder()
                .setFollow(true)
                .setLogOptions(RpcRevListOptions.newBuilder()
                        .addPaths(ByteString.copyFromUtf8("binary/D.zip"))
                        .setFormat("%H"))
                .setRepository(repository.getId());

        Queue<String> commitIds = log(builder, new QueueHandler());

        assertNotNull(commitIds);
        assertEquals("0a943a29376f2336b78312d99e65da17048951db", commitIds.poll()); //Copy from C to D
        assertEquals("e0a3f6d8c804c375f49d7609328111e8dc75a4bc", commitIds.poll()); //Adding C
    }

    @Test
    public void testLogFollowingEnabledViaConfiguration() {
        RpcLogRequest.Builder builder = RpcLogRequest.newBuilder()
                .setLogOptions(RpcRevListOptions.newBuilder()
                        .addPaths(ByteString.copyFromUtf8("binary/B.zip"))
                        .setFormat("%H"))
                .setOptions(RpcGitOptions.newBuilder()
                        .putConfig("log.follow", ByteString.copyFromUtf8("true")))
                .setRepository(repository.getId());

        Queue<String> commitIds = log(builder, new QueueHandler());

        assertNotNull(commitIds);
        assertEquals("2d8897c9ac29ce42c3442cf80ac977057045e7f6", commitIds.poll()); //Rename from A to B
        assertEquals("9c05f43f859375e392d90d23a13717c16d0fdcda", commitIds.poll()); //Adding A
    }

    @Test
    public void testLogFollowingRename() {
        RpcLogRequest.Builder builder = RpcLogRequest.newBuilder()
                .setFollow(true)
                .setLogOptions(RpcRevListOptions.newBuilder()
                        .addPaths(ByteString.copyFromUtf8("binary/B.zip"))
                        .setFormat("%H"))
                .setRepository(repository.getId());

        Queue<String> commitIds = log(builder, new QueueHandler());

        assertNotNull(commitIds);
        assertEquals("2d8897c9ac29ce42c3442cf80ac977057045e7f6", commitIds.poll()); //Rename from A to B
        assertEquals("9c05f43f859375e392d90d23a13717c16d0fdcda", commitIds.poll()); //Adding A
    }

    @Test
    public void testLogFollowTwoPaths() {
        RpcLogRequest.Builder builder = RpcLogRequest.newBuilder()
                .setFollow(true)
                .setLogOptions(RpcRevListOptions.newBuilder()
                        .addPaths(ByteString.copyFromUtf8("binary/B.zip"))
                        .addPaths(ByteString.copyFromUtf8("binary/D.zip")))
                .setRepository(repository.getId());

        CommandFailedException thrown = assertThrows(CommandFailedException.class, () -> log(builder, new QueueHandler()));
        assertSame(Status.Code.INTERNAL, thrown.getStatus().getCode());
    }

    @Test
    public void testLogFollowWithoutPath() {
        RpcLogRequest.Builder builder = RpcLogRequest.newBuilder().setFollow(true).setRepository(repository.getId());

        CommandFailedException thrown = assertThrows(CommandFailedException.class, () -> log(builder, new QueueHandler()));
        assertSame(Status.Code.INTERNAL, thrown.getStatus().getCode());
    }

    @Test
    public void testLogIncludeMergeCommits() {
        List<Commit> commits = new ArrayList<>();
        CallbackRevListStdioHandler handler =
                new CallbackRevListStdioHandler((commit, refChange) -> commits.add(commit),
                        true, UNLIMITED_MESSAGE_LENGTH, Collections.emptyList());

        RpcLogRequest.Builder builder = RpcLogRequest.newBuilder()
                .setLogOptions(RpcRevListOptions.newBuilder()
                        .setAll(true)
                        .setFormat(revListToLogFormat(handler))
                        .setMerges(RpcRevListMerges.MERGES_INCLUDE))
                .setRepository(mergeCommits.getId());

        log(builder, handler);

        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 testLogInvalidRev() {
        RpcLogRequest.Builder builder = RpcLogRequest.newBuilder()
                .setLogOptions(RpcRevListOptions.newBuilder()
                        .addAllRevs(toByteStringsLazily(ImmutableList.of("--topo-order", "HEAD"))))
                .setRepository(repository.getId());

        StatusRuntimeException thrown = assertThrows(StatusRuntimeException.class,
                () -> log(builder, new StringStdioHandler()));
        assertSame(Status.Code.INVALID_ARGUMENT, thrown.getStatus().getCode());
        assertThat(thrown, HasGitError.invalidCommitish("--topo-order"));
    }

    @Test
    public void testLogInvalidRevWithLeadingSpace() {
        RpcLogRequest.Builder builder = RpcLogRequest.newBuilder()
                .setLogOptions(RpcRevListOptions.newBuilder()
                        .addRevs(ByteString.copyFromUtf8(" --topo-order"))
                        .addRevs(ByteString.copyFromUtf8("HEAD")))
                .setRepository(repository.getId());

        StatusRuntimeException thrown = assertThrows(StatusRuntimeException.class,
                () -> log(builder, new StringStdioHandler()));
        assertSame(Status.Code.INVALID_ARGUMENT, thrown.getStatus().getCode());
        assertThat(thrown, HasGitError.invalidCommitish("--topo-order"));
    }

    @Test
    public void testLogNoWalk() {
        List<Commit> commits = new ArrayList<>();
        CallbackRevListStdioHandler handler =
                new CallbackRevListStdioHandler((commit, refChange) -> commits.add(commit),
                        true, UNLIMITED_MESSAGE_LENGTH, Collections.emptyList());

        RpcLogRequest.Builder builder = RpcLogRequest.newBuilder()
                .setLogOptions(RpcRevListOptions.newBuilder()
                        .setAll(true)
                        .setFormat(revListToLogFormat(handler))
                        .setWalk(RpcRevListWalk.WALK_NO))
                .setRepository(repository.getId());

        log(builder, handler);

        assertEquals(5, commits.size());
        assertEquals("0a943a29376f2336b78312d99e65da17048951db", commits.get(0).getId());
        assertEquals("6053a1eaa1c009dd11092d09a72f3c41af1b59ad", commits.get(1).getId());
        assertEquals("d6edcbf924697ab811a867421dab60d954ccad99", commits.get(2).getId());
        assertEquals("57150c54c38d6570b2fd5e6d6cfc19476de44e84", commits.get(3).getId());
        assertEquals("2e10dd2d1d5eea9291b296e78312e8a703964a95", commits.get(4).getId());
    }

    @Test
    public void testLogOnlyMergeCommits() {
        List<Commit> commits = new ArrayList<>();
        CallbackRevListStdioHandler handler =
                new CallbackRevListStdioHandler((commit, refChange) -> commits.add(commit),
                        true, UNLIMITED_MESSAGE_LENGTH, Collections.emptyList());

        RpcLogRequest.Builder builder = RpcLogRequest.newBuilder()
                .setLogOptions(RpcRevListOptions.newBuilder()
                        .setAll(true)
                        .setFormat(revListToLogFormat(handler))
                        .setMerges(RpcRevListMerges.MERGES_ONLY))
                .setRepository(mergeCommits.getId());

        log(builder, handler);

        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());
    }

    @Test
    public void testLogStdInWithRevs() {
        RpcLogRequest.Builder builder = RpcLogRequest.newBuilder()
                .setLogOptions(RpcRevListOptions.newBuilder()
                        .addRevs(ByteString.copyFromUtf8("basic_branching"))
                        .addRevs(ByteString.copyFromUtf8("backdated_annotated_tag"))
                        .setFormat("%H")
                        .setWalk(RpcRevListWalk.WALK_NO))
                .setRepository(repository.getId());

        BidirectionalFragmentResponseObserver<RpcLogStdinFragment, List<String>> responseObserver =
                new BidirectionalFragmentResponseObserver<>(new LogStdioHandler(Stream.of("master")),
                        new GitProcessCompletionHandler(),
                        new RequestFragmentCreator<RpcLogStdinFragment>() {

                            @Nonnull
                            @Override
                            public RpcLogStdinFragment forStdin(@Nonnull RpcStreamChunk stdin) {
                                return RpcLogStdinFragment.newBuilder()
                                        .setStdin(stdin)
                                        .build();
                            }

                            @Nonnull
                            @Override
                            public RpcLogStdinFragment getInitial() {
                                return RpcLogStdinFragment.newBuilder()
                                        .setRequest(builder)
                                        .build();
                            }
                        });

        stub.logStdin(responseObserver);
        List<String> commits = responseObserver.asResult();

        assertNotNull(commits);
        assertEquals(3, commits.size());
        assertEquals("0a943a29376f2336b78312d99e65da17048951db", commits.get(0)); //master, via stdin
        assertEquals("d6edcbf924697ab811a867421dab60d954ccad99", commits.get(1)); //basic_branching, via cli
        assertEquals("57150c54c38d6570b2fd5e6d6cfc19476de44e84", commits.get(2)); //backdated_annotated_tag, via cli
    }

    @Test
    public void testLogTags() {
        List<Commit> commits = new ArrayList<>();
        CallbackRevListStdioHandler handler =
                new CallbackRevListStdioHandler((commit, refChange) -> commits.add(commit),
                        true, UNLIMITED_MESSAGE_LENGTH, Collections.emptyList());

        RpcLogRequest.Builder builder = RpcLogRequest.newBuilder()
                .setLogOptions(RpcRevListOptions.newBuilder()
                        .setFormat(handler.getFormat())
                        .setTags(true)
                        .setWalk(RpcRevListWalk.WALK_NO))
                .setRepository(repository.getId());

        log(builder, handler);

        assertEquals(2, commits.size());
        assertEquals("0a943a29376f2336b78312d99e65da17048951db", commits.get(0).getId());
        assertEquals("57150c54c38d6570b2fd5e6d6cfc19476de44e84", commits.get(1).getId());
    }

    @DirtiesRepo
    @Test
    public void testLogTagsDoesNotIncludeHead() {
        //First, set HEAD to a nonexistent ref. If HEAD is included in the rev-list arguments by the builder,
        //this will cause the command to fail
        builderFactory.builder(repository).symbolicRef()
                .set("HEAD", "refs/heads/nonexistent")
                .build()
                .run();

        RpcLogRequest.Builder builder = RpcLogRequest.newBuilder()
                .setLogOptions(RpcRevListOptions.newBuilder()
                        .setFormat("%H")
                        .setTags(true))
                .setRepository(repository.getId());

        Queue<String> commitIds = log(builder, new QueueHandler());

        assertNotNull(commitIds);
        assertTrue(commitIds.contains("0a943a29376f2336b78312d99e65da17048951db"));  //signed_tag, retagged_signed_tag
        assertTrue(commitIds.contains("57150c54c38d6570b2fd5e6d6cfc19476de44e84"));  //backdated_annotated_tag
        assertEquals(20, commitIds.size());
    }

    /**
     * When no refs are specified, the builder should automatically add "HEAD"
     */
    @Test
    public void testLogWithNoRefs() {
        SingleCommitRevListStdioHandler handler =
                new SingleCommitRevListStdioHandler(null, true, false, UNLIMITED_MESSAGE_LENGTH);

        RpcLogRequest.Builder builder = RpcLogRequest.newBuilder()
                .setLogOptions(RpcRevListOptions.newBuilder()
                        .setLimit(1)
                .setFormat(revListToLogFormat(handler)))
                .setRepository(repository.getId());

        Commit commit = log(builder, handler);

        assertNotNull(commit);
        assertEquals("0a943a29376f2336b78312d99e65da17048951db", commit.getId());
    }

    private static String revListToLogFormat(AbstractCommitRevListStdioHandler<?> stdoutHandler) {
        return "commit %H%n" + stdoutHandler.getFormat();
    }

    private RpcFormatPatchRequest.Builder createFormatPatchBuilder() {
        return RpcFormatPatchRequest.newBuilder()
                .setRepository(repository.getId());
    }

    private <T> T log(RpcLogRequest.Builder builder, ProcessStdioHandler<T> handler) {
        FragmentResponseObserver<T> responseObserver = new FragmentResponseObserver<>(
                handler, new GitProcessCompletionHandler());

        stub.log(builder.build(), responseObserver);
        return responseObserver.asResult();
    }

    private String streamPatch(RpcFormatPatchRequest.Builder builder) {
        ByteArrayOutputStream stream = new ByteArrayOutputStream();

        return streamPatch(builder, stream).toString();
    }

    private <T extends OutputStream> T streamPatch(RpcFormatPatchRequest.Builder builder, T stream) {
        SupplierStdoutHandler handler = new SupplierStdoutHandler(contentType -> {
            assertThat(contentType, equalTo("text/plain; charset=UTF-8"));
            return stream;
        });

        FragmentResponseObserver<Void> responseObserver =
                new FragmentResponseObserver<>(handler, new GitProcessCompletionHandler());

        stub.formatPatch(builder.build(), responseObserver);

        responseObserver.asResult();

        return stream;
    }

    private static class LogStdioHandler extends AbstractLineStdioHandler<List<String>> {

        private final List<String> lines;
        private final LineEncoder stdinEncoder;

        private LogStdioHandler(Stream<String> stdin) {
            super(StandardCharsets.UTF_8);

            lines = new ArrayList<>();
            stdinEncoder = new LineEncoder(stdin, UTF_8);
        }

        @Override
        public List<String> getResult() {
            return lines;
        }

        @Override
        public void onStart(@Nonnull OsProcess process) {
            process.wantWrite();
        }

        @Override
        public boolean onStdinReady(@Nonnull ByteBuffer buffer) {
            if (stdinEncoder.encodeTo(buffer)) {
                return true;
            }
            closeStdin();
            return false;
        }

        @Override
        protected boolean onStdout(@Nonnull String line, boolean truncated) {
            lines.add(line);
            return true;
        }
    }

    public static class QueueHandler extends AbstractLineStdioHandler<Queue<String>> {

        private final Queue<String> queue;

        public QueueHandler() {
            super(StandardCharsets.UTF_8);

            queue = new LinkedList<>();
        }

        @Override
        public Queue<String> getResult() {
            return queue;
        }

        @Override
        protected boolean onStdout(@Nonnull String line, boolean truncated) {
            queue.add(line);

            return true;
        }
    }
}
