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

import com.atlassian.bitbucket.scm.annotation.Repo;
import com.atlassian.stash.internal.scm.git.AbstractGitTest;
import com.atlassian.bitbucket.repository.Repository;
import com.atlassian.bitbucket.scm.MergeCommandParameters;
import com.atlassian.bitbucket.scm.MergeException;
import com.atlassian.bitbucket.scm.git.command.GitCommandFactory;
import com.atlassian.bitbucket.user.ApplicationUser;
import org.junit.Before;
import org.junit.Test;
import org.mockito.Mock;
import org.springframework.beans.factory.annotation.Autowired;

import static org.mockito.Mockito.when;

/**
 * Verifies different merge scenarios result in conflicts (or don't) as expected.
 * <p>
 * The primary purpose of this test is to verify that the trick way of performing a merge used by Stash, i.e. to
 * perform a git clone --shared --no-checkout, git reset --quiet --, git merge, works on all the versions of git
 * Stash supports, that its behaviour is consistent and that it is sufficiently performant to be executed in a
 * user-time context.
 * <p>
 * This test <i>does not push</i> the results to the backing bare clone. All it does is verify that the merge is
 * behaving as expected (specifically that merge conflicts are detected and result in an exception). This test is
 * somewhat redundant, in that we're testing a lot of git behaviours, but it should be useful to help catch any
 * outliers in git's behaviour for merging.
 */
public class MergeCommandTest extends AbstractGitTest {

    @Autowired
    private GitCommandFactory commandFactory;
    @Repo("classpath:git/merge.zip")
    private Repository repository;
    @Mock
    private ApplicationUser user;

    @Before
    public void setup() {
        when(user.getDisplayName()).thenReturn("Bryan Turner");
        when(user.getEmailAddress()).thenReturn("bturner@atlassian.com");
    }

    @Test
    public void testDeletedOnBoth() {
        runMerge("branch_that_has_same_file_deleted_on_both", "Testing merging in identical deletions");
    }

    @Test
    public void testDifferentFilesModifiedOnEach() {
        runMerge("branch_that_has_modified_file_no_merge_conflict", "Testing merging modifications to different files");
    }

    @Test
    public void testMergingAncestorIntoChild() {
        runMerge("branch_that_differ_by_one_file", "Testing merging an ancestor into its child", true);
    }

    @Test(expected = MergeException.class)
    public void testModifiedOnOneAndRemovedOnOther() {
        runMerge("branch_that_has_same_file_modified_and_removed", "Testing merging a modified file into a removal");
    }

    @Test
    public void testModifiedOnOneAndRenamedOnOther() {
        runMerge("branch_that_has_same_file_modified_and_renamed", "Testing merging a modified file into a rename");
    }
    
    @Test(expected = MergeException.class)
    public void testRemovedOnOneAndModifiedOnOther() {
        runMerge("branch_that_has_same_file_modified_and_removed",
                "Testing merging a removal onto a modified file",
                true);
    }

    @Test
    public void testRenamedOnBoth() {
        runMerge("branch_that_has_same_file_renamed_on_both", "Testing merging in identical renames");
    }

    @Test
    public void testRenamedOnOneAndModifiedOnOther() {
        runMerge("branch_that_has_same_file_modified_and_renamed",
                "Testing merging a rename onto a modified file",
                true);
    }

    @Test(expected = MergeException.class)
    public void testSameFileModifiedOnBothConflictingly() {
        runMerge("branch_that_has_same_text_file_modified", "Testing merging in conflicting modifications to the same file");
    }

    @Test
    public void testSameFileModifiedOnBothIdentically() {
        runMerge("branch_that_has_same_text_file_modified_on_both", "Testing merging in identical modifications");
    }

    @Test
    public void testWithEmptyCommit() {
        runMerge("branch_that_differ_by_empty_commit", "Testing merging in an empty commit");
    }

    @Test
    public void testWithFastForward() {
        runMerge("branch_that_differ_by_one_file", "Testing merging in a fast-forward commit");
    }

    @Test
    public void testWithNegatingCommits() {
        runMerge("branch_that_differ_by_negating_commits", "Testing merging in negating/reverting commits");
    }

    @Test
    public void testWithQuotesInMessage() {
        runMerge("branch_that_differ_by_one_file", "Testing merging \"in a fast-forward\" commit");
    }

    private void runMerge(String branchStem, String message) {
        runMerge(branchStem, message, false);
    }

    private void runMerge(String branchStem, String message, boolean reverse) {
        String source = branchStem + "_src";
        String target = branchStem + "_trgt";

        //Performing the "same" merge in the other direction can help test other interesting cases
        MergeCommandParameters.Builder builder = new MergeCommandParameters.Builder()
                .author(user)
                .dryRun(true)
                .message(message);
        if (reverse) {
            builder.fromBranch(target)
                    .toBranch(source);
        } else {
            builder.fromBranch(source)
                    .toBranch(target);
        }

        //If this returns a Branch, it means the command did not honour the dryRun flag
        assertNull(commandFactory.merge(repository, builder.build()).call());
    }
}
