package com.atlassian.bitbucket.mesh.git.pull;

import com.atlassian.bitbucket.mesh.AbstractGrpcTest;
import com.atlassian.bitbucket.mesh.MockResponseObserver;
import com.atlassian.bitbucket.mesh.git.GitRefPattern;
import com.atlassian.bitbucket.mesh.git.GitRepositories;
import com.atlassian.bitbucket.mesh.git.binary.GitBinary;
import com.atlassian.bitbucket.mesh.git.merge.GitMergeStrategy;
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.atlassian.bitbucket.mesh.util.Version;
import com.google.protobuf.ByteString;
import io.grpc.ManagedChannel;
import io.grpc.Status;
import io.grpc.StatusRuntimeException;
import io.grpc.stub.StreamObserver;
import org.junit.Before;
import org.junit.Test;
import org.springframework.beans.factory.annotation.Autowired;

import javax.annotation.Resource;
import java.util.List;
import java.util.regex.Pattern;
import java.util.stream.Collectors;

import static org.hamcrest.Matchers.*;
import static org.junit.Assume.assumeTrue;

public class PullRequestMergeRequestTest extends AbstractGrpcTest {

    @Autowired
    private GitBinary binary;
    private PullRequestServiceGrpc.PullRequestServiceBlockingStub blockingStub;
    @Resource
    private ManagedChannel managedChannel;
    @Repo(GitRepositories.MERGE)
    private Repository merge;
    private RpcPullRequest pullRequest;
    @Repo
    private Repository repository;
    private PullRequestServiceGrpc.PullRequestServiceStub stub;
    private Version version;

    @Before
    public void setup() {
        blockingStub = PullRequestServiceGrpc.newBlockingStub(managedChannel);
        stub = PullRequestServiceGrpc.newStub(managedChannel);
        version = binary.getVersion();
    }

    @DirtiesRepo("merge")
    @Test
    public void testMerge() throws Exception {
        pullRequest = preparePullRequest("branch_that_has_same_text_file_modified_on_both_clean_merge",
                "306d584aa574fa74901d3e288b2931b2c4f0e927",
                "7fa77c6e34c6c53de0bb880fc2b0b7924a601cd2",
                merge);

        // Uses the default merge strategy
        RpcMergePullRequestRequest.Builder builder = createMergePullRequestBuilder()
                .setStrategy(RpcMergeStrategy.MERGE_STRATEGY_NO_FF);

        RpcBranch target = mergeWithResponse(builder);
        assertNotNull(target);
        assertNotEquals("7fa77c6e34c6c53de0bb880fc2b0b7924a601cd2", target.getLatestCommit());
    }

    @DirtiesRepo("merge")
    @Test
    public void testMergeWithQuotesInMessage() throws Exception {
        pullRequest = preparePullRequest("branch_that_has_same_text_file_modified_on_both_clean_merge",
                "306d584aa574fa74901d3e288b2931b2c4f0e927",
                "7fa77c6e34c6c53de0bb880fc2b0b7924a601cd2",
                merge);

        RpcMergePullRequestRequest.Builder builder = createMergePullRequestBuilder()
                .setMessage(ByteString.copyFromUtf8("This merge \"should\" succeed and be fetched"))
                .setStrategy(RpcMergeStrategy.MERGE_STRATEGY_NO_FF);

        RpcBranch target = mergeWithResponse(builder);
        assertNotNull(target);
        assertNotEquals("7fa77c6e34c6c53de0bb880fc2b0b7924a601cd2", target.getLatestCommit());
    }

    @DirtiesRepo("merge")
    @Test
    public void testMergeCrossRepositoryWithAllowUnrelated() throws Exception {
        pullRequest = preparePullRequest("master",
                "0a943a29376f2336b78312d99e65da17048951db",
                repository,
                "branch_that_has_same_text_file_modified_on_both_clean_merge_trgt",
                "7fa77c6e34c6c53de0bb880fc2b0b7924a601cd2",
                merge);

        RpcMergePullRequestRequest.Builder builder = createMergePullRequestBuilder()
                .setAllowUnrelated(true)
                .setStrategy(RpcMergeStrategy.MERGE_STRATEGY_NO_FF);

        RpcBranch target = mergeWithResponse(builder);
        assertNotNull(target);
        assertNotEquals("7fa77c6e34c6c53de0bb880fc2b0b7924a601cd2", target.getLatestCommit());
    }

    @Test
    public void testMergeCrossRepositoryWithoutAllowUnrelated() {
        pullRequest = preparePullRequest("master",
                "0a943a29376f2336b78312d99e65da17048951db",
                repository,
                "branch_that_has_same_text_file_modified_on_both_clean_merge_trgt",
                "7fa77c6e34c6c53de0bb880fc2b0b7924a601cd2",
                merge);

        RpcMergePullRequestRequest.Builder builder = createMergePullRequestBuilder()
                .setStrategy(RpcMergeStrategy.MERGE_STRATEGY_NO_FF);

        StatusRuntimeException thrown = assertThrows(StatusRuntimeException.class, () -> mergeWithResponse(builder));
        assertSame(Status.Code.INVALID_ARGUMENT, thrown.getStatus().getCode());
        assertThat(thrown, HasGitError.invalidMerge(RpcInvalidMerge.Reason.UNRELATED));
    }

    //Note: The various fast-forward strategies all use the same MergeStrategy, which is used for this test, so their
    //      output handling are guaranteed to be the same. To reduce redundant testing, this test is the only one for
    //      conflicts on fast-forward strategies.
    @Test
    public void testMergeHasConflicts() throws Exception {
        pullRequest = preparePullRequest("branch_that_has_same_text_file_modified",
                "5bb214e6dd15643dabfe4b6b0739becfb1e59d9f",
                "c1654113b22f65f988f5a570d8cf32c7288ee7d2",
                merge);

        RpcMergePullRequestRequest.Builder builder = createMergePullRequestBuilder()
                .setMessage(ByteString.copyFromUtf8("This merge should fail with conflicts"))
                .setStrategy(RpcMergeStrategy.MERGE_STRATEGY_SQUASH);

        RpcMergeConflicts conflicts = mergeWithConflicts(builder);
        assertEquals(1, conflicts.getConflictsCount());

        RpcMergeConflict conflict = conflicts.getConflicts(0);
        assertEquals("CONFLICT (content): Merge conflict in file.txt",
                conflict.getMessage());

        assertEquals(1, conflict.getMoreMessagesCount());
        assertEquals("Squash commit -- not updating HEAD",
                conflict.getMoreMessages(0));
    }

    @Test
    public void testMergeThrowsOnOutdatedTarget() {
        pullRequest = preparePullRequest("branch_that_has_same_text_file_modified_on_both_clean_merge",
                "306d584aa574fa74901d3e288b2931b2c4f0e927",
                "e527f8731b48bca46d7e99e270d05a9a3fa91107",
                merge);

        RpcMergePullRequestRequest.Builder builder = createMergePullRequestBuilder()
                .setMessage(ByteString.copyFromUtf8("This merge should fail on fetch (rejected, non-fast-forward)"))
                .setStrategy(RpcMergeStrategy.MERGE_STRATEGY_NO_FF);

        StatusRuntimeException thrown = assertThrows(StatusRuntimeException.class, () -> mergeWithResponse(builder));
        assertSame(Status.Code.ABORTED, thrown.getStatus().getCode());
        assertThat(thrown, HasGitError.nonFastForwardUpdate("refs/heads/branch_that_has_same_text_file_modified_on_both_clean_merge_trgt"));
    }

    @DirtiesRepo("merge")
    @Test
    public void testMergeWithFastForwardOnlyStrategy() throws Exception {
        pullRequest = preparePullRequest("branch_that_has_same_text_file_modified_on_both_clean_merge_src",
                "306d584aa574fa74901d3e288b2931b2c4f0e927",
                merge,
                "master",
                "017caf31eca7c46eb3d1800fcac431cfa7147a01",
                merge);

        RpcMergePullRequestRequest.Builder builder = createMergePullRequestBuilder()
                .setStrategy(RpcMergeStrategy.MERGE_STRATEGY_FF_ONLY);

        RpcBranch target = mergeWithResponse(builder);
        assertNotNull(target);
        //The source commit is fast-forward from the target, so it should "merge" without creating a merge commit
        assertEquals("306d584aa574fa74901d3e288b2931b2c4f0e927", target.getLatestCommit());
    }

    @Test
    public void testMergeWithFastForwardOnlyStrategyThrowsInsteadOfMerging() {
        pullRequest = preparePullRequest("branch_that_has_same_text_file_modified_on_both_clean_merge",
                "306d584aa574fa74901d3e288b2931b2c4f0e927",
                "7fa77c6e34c6c53de0bb880fc2b0b7924a601cd2",
                merge);

        RpcMergePullRequestRequest.Builder builder = createMergePullRequestBuilder()
                .setStrategy(RpcMergeStrategy.MERGE_STRATEGY_FF_ONLY);

        StatusRuntimeException thrown = assertThrows(StatusRuntimeException.class, () -> mergeWithResponse(builder));
        assertSame(Status.Code.ABORTED, thrown.getStatus().getCode());
        assertThat(thrown, HasGitError.nonFastForwardMerge("306d584aa574fa74901d3e288b2931b2c4f0e927"));
    }

    @DirtiesRepo("merge")
    @Test
    public void testMergeWithFastForwardStrategy() throws Exception {
        pullRequest = preparePullRequest("branch_that_has_same_text_file_modified_on_both_clean_merge_src",
                "306d584aa574fa74901d3e288b2931b2c4f0e927",
                merge,
                "master",
                "017caf31eca7c46eb3d1800fcac431cfa7147a01",
                merge);

        RpcMergePullRequestRequest.Builder builder = createMergePullRequestBuilder()
                .setStrategy(RpcMergeStrategy.MERGE_STRATEGY_FF);

        RpcBranch target = mergeWithResponse(builder);
        assertNotNull(target);
        //The source commit is fast-forward from the target, so it should "merge" without creating a merge commit
        assertEquals("306d584aa574fa74901d3e288b2931b2c4f0e927", target.getLatestCommit());
    }

    @DirtiesRepo("merge")
    @Test
    public void testMergeWithFastForwardStrategyMergesIfNecessary() throws Exception {
        pullRequest = preparePullRequest("branch_that_has_same_text_file_modified_on_both_clean_merge",
                "306d584aa574fa74901d3e288b2931b2c4f0e927",
                "7fa77c6e34c6c53de0bb880fc2b0b7924a601cd2",
                merge);

        RpcMergePullRequestRequest.Builder builder = createMergePullRequestBuilder()
                .setStrategy(RpcMergeStrategy.MERGE_STRATEGY_FF);

        RpcBranch target = mergeWithResponse(builder);
        assertNotNull(target);
        assertNotEquals("306d584aa574fa74901d3e288b2931b2c4f0e927", target.getLatestCommit());
        assertNotEquals("7fa77c6e34c6c53de0bb880fc2b0b7924a601cd2", target.getLatestCommit());
    }

    @DirtiesRepo("merge")
    @Test
    public void testMergeWithRebaseFastForwardOnlyStrategy() throws Exception {
        pullRequest = preparePullRequest("branch_that_has_same_text_file_modified_on_both_clean_merge",
                "306d584aa574fa74901d3e288b2931b2c4f0e927",
                "7fa77c6e34c6c53de0bb880fc2b0b7924a601cd2",
                merge);

        RpcMergePullRequestRequest.Builder builder = createMergePullRequestBuilder()
                .setStrategy(RpcMergeStrategy.MERGE_STRATEGY_REBASE_FF_ONLY);

        RpcBranch target = mergeWithResponse(builder);
        assertNotNull(target);
        assertNotEquals("7fa77c6e34c6c53de0bb880fc2b0b7924a601cd2", target.getLatestCommit());
    }

    @DirtiesRepo("merge")
    @Test
    public void testMergeWithRebaseFastForwardOnlyStrategyFastForwardsIfPossible() throws Exception {
        pullRequest = preparePullRequest("branch_that_has_same_text_file_modified_on_both_clean_merge_src",
                "306d584aa574fa74901d3e288b2931b2c4f0e927",
                merge,
                "master",
                "017caf31eca7c46eb3d1800fcac431cfa7147a01",
                merge);

        RpcMergePullRequestRequest.Builder builder = createMergePullRequestBuilder()
                .setStrategy(RpcMergeStrategy.MERGE_STRATEGY_REBASE_FF_ONLY);

        RpcBranch target = mergeWithResponse(builder);
        assertNotNull(target);
        //The source commit is fast-forward from the target, so it should "merge" without creating a merge commit
        assertEquals("306d584aa574fa74901d3e288b2931b2c4f0e927", target.getLatestCommit());
    }

    @Test
    public void testMergeWithRebaseFastForwardOnlyStrategyThrowsOnEmptyRebase() {
        pullRequest = preparePullRequest("branch_that_differ_by_empty_commit",
                "017caf31eca7c46eb3d1800fcac431cfa7147a01",
                "298924b8c403240eaf89dcda0cce7271620ab2f6",
                merge);

        RpcMergePullRequestRequest.Builder builder = createMergePullRequestBuilder()
                .setStrategy(RpcMergeStrategy.MERGE_STRATEGY_REBASE_FF_ONLY);

        StatusRuntimeException thrown = assertThrows(StatusRuntimeException.class, () -> mergeWithResponse(builder));
        assertSame(Status.Code.FAILED_PRECONDITION, thrown.getStatus().getCode());
        assertThat(thrown, HasGitError.emptyRebaseMerge());
    }

    //Note: Rebase "merges" use a different MergeStrategy, so this test is necessary to assure the proper error
    //      handling is still performed by the handlers being used.
    @Test
    public void testMergeWithRebaseFastForwardOnlyStrategyThrowsOnConflicts() {
        pullRequest = preparePullRequest("branch_that_has_same_text_file_modified",
                "5bb214e6dd15643dabfe4b6b0739becfb1e59d9f",
                "c1654113b22f65f988f5a570d8cf32c7288ee7d2",
                merge);

        RpcMergePullRequestRequest.Builder builder = createMergePullRequestBuilder()
                .setStrategy(RpcMergeStrategy.MERGE_STRATEGY_REBASE_FF_ONLY);

        StatusRuntimeException thrown = assertThrows(StatusRuntimeException.class, () -> mergeWithResponse(builder));
        assertSame(Status.Code.FAILED_PRECONDITION, thrown.getStatus().getCode());
        assertThat(thrown, HasGitError.rebaseConflicted("5bb214e6dd15643dabfe4b6b0739becfb1e59d9f"));
    }

    /**
     * Verifies the {@link GitMergeStrategy#REBASE_NO_FF "Rebase and merge"} strategy.
     * <p>
     * Note that, unlike {@link GitMergeStrategy#REBASE_FF_ONLY "Rebase and fast-forward}, there aren't additional
     * tests for empty rebases or conflicts. The rebased commits are <i>always</i> fast-forward from the toRef, so
     * {@code git merge --no-ff} should never fail due to conflicts.
     */
    @DirtiesRepo("merge")
    @Test
    public void testMergeWithRebaseNoFastForwardStrategy() throws Exception {
        pullRequest = preparePullRequest("branch_that_has_same_text_file_modified_on_both_clean_merge",
                "306d584aa574fa74901d3e288b2931b2c4f0e927",
                "7fa77c6e34c6c53de0bb880fc2b0b7924a601cd2",
                merge);

        RpcMergePullRequestRequest.Builder builder = createMergePullRequestBuilder()
                .setStrategy(RpcMergeStrategy.MERGE_STRATEGY_REBASE_NO_FF);

        RpcBranch target = mergeWithResponse(builder);
        assertNotNull(target);
        assertNotEquals("7fa77c6e34c6c53de0bb880fc2b0b7924a601cd2", target.getLatestCommit());
    }

    @DirtiesRepo("merge")
    @Test
    public void testMergeWithRebaseNoFastForwardStrategyAlwaysMerges() throws Exception {
        pullRequest = preparePullRequest("branch_that_has_same_text_file_modified_on_both_clean_merge_src",
                "306d584aa574fa74901d3e288b2931b2c4f0e927",
                merge,
                "master",
                "017caf31eca7c46eb3d1800fcac431cfa7147a01",
                merge);

        RpcMergePullRequestRequest.Builder builder = createMergePullRequestBuilder()
                .setStrategy(RpcMergeStrategy.MERGE_STRATEGY_REBASE_NO_FF);

        RpcBranch target = mergeWithResponse(builder);
        assertNotNull(target);
        assertNotEquals("017caf31eca7c46eb3d1800fcac431cfa7147a01", target.getLatestCommit());
    }

    @DirtiesRepo("merge")
    @Test
    public void testMergeWithSquashFastForwardOnlyStrategy() throws Exception {
        pullRequest = preparePullRequest("branch_that_has_same_text_file_modified_on_both_clean_merge_src",
                "306d584aa574fa74901d3e288b2931b2c4f0e927",
                merge,
                "master",
                "017caf31eca7c46eb3d1800fcac431cfa7147a01",
                merge);

        RpcMergePullRequestRequest.Builder builder = createMergePullRequestBuilder()
                .setStrategy(RpcMergeStrategy.MERGE_STRATEGY_SQUASH_FF_ONLY);

        RpcBranch target = mergeWithResponse(builder);
        assertNotNull(target);
        assertNotEquals("017caf31eca7c46eb3d1800fcac431cfa7147a01", target.getLatestCommit());
    }

    @Test
    public void testMergeWithSquashFastForwardOnlyStrategyThrowsInsteadOfMerging() {
        pullRequest = preparePullRequest("branch_that_has_same_text_file_modified_on_both_clean_merge",
                "306d584aa574fa74901d3e288b2931b2c4f0e927",
                "7fa77c6e34c6c53de0bb880fc2b0b7924a601cd2",
                merge);

        RpcMergePullRequestRequest.Builder builder = createMergePullRequestBuilder()
                .setStrategy(RpcMergeStrategy.MERGE_STRATEGY_SQUASH_FF_ONLY);

        StatusRuntimeException thrown = assertThrows(StatusRuntimeException.class, () -> mergeWithResponse(builder));
        assertSame(Status.Code.ABORTED, thrown.getStatus().getCode());
        assertThat(thrown, HasGitError.nonFastForwardMerge("306d584aa574fa74901d3e288b2931b2c4f0e927"));
    }

    @DirtiesRepo("merge")
    @Test
    public void testMergeWithSquashStrategy() throws Exception {
        pullRequest = preparePullRequest("branch_that_has_same_text_file_modified_on_both_clean_merge",
                "306d584aa574fa74901d3e288b2931b2c4f0e927",
                "7fa77c6e34c6c53de0bb880fc2b0b7924a601cd2",
                merge);

        RpcMergePullRequestRequest.Builder builder = createMergePullRequestBuilder()
                .setStrategy(RpcMergeStrategy.MERGE_STRATEGY_SQUASH);

        RpcBranch target = mergeWithResponse(builder);
        assertNotNull(target);
        assertNotEquals("7fa77c6e34c6c53de0bb880fc2b0b7924a601cd2", target.getLatestCommit());
    }

    @Test
    public void testMergeWithSquashStrategyThrowsOnEmptyCommit() {
        pullRequest = preparePullRequest("refs/heads/branch_that_differ_by_empty_commit",
                "017caf31eca7c46eb3d1800fcac431cfa7147a01",
                "298924b8c403240eaf89dcda0cce7271620ab2f6",
                merge);

        RpcMergePullRequestRequest.Builder builder = createMergePullRequestBuilder()
                .setStrategy(RpcMergeStrategy.MERGE_STRATEGY_SQUASH);

        StatusRuntimeException thrown = assertThrows(StatusRuntimeException.class, () -> mergeWithResponse(builder));
        assertSame(Status.Code.FAILED_PRECONDITION, thrown.getStatus().getCode());
        assertThat(thrown, HasGitError.emptySquashMerge());
    }

    //Note: Squash merges use a different MergeStrategy, so this test is necessary to assure the proper error handling
    //      is still performed by the output handler being used.
    @Test
    public void testMergeWithSquashStrategyThrowsOnConflicts() throws Exception {
        pullRequest = preparePullRequest("refs/heads/branch_that_has_same_text_file_modified",
                "5bb214e6dd15643dabfe4b6b0739becfb1e59d9f",
                "c1654113b22f65f988f5a570d8cf32c7288ee7d2",
                merge);

        RpcMergePullRequestRequest.Builder builder = createMergePullRequestBuilder()
                .setStrategy(RpcMergeStrategy.MERGE_STRATEGY_SQUASH);

        RpcMergeConflicts mergeConflicts = mergeWithConflicts(builder);
        assertEquals(1, mergeConflicts.getConflictsCount());

        RpcMergeConflict conflict = mergeConflicts.getConflicts(0);
        assertEquals("CONFLICT (content): Merge conflict in file.txt", conflict.getMessage());

        assertEquals(1, conflict.getMoreMessagesCount());
        assertEquals("Squash commit -- not updating HEAD", conflict.getMoreMessages(0));
    }

    @Test
    public void testTryMergeForPullRequest() {
        pullRequest = preparePullRequest("branch_that_has_same_text_file_modified_on_both_clean_merge",
                "306d584aa574fa74901d3e288b2931b2c4f0e927",
                "7fa77c6e34c6c53de0bb880fc2b0b7924a601cd2",
                merge);

        RpcMergePullRequestRequest.Builder builder = createMergePullRequestBuilder()
                .setStrategy(RpcMergeStrategy.MERGE_STRATEGY_NO_FF);

        RpcTryPullRequestMergeResponse mergeResponse = tryMerge(builder);
        assertFalse(mergeResponse.hasConflicts());
    }

    @Test
    public void testTryMergeForPullRequestWithBinarySameNameAddedConflict() {
        runTryMerge("branch_that_has_different_binary_files_with_same_name_added",
                "f6f5c9134cc5c7282cbbd91f3ec52e4bd4671ef5",
                "1335a5b58bcdc27804915f352c883c5fa9024e5e",
                Pattern.compile("CONFLICT \\(add/add\\): Merge conflict in file.bin"),
                "file.bin");
    }

    @Test
    public void testTryMergeForPullRequestWithBinaryContentConflict() {
        runTryMerge("branch_that_has_same_binary_file_modified",
                "5c279d6c7a3a053a145905aa9682ce02c16449e9",
                "3ff1eee8ee06b82bc602083053d619029a78931a",
                Pattern.compile("CONFLICT \\(content\\): Merge conflict in file.bin"),
                "file.bin");
    }

    @Test
    public void testTryMergeForPullRequestWithConflictWithMoreMessages() {
        // The submodule extra helper messages only exist in Git 2.38+
        assumeTrue(version.compareTo(new Version(2, 38)) >= 0);

        pullRequest = preparePullRequest("branch_that_has_nested_submodule",
                "4a1e20264775c2accef9d715cef7950f21e4c2a5",
                "e03fa9a080fa006151721bfd9f1da220a16b1468",
                merge);

        RpcMergePullRequestRequest.Builder builder = createMergePullRequestBuilder()
                .setStrategy(RpcMergeStrategy.MERGE_STRATEGY_NO_FF);

        RpcTryPullRequestMergeResponse mergeResponse = tryMerge(builder);

        RpcMergeConflicts mergeConflicts = mergeResponse.getConflicts();

        assertEquals(1, mergeConflicts.getConflictsCount());
        RpcMergeConflict conflict = mergeConflicts.getConflicts(0);
        assertEquals("CONFLICT (submodule): Merge conflict in submodules/test", conflict.getMessage());

        assertEquals(2, conflict.getMoreMessagesCount());
        assertEquals("Recursive merging with submodules currently only supports trivial cases.", conflict.getMoreMessages(0));
        assertEquals("Please manually handle the merging of each conflicted submodule.", conflict.getMoreMessages(1));
    }

    @Test
    public void testTryMergeForPullRequestWithModifiedOnOneAndRemovedOnOtherConflict() {
        runTryMerge("branch_that_has_same_file_modified_and_removed",
                "59539850ff5fb3524c68284a9a40e61c0d5569a0",
                "2c98a0b510355efd24bbd0680e2325e0f5f1887e",
                Pattern.compile("(.+) deleted in (.+) and modified in (.+)\\.  ?Version (.+) of (.+) left in tree\\.?$"));
    }

    @Test
    public void testTryMergeForPullRequestWithMultipleConflicts() {
        pullRequest = preparePullRequest("branch_with_ignore_conflicts",
                "c927809216a016aee8635896daf7af1fce50f6e1",
                "74de5497dfca9731e455d60552f9a8906e5dc1ac",
                merge);

        RpcMergePullRequestRequest.Builder builder = createMergePullRequestBuilder()
                .setStrategy(RpcMergeStrategy.MERGE_STRATEGY_NO_FF);

        RpcTryPullRequestMergeResponse mergeResponse = tryMerge(builder);

        RpcMergeConflicts mergeConflicts = mergeResponse.getConflicts();
        assertEquals(2, mergeConflicts.getConflictsCount());

        List<String> messages = mergeConflicts.getConflictsList().stream()
                .map(RpcMergeConflict::getMessage)
                .collect(Collectors.toList());
        assertThat(messages, containsInAnyOrder(
                "CONFLICT (add/add): Merge conflict in .gitignore",
                "CONFLICT (content): Merge conflict in dir/file.txt"));
    }

    @Test
    public void testTryMergeForPullRequestWithSameNameAddedConflict() {
        runTryMerge("branch_that_has_different_files_with_same_name_added",
                "956766bea961e476bd50d8384a5a205ee1c899c0",
                "6d45f46d4c9347b3fd38b650f75ac43d6e4bb8b0",
                Pattern.compile("CONFLICT \\(add/add\\): Merge conflict in file.txt"));
    }

    @Test
    public void testTryMergeForPullRequestWithSameFileModifiedOnBothConflict() {
        runTryMerge("branch_that_has_same_text_file_modified",
                "5bb214e6dd15643dabfe4b6b0739becfb1e59d9f",
                "c1654113b22f65f988f5a570d8cf32c7288ee7d2",
                Pattern.compile("CONFLICT \\(content\\): Merge conflict in file.txt"));
    }

    private RpcMergePullRequestRequest.Builder createMergePullRequestBuilder() {
        return RpcMergePullRequestRequest.newBuilder()
                .setAuthor(RpcPerson.newBuilder()
                        .setEmail("jdoe@test.com")
                        .setName(ByteString.copyFromUtf8("John Doe"))
                        .build())
                .setCommitter(RpcPerson.newBuilder()
                        .setEmail("janed@test.com")
                        .setName(ByteString.copyFromUtf8("Jane Doe"))
                        .build())
                .setMessage(ByteString.copyFromUtf8("This merge should succeed and be fetched"))
                .setPullRequest(pullRequest);
    }

    private RpcMergeConflicts mergeWithConflicts(RpcMergePullRequestRequest.Builder builder) throws Exception {
        MockResponseObserver<RpcMergePullRequestResponseFragment> responseObserver = new MockResponseObserver<>();
        StreamObserver<RpcMergePullRequestFragment> observer = stub.merge(responseObserver);

        observer.onNext(RpcMergePullRequestFragment.newBuilder()
                .setMergeRequest(builder)
                .build());

        observer.onCompleted();
        responseObserver.await();

        List<RpcMergePullRequestResponseFragment> fragments = responseObserver.getFragments();

        assertNotNull(fragments);
        assertEquals(1, fragments.size());
        RpcMergePullRequestResponseFragment fragment = fragments.get(0);
        assertTrue(fragment.hasMergeResponse());

        RpcMergeResponse mergeResponse = fragment.getMergeResponse();
        assertTrue(mergeResponse.hasConflicts());

        return mergeResponse.getConflicts();
    }

    private RpcBranch mergeWithResponse(RpcMergePullRequestRequest.Builder builder) throws Exception {
        MockResponseObserver<RpcMergePullRequestResponseFragment> responseObserver = new MockResponseObserver<>();
        StreamObserver<RpcMergePullRequestFragment> observer = stub.merge(responseObserver);

        observer.onNext(RpcMergePullRequestFragment.newBuilder()
                .setMergeRequest(builder)
                .build());

        observer.onNext(RpcMergePullRequestFragment.newBuilder()
                .setUpdateRef(RpcUpdateRef.getDefaultInstance())
                .build());

        observer.onCompleted();
        responseObserver.await();

        List<RpcMergePullRequestResponseFragment> fragments = responseObserver.getFragments();

        //Fragment 0 is the merge hooks fragment, fragment 1 is the merge result
        assertNotNull(fragments);
        assertEquals(2, fragments.size());

        RpcMergePullRequestResponseFragment fragment = fragments.get(1);
        assertTrue(fragment.hasMergeResponse());

        RpcMergeResponse mergeResponse = fragment.getMergeResponse();
        assertTrue(mergeResponse.hasBranch());

        return mergeResponse.getBranch();
    }

    private RpcPullRequest preparePullRequest(String id, String fromHash, String toHash, Repository repo) {
        return preparePullRequest(id + "_src", fromHash, repo, id + "_trgt", toHash, repo);
    }

    private RpcPullRequest preparePullRequest(String fromId, String fromHash, Repository fromRepo, String toId,
                                              String toHash, Repository toRepo) {
        return RpcPullRequest.newBuilder()
                .setFromRef(prepareRef(fromId, fromHash, fromRepo))
                .setState(RpcPullRequestState.PR_STATE_OPEN)
                .setToRef(prepareRef(toId, toHash, toRepo))
                .setVersion(0)
                .build();
    }

    private RpcPullRequestRef prepareRef(String id, String hash, Repository repo) {
        return RpcPullRequestRef.newBuilder()
                .setId(ByteString.copyFromUtf8(GitRefPattern.HEADS.qualify(id)))
                .setLatestCommit(hash)
                .setRepository(repo.getId())
                .build();
    }

    private void runTryMerge(String branch, String fromHash, String toHash,
                             Pattern expectedConflictPattern) {
        runTryMerge(branch, fromHash, toHash, expectedConflictPattern, null);
    }

    private void runTryMerge(String branch, String fromHash, String toHash,
                             Pattern expectedConflictPattern, String expectedBinaryPath) {
        pullRequest = preparePullRequest(branch, fromHash, toHash, merge);

        RpcMergePullRequestRequest.Builder builder = createMergePullRequestBuilder()
                .setStrategy(RpcMergeStrategy.MERGE_STRATEGY_NO_FF);

        RpcTryPullRequestMergeResponse mergeResponse = tryMerge(builder);

        RpcMergeConflicts mergeConflicts = mergeResponse.getConflicts();
        assertEquals(expectedConflictPattern == null ? 0 : 1, mergeConflicts.getConflictsCount());
        assertEquals(expectedBinaryPath == null ? 0 : 1, mergeConflicts.getBinaryPathsCount());

        if (expectedConflictPattern != null) {
            assertThat(mergeConflicts.getConflicts(0).getMessage(), matchesPattern(expectedConflictPattern));
        }
        if (expectedBinaryPath != null) {
            assertEquals(expectedBinaryPath, mergeConflicts.getBinaryPaths(0).toStringUtf8());
        }
    }

    private RpcTryPullRequestMergeResponse tryMerge(RpcMergePullRequestRequest.Builder builder) {
        RpcTryPullRequestMergeResponse response = blockingStub.tryMerge(builder.build());
        assertNotNull(response);
        return response;
    }
}
