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

import com.atlassian.bitbucket.content.*;
import com.atlassian.bitbucket.scm.annotation.Repo;
import com.atlassian.bitbucket.commit.NoSuchCommitException;
import com.atlassian.bitbucket.user.TestApplicationUser;
import com.atlassian.stash.internal.content.DiffContentCallbackFilter;
import com.atlassian.stash.internal.scm.git.AbstractGitTest;
import com.atlassian.bitbucket.repository.Repository;
import com.atlassian.bitbucket.scm.DiffCommandParameters;
import com.atlassian.bitbucket.scm.git.command.GitCommandBuilderFactory;
import com.atlassian.bitbucket.scm.git.command.GitCommandFactory;
import com.atlassian.bitbucket.scm.git.command.GitScmCommandBuilder;
import com.atlassian.bitbucket.scm.git.command.diff.GitDiffBuilder;
import com.atlassian.bitbucket.scm.git.command.diff.GitDiffColor;
import com.atlassian.bitbucket.scm.git.command.diff.GitDiffRenames;
import org.apache.commons.io.FileUtils;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.InOrder;
import org.springframework.beans.factory.annotation.Autowired;

import javax.annotation.Nonnull;
import java.io.File;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import static com.atlassian.stash.internal.scm.git.GitFactoriesTestHelper.createEmptyRepository;
import static com.atlassian.stash.internal.scm.git.command.diff.DiffTreeCommandTest.NULL_TREE;
import static org.mockito.Mockito.*;

public class DiffCommandTest extends AbstractGitTest {

    @Autowired
    private GitCommandBuilderFactory builderFactory;
    @Autowired
    private GitCommandFactory commandFactory;
    @Captor
    private ArgumentCaptor<Path> destinationCaptor;
    @Repo("classpath:git/diff-tree.zip")
    private Repository diffTree;
    @Repo
    private Repository repository;
    @Captor
    private ArgumentCaptor<Path> sourceCaptor;
    @Captor
    private ArgumentCaptor<DiffSummary> summaryCaptor;
    @Rule
    public TemporaryFolder folder = new TemporaryFolder();

    @Test
    public void testBinaryAdd() throws Exception {
        DiffContentCallback callback = mock(DiffContentCallback.class);
        DiffContext context = contextFor(25, 100);
        createBuilder()
                .color(GitDiffColor.NEVER)
                .dstPrefix(DiffOutputHandler.DST_PREFIX)
                .path("binary/A.zip")
                .rev("9c05f43f859375e392d90d23a13717c16d0fdcda")
                .srcPrefix(DiffOutputHandler.SRC_PREFIX)
                .build(new DiffOutputHandler(callback, context))
                .call();

        InOrder ordered = inOrder(callback);
        ordered.verify(callback).onStart(same(context));
        ordered.verify(callback).onBinary(isNull(Path.class), destinationCaptor.capture());
        ordered.verify(callback).onEnd(isNotNull(DiffSummary.class));
        ordered.verifyNoMoreInteractions();

        assertPath("binary/A.zip", destinationCaptor.getValue());
    }

    @Test
    public void testCommitDiff() throws Exception {
        DiffContentCallback callback = mock(DiffContentCallback.class);
        DiffContext context = contextFor(25, 132);
        createBuilder()
                .color(GitDiffColor.NEVER)
                .dstPrefix(DiffOutputHandler.DST_PREFIX)
                .rev("6053a1eaa1c009dd11092d09a72f3c41af1b59ad")
                .srcPrefix(DiffOutputHandler.SRC_PREFIX)
                .build(new DiffOutputHandler(callback, context))
                .call();

        //Exact segment details are tested in testCommitDiff, and not repeated here for brevity
        InOrder ordered = inOrder(callback);
        ordered.verify(callback).onStart(same(context));
        verifyDiffStart(ordered.verify(callback), "branch_mod_merge/file.txt");
        ordered.verify(callback).onHunkStart(eq(1), eq(1), eq(1), eq(2));
        ordered.verify(callback).onSegmentStart(same(DiffSegmentType.CONTEXT));
        ordered.verify(callback).onSegmentLine(eq("branch, mod and merge scenario"), noConflict(), eq(false));
        ordered.verify(callback).onSegmentEnd(eq(false));
        ordered.verify(callback).onSegmentStart(same(DiffSegmentType.ADDED));
        ordered.verify(callback).onSegmentLine(eq("a modification"), noConflict(), eq(false));
        ordered.verify(callback).onSegmentEnd(eq(false));
        ordered.verify(callback).onHunkEnd(eq(false));
        ordered.verify(callback).onDiffEnd(eq(false));
        ordered.verify(callback).onEnd(isNotNull(DiffSummary.class));
        ordered.verifyNoMoreInteractions();
    }

    @Test
    public void testCommitDiffWithCR() throws Exception {
        DiffContentCallback callback = mock(DiffContentCallback.class);
        DiffContext context = contextFor(25, 132);
        builderFactory.builder(diffTree)
                .diff()
                .color(GitDiffColor.NEVER)
                .dstPrefix(DiffOutputHandler.DST_PREFIX)
                .path("mac.txt")
                .rev("83f0ca32d75e3121c57a3cc2d549a8a01a1cfafc")
                .srcPrefix(DiffOutputHandler.SRC_PREFIX)
                .build(new DiffOutputHandler(callback, context))
                .call();

        InOrder ordered = inOrder(callback);
        ordered.verify(callback).onStart(same(context));
        verifyDiffStart(ordered.verify(callback), "mac.txt");
        ordered.verify(callback).onHunkStart(eq(1), eq(1), eq(1), eq(1));
        ordered.verify(callback).onSegmentStart(same(DiffSegmentType.REMOVED));
        ordered.verify(callback).onSegmentLine(eq("This is a\rmac file\rwith line endings\rin cr with edit"), noConflict(), eq(false));
        ordered.verify(callback).onSegmentEnd(eq(false));
        ordered.verify(callback).onSegmentStart(same(DiffSegmentType.ADDED));
        ordered.verify(callback).onSegmentLine(eq("This is a\rmac file\rwith line endings\rin cr "), noConflict(), eq(false));
        ordered.verify(callback).onSegmentEnd(eq(false));
        ordered.verify(callback).onHunkEnd(eq(false));
        ordered.verify(callback).onDiffEnd(eq(false));
        ordered.verify(callback).onEnd(isNotNull(DiffSummary.class));
        ordered.verifyNoMoreInteractions();
    }

    @Test
    public void testCommitDiffWithCRAndLF() throws Exception {
        DiffContentCallback callback = mock(DiffContentCallback.class);
        DiffContext context = contextFor(25, 132);
        builderFactory.builder(diffTree)
                .diff()
                .color(GitDiffColor.NEVER)
                .dstPrefix(DiffOutputHandler.DST_PREFIX)
                .path("mac_and_unix.txt")
                .rev("83f0ca32d75e3121c57a3cc2d549a8a01a1cfafc")
                .srcPrefix(DiffOutputHandler.SRC_PREFIX)
                .build(new DiffOutputHandler(callback, context))
                .call();

        InOrder ordered = inOrder(callback);
        ordered.verify(callback).onStart(same(context));
        verifyDiffStart(ordered.verify(callback), "mac_and_unix.txt");
        ordered.verify(callback).onHunkStart(eq(9), eq(7), eq(9), eq(7));
        ordered.verify(callback).onSegmentStart(same(DiffSegmentType.REMOVED));
        ordered.verify(callback).onSegmentLine(eq("-edit\rand "), noConflict(), eq(false));
        ordered.verify(callback).onSegmentEnd(eq(false));
        ordered.verify(callback).onSegmentStart(same(DiffSegmentType.ADDED));
        ordered.verify(callback).onSegmentLine(eq("edit\rand "), noConflict(), eq(false));
        ordered.verify(callback).onSegmentEnd(eq(false));
        ordered.verify(callback).onHunkEnd(eq(false));
        ordered.verify(callback).onDiffEnd(eq(false));
        ordered.verify(callback).onEnd(isNotNull(DiffSummary.class));
        ordered.verifyNoMoreInteractions();
    }

    @Test
    public void testDiffAgainstNullTree() throws Exception {
        DiffContentCallback callback = mock(DiffContentCallback.class);
        DiffContext context = contextFor(25, 80);
        createBuilder()
                .color(GitDiffColor.NEVER)
                .dstPrefix(DiffOutputHandler.DST_PREFIX)
                .ancestor(NULL_TREE)
                .rev("57150c54c38d6570b2fd5e6d6cfc19476de44e84")
                .srcPrefix(DiffOutputHandler.SRC_PREFIX)
                .build(new DiffOutputHandler(callback, context))
                .call();

        // verify that no context or deleted lines are returned (all lines are additions compared to the null tree)
        verify(callback, never()).onSegmentStart(same(DiffSegmentType.CONTEXT));
        verify(callback, never()).onSegmentStart(same(DiffSegmentType.REMOVED));

        InOrder ordered = inOrder(callback);
        ordered.verify(callback).onStart(same(context));
        verifyDiffStart(ordered.verify(callback), null, "add_file/add_file.txt");
        ordered.verify(callback).onHunkStart(eq(0), eq(0), eq(1), eq(1));
        ordered.verify(callback).onSegmentLine(eq("this is a test file"), noConflict(), eq(false));
        ordered.verify(callback).onHunkEnd(eq(false));
        ordered.verify(callback).onDiffEnd(eq(false));
        verifyDiffStart(ordered.verify(callback), null, "modification/mod_file.txt");
        ordered.verify(callback).onHunkStart(eq(0), eq(0), eq(1), eq(2));
        ordered.verify(callback).onSegmentLine(eq("this is a test file"), noConflict(), eq(false));
        ordered.verify(callback).onSegmentLine(eq("another line"), noConflict(), eq(false));
        ordered.verify(callback).onHunkEnd(eq(false));
        ordered.verify(callback).onDiffEnd(eq(false));
        verifyDiffStart(ordered.verify(callback), null, "mv_file/moved_file.txt");
        ordered.verify(callback).onHunkStart(eq(0), eq(0), eq(1), eq(1));
        ordered.verify(callback).onSegmentLine(eq("This file will be moved"), noConflict(), eq(false));
        ordered.verify(callback).onHunkEnd(eq(false));
        ordered.verify(callback).onDiffEnd(eq(false));
        verifyDiffStart(ordered.verify(callback), null, "out_of_order/file.txt");
        ordered.verify(callback).onHunkStart(eq(0), eq(0), eq(1), eq(3));
        ordered.verify(callback).onSegmentLine(eq("out of order commits scenario"), noConflict(), eq(false));
        ordered.verify(callback).onSegmentLine(eq("a modification"), noConflict(), eq(false));
        ordered.verify(callback).onSegmentLine(eq("another modification"), noConflict(), eq(false));
        ordered.verify(callback).onHunkEnd(eq(false));
        ordered.verify(callback).onDiffEnd(eq(false));
        ordered.verify(callback).onEnd(isNotNull(DiffSummary.class));
        ordered.verifyNoMoreInteractions();
    }

    @Test
    public void testEmptyDiff() throws Exception {
        DiffContentCallback callback = mock(DiffContentCallback.class);
        DiffContext context = contextFor(25, 132);
        createBuilder()
                .ancestor("8fb8538799b2481750eac52716931d895d029b7c")
                .color(GitDiffColor.NEVER)
                .dstPrefix(DiffOutputHandler.DST_PREFIX)
                .path("nonexistent/file.txt")
                .rev("042fb0cc198e7320b59a025689b45a2cb52f4684")
                .srcPrefix(DiffOutputHandler.SRC_PREFIX)
                .build(new DiffOutputHandler(callback, context))
                .call();

        InOrder ordered = inOrder(callback);
        ordered.verify(callback).onStart(same(context));
        ordered.verify(callback).onEnd(isNotNull(DiffSummary.class));
        ordered.verifyNoMoreInteractions();
    }

    @Test
    public void testMultipleDiffs() throws Exception {
        DiffContentCallback callback = mock(DiffContentCallback.class);
        DiffContext context = contextFor(25, 5000);
        createBuilder()
                .ancestor("409a2180fd95b744df8d817e770b7f8c853f78dc")
                .color(GitDiffColor.NEVER)
                .dstPrefix(DiffOutputHandler.DST_PREFIX)
                .rev("0a943a29376f2336b78312d99e65da17048951db")
                .srcPrefix(DiffOutputHandler.SRC_PREFIX)
                .build(new DiffOutputHandler(callback, context))
                .call();

        //The full shape of the diff is verified in testMultipleDiffs. This focuses entirely on the sequencing of
        //onStart, onDiffStart, onEnd and onDiffEnd
        InOrder ordered = inOrder(callback);
        ordered.verify(callback).onStart(same(context));
        verifyDiffStart(ordered.verify(callback), null, "add_file/add_file.txt");
        ordered.verify(callback).onDiffEnd(eq(false));
        verifyDiffStart(ordered.verify(callback), null, "basic_branching/file.txt");
        ordered.verify(callback).onDiffEnd(eq(false));
        ordered.verify(callback).onBinary(isNull(Path.class), eq(new SimplePath("binary/B.zip")));
        ordered.verify(callback).onBinary(isNull(Path.class), eq(new SimplePath("binary/C.zip")));
        ordered.verify(callback).onBinary(isNull(Path.class), eq(new SimplePath("binary/D.zip")));
        verifyDiffStart(ordered.verify(callback), null, "branch_mod_merge/file.txt");
        ordered.verify(callback).onDiffEnd(eq(false));
        verifyDiffStart(ordered.verify(callback), null, "modification/mod_file.txt");
        ordered.verify(callback).onDiffEnd(eq(false));
        verifyDiffStart(ordered.verify(callback), null, "moved_dir/file.txt");
        ordered.verify(callback).onDiffEnd(eq(false));
        verifyDiffStart(ordered.verify(callback), null, "mv_file/moved_file.txt");
        ordered.verify(callback).onDiffEnd(eq(false));
        verifyDiffStart(ordered.verify(callback), "out_of_order/file.txt");
        ordered.verify(callback).onDiffEnd(eq(false));
        verifyDiffStart(ordered.verify(callback), null, "symlink/link.txt");
        ordered.verify(callback).onDiffEnd(eq(false));
        verifyDiffStart(ordered.verify(callback), null, "symlink/target.txt");
        ordered.verify(callback).onDiffEnd(eq(false));
        ordered.verify(callback).onEnd(isNotNull(DiffSummary.class));
        ordered.verifyNoMoreInteractions();
    }

    @Test
    public void testMultipleDiffsFilter() throws Exception {
        DiffContentCallback callback = mock(DiffContentCallback.class);
        DiffContext context = contextFor(25, 5000);
        createBuilder()
                .ancestor("409a2180fd95b744df8d817e770b7f8c853f78dc")
                .color(GitDiffColor.NEVER)
                .dstPrefix(DiffOutputHandler.DST_PREFIX)
                .rev("0a943a29376f2336b78312d99e65da17048951db")
                .srcPrefix(DiffOutputHandler.SRC_PREFIX)
                .build(new DiffOutputHandler(new DiffContentCallbackFilter(callback, new DiffContentFilter() {
                    @Override
                    public boolean apply(@Nonnull String line) {
                        return line.contains("a mod");
                    }
                }), context))
                .call();

        InOrder ordered = inOrder(callback);
        ordered.verify(callback).onStart(same(context));
        verifyDiffStart(ordered.verify(callback), null, "branch_mod_merge/file.txt");
        ordered.verify(callback).onHunkStart(0, 0, 2, 1);
        ordered.verify(callback).onSegmentStart(DiffSegmentType.ADDED);
        ordered.verify(callback).onSegmentLine("a modification", null, false);
        ordered.verify(callback).onSegmentEnd(false);
        ordered.verify(callback).onHunkEnd(false);
        ordered.verify(callback).onDiffEnd(eq(false));
        verifyDiffStart(ordered.verify(callback), "out_of_order/file.txt");
        ordered.verify(callback).onHunkStart(2, 0, 2, 1);
        ordered.verify(callback).onSegmentStart(DiffSegmentType.ADDED);
        ordered.verify(callback).onSegmentLine("a modification", null, false);
        ordered.verify(callback).onSegmentEnd(false);
        ordered.verify(callback).onHunkEnd(false);
        ordered.verify(callback).onDiffEnd(eq(false));
        ordered.verify(callback).onEnd(isNotNull(DiffSummary.class));
        ordered.verifyNoMoreInteractions();
    }

    @Test(expected = NoSuchCommitException.class)
    public void testNoSuchCommit() {
        DiffContentCallback callback = mock(DiffContentCallback.class);

        try {
            commandFactory.diff(repository,
                    new DiffCommandParameters.Builder()
                            .maxLineLength(1000)
                            .maxLines(100)
                            .sinceId("9fbc34a0d905950131d73f352abe68520c6db2a3")
                            .untilId("57150c54c38d6570b2fd5e6d6cfc19476de44e83")
                            .build(),
                    callback).call();
        } finally {
            //If the command fails (and throwing NoSuchCommit should fail), ensure the callback is never used
            verifyZeroInteractions(callback);
        }
    }

    @Test(expected = NoSuchCommitException.class)
    public void testNoSuchCommitV2() {
        DiffContentCallback callback = mock(DiffContentCallback.class);

        try {
            commandFactory.diff(repository,
                    new DiffCommandParameters.Builder()
                            .maxLineLength(1000)
                            .maxLines(100)
                            .sinceId("9fbc34a0d905950131d73f352abe68520c6db2a3")
                            .untilId("57150c54c38d6570b2fd5e6d6cfc19476de44e83")
                            .build(),
                    callback).call();
        } finally {
            //If the command fails (and throwing NoSuchCommit should fail), ensure the callback is never used
            verifyZeroInteractions(callback);
        }
    }

    @Test
    public void testPathDiff() throws Exception {
        DiffContentCallback callback = mock(DiffContentCallback.class);
        createBuilder()
                .ancestor("8fb8538799b2481750eac52716931d895d029b7c")
                .color(GitDiffColor.NEVER)
                .dstPrefix(DiffOutputHandler.DST_PREFIX)
                .rev("042fb0cc198e7320b59a025689b45a2cb52f4684")
                .srcPrefix(DiffOutputHandler.SRC_PREFIX)
                .build(new DiffOutputHandler(callback, contextFor(25, 10)))
                .call();
        
        InOrder ordered = inOrder(callback);
        verifyDiffStart(ordered.verify(callback), null, "branch_mod_merge/file.txt");
        ordered.verify(callback).onHunkStart(eq(0), eq(0), eq(1), eq(1));
        ordered.verify(callback).onSegmentStart(same(DiffSegmentType.ADDED));
        ordered.verify(callback).onSegmentLine(eq("branch, mo"), noConflict(), eq(true));
        ordered.verify(callback).onSegmentEnd(eq(false));
        ordered.verify(callback).onHunkEnd(eq(false));
        ordered.verify(callback).onDiffEnd(eq(false));
        ordered.verify(callback).onEnd(isNotNull(DiffSummary.class));
        ordered.verifyNoMoreInteractions();
    }

    @Test
    public void testRenamesEnabledViaConfiguration() throws Exception {
        DiffContentCallback callback = mock(DiffContentCallback.class);

        //Colors are disabled and rename detection is enabled via command line configuration. This verifies
        //the builder correctly builds command lines with configuration, and that it's honored by Git.
        //**Note** This has to use the free-form builder instead of GitDiffBuilder because the type-safe
        //builder explicitly passes --no-renames (or another rename flag), which overrides configuration.
        builderFactory.builder(repository)
                .argument("-U")
                .argument("--dst-prefix=" + DiffOutputHandler.DST_PREFIX)
                .argument("--src-prefix=" + DiffOutputHandler.SRC_PREFIX)
                .argument("9c05f43f859375e392d90d23a13717c16d0fdcda")
                .argument("2d8897c9ac29ce42c3442cf80ac977057045e7f6")
                .command("diff")
                .withConfiguration("color.ui", "never")      //Ensure colors are suppressed via a configuration flag
                .withConfiguration("diff.renames", "copies") //Turn on rename detection via a configuration flag
                .build(new DiffOutputHandler(callback, contextFor(25, 100)))
                .call();

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

    @Test
    public void testSimpleRename() throws Exception {
        DiffContentCallback callback = mock(DiffContentCallback.class);
        createBuilder()
                .ancestor("9c05f43f859375e392d90d23a13717c16d0fdcda")
                .color(GitDiffColor.NEVER)
                .dstPrefix(DiffOutputHandler.DST_PREFIX)
                .renames(GitDiffRenames.FIND_RENAMES)
                .rev("2d8897c9ac29ce42c3442cf80ac977057045e7f6")
                .srcPrefix(DiffOutputHandler.SRC_PREFIX)
                .build(new DiffOutputHandler(callback, contextFor(25, 100)))
                .call();

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

    @Test
    public void testFilenameWithSpaces() throws Exception {
        DiffContentCallback callback = mock(DiffContentCallback.class);
        String fileWithSpaces = "a b.txt";
        File dir = createEmptyRepository(folder.newFolder(), builderFactory);
        FileUtils.writeStringToFile(new File(dir, fileWithSpaces), "abc");
        GitScmCommandBuilder builder = builderFactory.builder().workingDirectory(dir);
        builder.add().path(fileWithSpaces).build().call();
        builder.commit().author(new TestApplicationUser("test", 0, "test@git.com")).message("Some commit").build().call();
        builderFactory.builder().workingDirectory(dir).diff()
                .color(GitDiffColor.NEVER)
                .dstPrefix(DiffOutputHandler.DST_PREFIX)
                .rev("master")
                .srcPrefix(DiffOutputHandler.SRC_PREFIX)
                .build(new DiffOutputHandler(callback, contextFor(25, 100)))
                .call();
        verifyDiffStart(verify(callback), null, fileWithSpaces);
    }

    @Test
    public void testRootCommit() throws Exception {
        DiffContentCallback callback = mock(DiffContentCallback.class);
        createBuilder()
                .color(GitDiffColor.NEVER)
                .dstPrefix(DiffOutputHandler.DST_PREFIX)
                .rev("409a2180fd95b744df8d817e770b7f8c853f78dc")
                .srcPrefix(DiffOutputHandler.SRC_PREFIX)
                .build(new DiffOutputHandler(callback, contextFor(25, 100)))
                .call();

        final InOrder ordered = inOrder(callback);
        ordered.verify(callback).onStart(any(DiffContext.class));
        verifyDiffStart(ordered.verify(callback), null, "out_of_order/file.txt");
        ordered.verify(callback).onHunkStart(eq(0), eq(0), eq(1), eq(1));

        ordered.verify(callback).onSegmentStart(same(DiffSegmentType.ADDED));
        ordered.verify(callback).onSegmentLine(eq("out of order commits scenario"), noConflict(), eq(false));
        ordered.verify(callback).onSegmentEnd(eq(false));

        ordered.verify(callback).onHunkEnd(eq(false));
        ordered.verify(callback).onDiffEnd(eq(false));

        ordered.verify(callback).onEnd(isNotNull(DiffSummary.class));
        verifyNoMoreInteractions(callback);
    }

    @Test
    public void testPatterns() {
        assertPattern(DiffOutputHandler.PATTERN_DIFF_HEADER,
                "diff --git src://test.txt dst://test.txt",
                "src://test.txt", "dst://test.txt");
        assertPattern(DiffOutputHandler.PATTERN_DIFF_HEADER,
                "diff --git src://File With Spaces.txt dst://File With Spaces.txt",
                "src://File With Spaces.txt", "dst://File With Spaces.txt");
        assertPattern(DiffOutputHandler.PATTERN_DIFF_HEADER,
                "diff --git \"src://file\\\"withquotes\" \"dst://file\\\"withquotes\"",
                "\"src://file\\\"withquotes\"", "\"dst://file\\\"withquotes\"");
        assertPattern(DiffOutputHandler.PATTERN_HUNK_HEADER,
                "@@ -1 +1,2 @@",
                "1", null, "1", "2");
        assertPattern(DiffOutputHandler.PATTERN_HUNK_HEADER,
                "@@ -58,7 +50,6 @@ public class FileHistoryResource {",
                "58", "7", "50", "6");
    }
    
    private static void assertPath(String expected, Path actual) {
        if (expected == null) {
            assertNull(actual);
        } else {
            assertEquals(expected, actual.toString());
        }
    }

    private static void assertPattern(Pattern pattern, String line, String... values) {
        Matcher matcher = pattern.matcher(line);
        assertTrue("Pattern [" + pattern.pattern() + "] should have matched [" + line + "]", matcher.matches());
        assertEquals(values.length, matcher.groupCount());
        
        for (int i = 0; i < values.length; ++i) {
            String expected = values[i];
            String actual = matcher.group(i + 1);
            if (expected == null) {
                assertNull(actual);
            } else {
                assertEquals(expected, actual);
            }
        }
    }

    private static DiffContext contextFor(int maxLines, int maxLineLength) {
        return new DiffContext.Builder("from", "to")
                .contextLines(3)
                .maxLineLength(maxLineLength)
                .maxLines(maxLines)
                .whitespace(DiffWhitespace.SHOW)
                .build();
    }

    private static ConflictMarker noConflict() {
        return isNull(ConflictMarker.class);
    }

    private GitDiffBuilder createBuilder() {
        return builderFactory.builder(repository).diff();
    }

    private void verifyDiffStart(DiffContentCallback callback, String path) throws Exception {
        verifyDiffStart(callback, path, path);
    }
    
    private void verifyDiffStart(DiffContentCallback callback, String src, String dst) throws Exception {
        callback.onDiffStart(sourceCaptor.capture(), destinationCaptor.capture());

        assertPath(src, sourceCaptor.getValue());
        assertPath(dst, destinationCaptor.getValue());

        //Exploiting the fact that Mockito returns the direct list, rather than a copy or immutable wrapper
        destinationCaptor.getAllValues().clear();
        sourceCaptor.getAllValues().clear();
    }
}
