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

import com.atlassian.bitbucket.mesh.AbstractGrpcTest;
import com.atlassian.bitbucket.mesh.MockResponseObserver;
import com.atlassian.bitbucket.mesh.git.GitRepositories;
import com.atlassian.bitbucket.mesh.git.binary.GitBinary;
import com.atlassian.bitbucket.mesh.repository.Repository;
import com.atlassian.bitbucket.mesh.rpc.v1.*;
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.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 static org.hamcrest.Matchers.greaterThanOrEqualTo;
import static org.hamcrest.Matchers.stringContainsInOrder;
import static org.junit.Assume.assumeThat;

public class MergeRequestConflictsTest extends AbstractGrpcTest {

    private static final Version VERSION_2_21 = new Version(2, 21);
    private static final Version VERSION_2_34 = new Version(2, 34);

    @Autowired
    private GitBinary binary;
    @Resource
    private ManagedChannel managedChannel;
    @Repo(GitRepositories.MERGE)
    private Repository merge;
    private CommitServiceGrpc.CommitServiceStub stub;

    private Version version;

    @Before
    public void setUp() {
        stub = CommitServiceGrpc.newStub(managedChannel);
        version = binary.getVersion();
    }

    @Test
    public void testMergeWithAddAddConflict() throws Exception {
        RpcMergeRequest request = createBuilder()
                .setFromBranch(ByteString.copyFromUtf8("branch_that_has_different_files_with_same_name_added_src"))
                .setFromCommitId("956766bea961e476bd50d8384a5a205ee1c899c0")
                .setToBranch(ByteString.copyFromUtf8("branch_that_has_different_files_with_same_name_added_trgt"))
                .setToCommitId("6d45f46d4c9347b3fd38b650f75ac43d6e4bb8b0")
                .build();

        List<RpcMergeResponseFragment> mergeResponseFragments = merge(request);
        RpcMergeConflicts mergeConflicts = assertHasConflicts(mergeResponseFragments, 0, 1);
        assertMergeConflict(mergeConflicts.getConflicts(0), "CONFLICT (add/add)", "file.txt");
    }

    @Test
    public void testMergeWithAddRenameConflict() throws Exception {
        RpcMergeRequest request = createBuilder()
                .setFromBranch(ByteString.copyFromUtf8("branch_that_has_different_files_added_and_moved_to_same_with_different_content_src"))
                .setFromCommitId("915ec9c5ffe7007456ef8cfecbb4fa0f25e23938")
                .setToBranch(ByteString.copyFromUtf8("branch_that_has_different_files_added_and_moved_to_same_with_different_content_trgt"))
                .setToCommitId("0bfbe00cd171314d2f0cc875afc7502af5f072c1")
                .build();

        List<RpcMergeResponseFragment> mergeResponseFragments = merge(request);
        RpcMergeConflicts mergeConflicts = assertHasConflicts(mergeResponseFragments, 0, 1);

        if (version.compareTo(VERSION_2_34) < 0) {
            //Git 2.33 and earlier
            assertMergeConflict(mergeConflicts.getConflicts(0), "CONFLICT (rename/add)",
                    "file1.txt", "Folder/renamed_file.txt");
        } else {
            //Git 2.34 and newer
            assertMergeConflict(mergeConflicts.getConflicts(0), "CONFLICT (add/add)",
                    "Merge conflict in Folder/renamed_file.txt");
        }
    }

    @Test
    public void testMergeWithBinaryAddAddConflict() throws Exception {
        RpcMergeRequest request = createBuilder()
                .setFromBranch(ByteString.copyFromUtf8("branch_that_has_different_binary_files_with_same_name_added_src"))
                .setFromCommitId("f6f5c9134cc5c7282cbbd91f3ec52e4bd4671ef5")
                .setToBranch(ByteString.copyFromUtf8("branch_that_has_different_binary_files_with_same_name_added_trgt"))
                .setToCommitId("1335a5b58bcdc27804915f352c883c5fa9024e5e")
                .build();

        List<RpcMergeResponseFragment> mergeResponseFragments = merge(request);
        RpcMergeConflicts mergeConflicts = assertHasConflicts(mergeResponseFragments, 1, 1);
        assertMergeConflict(mergeConflicts.getConflicts(0), "CONFLICT (add/add)");
    }

    @Test
    public void testMergeWithBinaryModifyModifyConflict() throws Exception {
        RpcMergeRequest request = createBuilder()
                .setFromBranch(ByteString.copyFromUtf8("branch_that_has_same_binary_file_modified_src"))
                .setFromCommitId("5c279d6c7a3a053a145905aa9682ce02c16449e9")
                .setToBranch(ByteString.copyFromUtf8("branch_that_has_same_binary_file_modified_trgt"))
                .setToCommitId("3ff1eee8ee06b82bc602083053d619029a78931a")
                .build();

        List<RpcMergeResponseFragment> mergeResponseFragments = merge(request);
        RpcMergeConflicts mergeConflicts = assertHasConflicts(mergeResponseFragments, 1, 1);
        assertMergeConflict(mergeConflicts.getConflicts(0), "CONFLICT (content)", "file.bin");
    }

    @Test
    public void testMergeWithConvergentRenameConflict() throws Exception {
        RpcMergeRequest request = createBuilder()
                .setFromBranch(ByteString.copyFromUtf8("branch_that_has_different_files_renamed_to_same_src"))
                .setFromCommitId("a9345fa4efc80e225901d30dd26006b2132d38c6")
                .setToBranch(ByteString.copyFromUtf8("branch_that_has_different_files_renamed_to_same_trgt"))
                .setToCommitId("cd7cfb96c844682d6e973f9fb77141ce096e512c")
                .build();

        List<RpcMergeResponseFragment> mergeResponseFragments = merge(request);
        RpcMergeConflicts mergeConflicts = assertHasConflicts(mergeResponseFragments, 0, 1);
        if (version.compareTo(VERSION_2_34) < 0) {
            //Git 2.33 and earlier handle renaming two different files to the same name by merging their
            //contents in the target file
            assertMergeConflict(mergeConflicts.getConflicts(0), "CONFLICT (rename/rename)",
                    "file2.txt", "renamed_file.txt");
        } else {
            //Git 2.34 and newer merge the contents in the target file like Git 2.21-2.33, but report an
            //add/add conflict (without a source path) instead of rename/rename
            assertMergeConflict(mergeConflicts.getConflicts(0), "CONFLICT (add/add)", "renamed_file.txt");
        }
    }

    @Test
    public void testMergeWithDeleteModifyConflict() throws Exception {
        RpcMergeRequest request = createBuilder()
                .setFromBranch(ByteString.copyFromUtf8("branch_that_has_same_file_modified_and_removed_src"))
                .setFromCommitId("59539850ff5fb3524c68284a9a40e61c0d5569a0")
                .setRepository(merge.getId())
                .setStrategy(RpcMergeStrategy.MERGE_STRATEGY_NO_FF)
                .setToBranch(ByteString.copyFromUtf8("branch_that_has_same_file_modified_and_removed_trgt"))
                .setToCommitId("2c98a0b510355efd24bbd0680e2325e0f5f1887e")
                .build();

        List<RpcMergeResponseFragment> mergeResponseFragments = merge(request);
        RpcMergeConflicts mergeConflicts = assertHasConflicts(mergeResponseFragments, 0, 1);
        assertMergeConflict(mergeConflicts.getConflicts(0), "CONFLICT (modify/delete)",
                "file.txt");
    }

    @Test
    public void testMergeWithDeleteRenameConflict() throws Exception {
        RpcMergeRequest request = createBuilder()
                .setFromBranch(ByteString.copyFromUtf8("branch_that_has_same_file_renamed_and_deleted_src"))
                .setFromCommitId("4c2660490320c5bd7eeef1e414281e712f41562e")
                .setToBranch(ByteString.copyFromUtf8("branch_that_has_same_file_renamed_and_deleted_trgt"))
                .setToCommitId("88f543a36b85dabb8239052aae1a141f5bc80ece")
                .build();

        List<RpcMergeResponseFragment> mergeResponseFragments = merge(request);
        RpcMergeConflicts mergeConflicts = assertHasConflicts(mergeResponseFragments, 0, 1);
        assertMergeConflict(mergeConflicts.getConflicts(0), "CONFLICT (rename/delete)",
                "file.txt", "renamed_file.txt");
    }

    @Test
    public void testMergeWithDirectoryFileConflict() throws Exception {
        RpcMergeRequest request = createBuilder()
                .setFromBranch(ByteString.copyFromUtf8("branch_that_has_file_and_folder_with_same_name_trgt"))
                .setFromCommitId("9905e15fe705e33e14845eba772fb657dae2fabf")
                .setToBranch(ByteString.copyFromUtf8("branch_that_has_file_and_folder_with_same_name_src"))
                .setToCommitId("5def7c91516ab5db031839fb8302393f81a2ec80")
                .build();

        List<RpcMergeResponseFragment> mergeResponseFragments = merge(request);
        RpcMergeConflicts mergeConflicts = assertHasConflicts(mergeResponseFragments, 0, 1);
        if (version.compareTo(VERSION_2_34) < 0) {
            //Git 2.33 and older
            assertMergeConflict(mergeConflicts.getConflicts(0), "CONFLICT (directory/file)", "File");
        } else {
            //Git 2.34 and newer
            assertMergeConflict(mergeConflicts.getConflicts(0), "CONFLICT (file/directory)", "File");
        }
    }

    @Test
    public void testMergeWithDivergentRenameConflict() throws Exception {
        RpcMergeRequest request = createBuilder()
                .setFromBranch(ByteString.copyFromUtf8("branch_that_has_file_moved_to_different_folders_src"))
                .setFromCommitId("ec5662a729f04951119a612790bb1cebbca45e26")
                .setToBranch(ByteString.copyFromUtf8("branch_that_has_file_moved_to_different_folders_trgt"))
                .setToCommitId("3fe45762792a221d6d0f05940c2f2c6760af323e")
                .build();

        List<RpcMergeResponseFragment> mergeResponseFragments = merge(request);
        RpcMergeConflicts mergeConflicts = assertHasConflicts(mergeResponseFragments, 0, 1);
        assertMergeConflict(mergeConflicts.getConflicts(0), "CONFLICT (rename/rename)",
                "file.txt", "Folder2/file.txt");
    }

    @Test
    public void testMergeWithFileDirectoryConflict() throws Exception {
        RpcMergeRequest request = createBuilder()
                .setFromBranch(ByteString.copyFromUtf8("branch_that_has_file_and_folder_with_same_name_src"))
                .setFromCommitId("5def7c91516ab5db031839fb8302393f81a2ec80")
                .setToBranch(ByteString.copyFromUtf8("branch_that_has_file_and_folder_with_same_name_trgt"))
                .setToCommitId("9905e15fe705e33e14845eba772fb657dae2fabf")
                .build();

        List<RpcMergeResponseFragment> mergeResponseFragments = merge(request);
        RpcMergeConflicts mergeConflicts = assertHasConflicts(mergeResponseFragments, 0, 1);
        assertMergeConflict(mergeConflicts.getConflicts(0), "CONFLICT (file/directory)", "File");
    }

    @Test
    public void testMergeWithModifyDeleteConflict() throws Exception {
        RpcMergeRequest request = createBuilder()
                .setFromBranch(ByteString.copyFromUtf8("branch_that_has_same_file_modified_and_removed_trgt"))
                .setFromCommitId("2c98a0b510355efd24bbd0680e2325e0f5f1887e")
                .setToBranch(ByteString.copyFromUtf8("branch_that_has_same_file_modified_and_removed_src"))
                .setToCommitId("59539850ff5fb3524c68284a9a40e61c0d5569a0")
                .build();

        List<RpcMergeResponseFragment> mergeResponseFragments = merge(request);
        RpcMergeConflicts mergeConflicts = assertHasConflicts(mergeResponseFragments, 0, 1);
        assertMergeConflict(mergeConflicts.getConflicts(0), "CONFLICT (modify/delete)",
                "file.txt");
    }

    @Test
    public void testMergeWithModifyModifyConflict() throws Exception {
        RpcMergeRequest request = createBuilder()
                .setFromBranch(ByteString.copyFromUtf8("branch_that_has_same_text_file_modified_src"))
                .setFromCommitId("5bb214e6dd15643dabfe4b6b0739becfb1e59d9f")
                .setToBranch(ByteString.copyFromUtf8("branch_that_has_same_text_file_modified_trgt"))
                .setToCommitId("c1654113b22f65f988f5a570d8cf32c7288ee7d2")
                .build();

        List<RpcMergeResponseFragment> mergeResponseFragments = merge(request);
        RpcMergeConflicts mergeConflicts = assertHasConflicts(mergeResponseFragments, 0, 1);
        assertMergeConflict(mergeConflicts.getConflicts(0), "CONFLICT (content)", "file.txt");
    }

    @Test
    public void testMergeWithMultipleConflicts() throws Exception {
        RpcMergeRequest request = createBuilder()
                .setFromBranch(ByteString.copyFromUtf8("branch_with_ignore_conflicts_src"))
                .setFromCommitId("c927809216a016aee8635896daf7af1fce50f6e1")
                .setToBranch(ByteString.copyFromUtf8("branch_with_ignore_conflicts_trgt"))
                .setToCommitId("74de5497dfca9731e455d60552f9a8906e5dc1ac")
                .build();

        List<RpcMergeResponseFragment> mergeResponseFragments = merge(request);
        RpcMergeConflicts mergeConflicts = assertHasConflicts(mergeResponseFragments, 0, 2);
        if (version.compareTo(VERSION_2_34) < 0) {
            //Git 2.33 and earlier
            assertMergeConflict(mergeConflicts.getConflicts(0), "CONFLICT (content)", "dir/file.txt");
            assertMergeConflict(mergeConflicts.getConflicts(1), "CONFLICT (add/add)", ".gitignore");
        } else {
            //Git 2.34 and newer, which output the same conflicts but in a different order
            assertMergeConflict(mergeConflicts.getConflicts(0), "CONFLICT (add/add)", ".gitignore");
            assertMergeConflict(mergeConflicts.getConflicts(1), "CONFLICT (content)", "dir/file.txt");
        }
    }

    @Test
    public void testMergeWithNestedSubmoduleConflict() throws Exception {
        RpcMergeRequest request = createBuilder()
                .setFromBranch(ByteString.copyFromUtf8("branch_that_has_nested_submodule_src"))
                .setFromCommitId("4a1e20264775c2accef9d715cef7950f21e4c2a5")
                .setToBranch(ByteString.copyFromUtf8("branch_that_has_nested_submodule_trgt"))
                .setToCommitId("e03fa9a080fa006151721bfd9f1da220a16b1468")
                .build();

        List<RpcMergeResponseFragment> mergeResponseFragments = merge(request);
        RpcMergeConflicts mergeConflicts = assertHasConflicts(mergeResponseFragments, 0, 1);
        assertMergeConflict(mergeConflicts.getConflicts(0), "CONFLICT (submodule)",
                "submodules/test");
    }

    @Test
    public void testMergeWithRenameAddConflict() throws Exception {
        assumeThat(version, greaterThanOrEqualTo(VERSION_2_21));

        RpcMergeRequest request = createBuilder()
                .setFromBranch(ByteString.copyFromUtf8("branch_that_has_different_files_added_and_moved_to_same_with_different_content_trgt"))
                .setFromCommitId("0bfbe00cd171314d2f0cc875afc7502af5f072c1")
                .setToBranch(ByteString.copyFromUtf8("branch_that_has_different_files_added_and_moved_to_same_with_different_content_src"))
                .setToCommitId("915ec9c5ffe7007456ef8cfecbb4fa0f25e23938")
                .build();

        List<RpcMergeResponseFragment> mergeResponseFragments = merge(request);
        RpcMergeConflicts mergeConflicts = assertHasConflicts(mergeResponseFragments, 0, 1);
        if (version.compareTo(VERSION_2_34) < 0) {
            //Git 2.33 and earlier
            assertMergeConflict(mergeConflicts.getConflicts(0), "CONFLICT (rename/add)",
                    "file1.txt", "Folder/renamed_file.txt");
        } else {
            //Git 2.34 and newer
            assertMergeConflict(mergeConflicts.getConflicts(0), "CONFLICT (add/add)", "Folder/renamed_file.txt");
        }
    }

    @Test
    public void testMergeWithRenameAddTriggeringContentConflict() throws Exception {
        RpcMergeRequest request = createBuilder()
                .setFromBranch(ByteString.copyFromUtf8("branch_that_has_rename_add_triggering_content_conflict_src"))
                .setFromCommitId("57d4c611655e59f772e9a2fe810520299b6ff376")
                .setToBranch(ByteString.copyFromUtf8("branch_that_has_rename_add_triggering_content_conflict_trgt"))
                .setToCommitId("d1f6232270aa1962d702c1c01bb01461fbaa4c62")
                .build();

        List<RpcMergeResponseFragment> mergeResponseFragments = merge(request);
        if (version.compareTo(VERSION_2_21) < 0) {
            //Git 2.20 and earlier
            RpcMergeConflicts mergeConflicts = assertHasConflicts(mergeResponseFragments, 0, 2);
            assertMergeConflict(mergeConflicts.getConflicts(0), "CONFLICT (rename/add)", "numbers.txt", "count.txt");
            assertMergeConflict(mergeConflicts.getConflicts(1), "CONFLICT (content)", "count.txt");
        } else if (version.compareTo(VERSION_2_34) < 0) {
            //Git 2.21-2.33
            RpcMergeConflicts mergeConflicts = assertHasConflicts(mergeResponseFragments, 0, 1);
            assertMergeConflict(mergeConflicts.getConflicts(0), "CONFLICT (rename/add)", "numbers.txt", "count.txt");
        } else {
            //Git 2.34 and newer
            RpcMergeConflicts mergeConflicts = assertHasConflicts(mergeResponseFragments, 0, 2);
            assertMergeConflict(mergeConflicts.getConflicts(0), "CONFLICT (rename involved in collision)",
                    "numbers.txt", "count.txt", "content conflicts AND collides");
            assertMergeConflict(mergeConflicts.getConflicts(1), "CONFLICT (add/add)", "count.txt");
        }
    }

    @Test
    public void testMergeWithRenameDeleteConflict() throws Exception {
        RpcMergeRequest request = createBuilder()
                .setFromBranch(ByteString.copyFromUtf8("branch_that_has_same_file_renamed_and_deleted_trgt"))
                .setFromCommitId("88f543a36b85dabb8239052aae1a141f5bc80ece")
                .setToBranch(ByteString.copyFromUtf8("branch_that_has_same_file_renamed_and_deleted_src"))
                .setToCommitId("4c2660490320c5bd7eeef1e414281e712f41562e")
                .build();

        List<RpcMergeResponseFragment> mergeResponseFragments = merge(request);
        RpcMergeConflicts mergeConflicts = assertHasConflicts(mergeResponseFragments, 0, 1);
        assertMergeConflict(mergeConflicts.getConflicts(0), "CONFLICT (rename/delete)",
                "file.txt", "renamed_file.txt");
    }

    @Test
    public void testMergeWithSubmoduleConflict() throws Exception {
        RpcMergeRequest request = createBuilder()
                .setFromBranch(ByteString.copyFromUtf8("branch_that_has_submodule_at_different_commit_src"))
                .setFromCommitId("ee85213ec42077581cc408fee21cf2dc2278858b")
                .setToBranch(ByteString.copyFromUtf8("branch_that_has_submodule_at_different_commit_trgt"))
                .setToCommitId("df2fc897281251b855c7a8ab663ff7c45689afbb")
                .build();

        List<RpcMergeResponseFragment> mergeResponseFragments = merge(request);
        RpcMergeConflicts mergeConflicts = assertHasConflicts(mergeResponseFragments, 0, 1);
        assertMergeConflict(mergeConflicts.getConflicts(0), "CONFLICT (submodule)");
    }

    private static RpcMergeConflicts assertHasConflicts(List<RpcMergeResponseFragment> actualFragments,
                                                        int expectedBinaryPathCount, int expectedConflictCount) {
        assertNotNull(actualFragments);
        assertEquals(1, actualFragments.size());

        RpcMergeResponseFragment mergeResponseFragment = actualFragments.get(0);
        assertNotNull(mergeResponseFragment);

        RpcMergeResponse mergeResponse = mergeResponseFragment.getMergeResponse();
        assertNotNull(mergeResponse);
        assertTrue(mergeResponse.hasConflicts());

        RpcMergeConflicts conflicts = mergeResponse.getConflicts();
        assertNotNull(conflicts);
        assertEquals(expectedBinaryPathCount, conflicts.getBinaryPathsCount());
        assertEquals(expectedConflictCount, conflicts.getConflictsCount());

        return conflicts;
    }

    private static void assertMergeConflict(RpcMergeConflict actualConflict, String... expectedConflictMessage) {
        assertNotNull(actualConflict);
        assertThat(actualConflict.getMessage(), stringContainsInOrder(expectedConflictMessage));
    }

    private RpcMergeRequest.Builder createBuilder() {
        return RpcMergeRequest.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("Testing merge conflicts"))
                .setRepository(merge.getId())
                .setStrategy(RpcMergeStrategy.MERGE_STRATEGY_NO_FF);
    }

    private List<RpcMergeResponseFragment> merge(RpcMergeRequest request) throws Exception {
        MockResponseObserver<RpcMergeResponseFragment> responseObserver = new MockResponseObserver<>();
        StreamObserver<RpcMergeFragment> observer = stub.merge(responseObserver);

        observer.onNext(RpcMergeFragment.newBuilder()
                .setMergeRequest(request)
                .build());

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

        return responseObserver.getFragments();
    }
}
