package com.atlassian.bitbucket.mesh.git.builder.diff;

import com.atlassian.bitbucket.mesh.git.content.DiffCallback;
import com.atlassian.bitbucket.mesh.git.content.DiffSegmentType;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.InOrder;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnitRunner;

import static com.atlassian.bitbucket.mesh.util.ProcessStdioHandlerUtils.handleStdout;
import static org.apache.commons.lang3.StringUtils.repeat;
import static org.mockito.Mockito.*;

@RunWith(MockitoJUnitRunner.class)
public class DiffStdioHandlerTest {

    @Mock
    private DiffCallback callback;

    @Test
    public void testMixedCrAndCrLfMix() {
        //The purpose of this test is to ensure Mesh doesn't regress when the diff output handler is modified. It
        //fails in a particularly annoying way, resulting in the exception (IllegalStateException) thrown by the
        //DiffStdioHandler being swallowed because it's raised after BBS starts streaming diff chunks to the caller.

        //This input file has an interesting mix of CR, CRLF and CF combination
        //Linux file command returns:
        //    unified diff output, UTF-8 Unicode text, with CRLF, CR, LF line terminators
        //
        //This particular file broke the Diff output parser before BSERV-8424 was fixed.
        process("git-diff-with-CR-and-CRLF.out", 10000, 5000);

        verify(callback).onDiffStart(anyString(), anyString());
        verify(callback, times(4)).onHunkStart(anyInt(), anyInt(), anyInt(), anyInt(), any());
        verify(callback, times(21)).onSegmentStart(any(DiffSegmentType.class));
        verify(callback, times(199)).onSegmentLine(anyString(), anyBoolean());
        verify(callback, times(21)).onSegmentEnd(anyBoolean());
        verify(callback, times(4)).onHunkEnd(anyBoolean());
        verify(callback).onDiffEnd(anyBoolean());
    }

    @Test
    public void testEscapedNamesV2() {
        process("git-diff-escaped-file.out");

        verify(callback).onDiffStart("SourceView/foo\" onerror=\"alert(666)\"", null);
        verify(callback).onDiffStart(null, "\u55b6\u5c02.txt");
        verify(callback).onDiffStart("\u55b6\u5c02 \".txt", "\u55b6\u5c02 \".txt");
    }

    @Test
    public void testSpaceNames() {
        process("git-diff-space-trailingtab.out");
        verify(callback).onDiffStart(null, "a b.txt");
    }

    @Test
    public void testBinaryFileNameWithNonEnglishCharacters() {
        process("git-diff-with-binary-fine-non-english-file-name.out");
        verify(callback).onBinary("Guide de démarrage.pdf", "Guide de démarrage.pdf");
    }

    @Test
    public void testTranscodedDiffWithBinaryFile() {
        process("git-diff-transcoded-binary.out");

        InOrder ordered = inOrder(callback);
        ordered.verify(callback).onDiffStart("README.md", "README.md");
        ordered.verify(callback).onHunkStart(1, 1, 1, 4, null);
        ordered.verify(callback).onSegmentStart(DiffSegmentType.ADDED);
        ordered.verify(callback).onSegmentLine("Edited on branch", false);
        ordered.verify(callback, times(2)).onSegmentLine("", false);
        ordered.verify(callback).onSegmentEnd(false);
        ordered.verify(callback).onSegmentStart(DiffSegmentType.CONTEXT);
        ordered.verify(callback).onSegmentLine("Hello, world!", false);
        ordered.verify(callback).onSegmentEnd(false);
        ordered.verify(callback).onHunkEnd(false);
        ordered.verify(callback).onDiffEnd(false);
        ordered.verify(callback).onBinary(null, "barney-rubble.png");
    }

    @Test
    public void testTranscodedDiffWithTwoBinaryFiles() {
        process("git-diff-transcoded-binary-two-files.out");

        InOrder ordered = inOrder(callback);
        ordered.verify(callback).onBinary("barney-rubble.png", null);
        ordered.verify(callback).onBinary(null, "brubble.jpg");
    }

    @Test
    public void testNewEmptyFile() {
        process("git-diff-new-empty-file.out");

        InOrder ordered = inOrder(callback);
        ordered.verify(callback).onDiffStart(null,
                "features/pipelines/src/main/resources/i18n/bitbucket-pipelines.properties");
        ordered.verify(callback).onDiffEnd(false);
    }

    @Test
    public void testNullByteAfter8000Bytes() {
        process("git-diff-with-null-byte-after-8000-chars.out");

        InOrder ordered = inOrder(callback);
        ordered.verify(callback).onDiffStart("file", "file");
        ordered.verify(callback).onHunkStart(1, 1, 1, 1, null);
        ordered.verify(callback).onSegmentStart(DiffSegmentType.REMOVED);
        ordered.verify(callback).onSegmentLine(eq(repeat("0", 100)), eq(true));
        ordered.verify(callback).onSegmentEnd(false);
        ordered.verify(callback).onSegmentStart(DiffSegmentType.ADDED);
        ordered.verify(callback).onSegmentLine(eq(repeat("0", 100)), eq(true));
        ordered.verify(callback).onSegmentEnd(false);
        ordered.verify(callback).onHunkEnd(false);
        ordered.verify(callback).onDiffEnd(false);
        verifyNoMoreInteractions(callback);
    }

    @Test
    public void testTruncationAfterHunk() {
        // The max number of lines is set to 10 in the context.
        // The input file contains a diff with 10 lines in the first segment and then a new hunk, so the truncation
        // should happen just after the first hunk ends.
        process("git-diff-truncated-after-hunk.out");

        InOrder ordered = inOrder(callback);
        ordered.verify(callback).onDiffStart("src/language-packs.json", "src/language-packs.json");
        ordered.verify(callback).onHunkStart(83, 21, 83, 21, null);
        ordered.verify(callback).onSegmentStart(DiffSegmentType.CONTEXT);
        ordered.verify(callback, times(10)).onSegmentLine(anyString(), anyBoolean());
        ordered.verify(callback).onSegmentEnd(false);
        ordered.verify(callback).onHunkEnd(false);
        ordered.verify(callback).onDiffEnd(true);
        verifyNoMoreInteractions(callback);
    }

    @Test
    public void testTruncationInSegment() {
        // The max number of lines is set to 10 in the context.
        // The input file contains a diff with 12 lines in the first segment, which means the segment should be truncated.
        process("git-diff-truncated-in-segment.out");

        InOrder ordered = inOrder(callback);
        ordered.verify(callback).onDiffStart("src/language-packs.json", "src/language-packs.json");
        ordered.verify(callback).onHunkStart(83, 21, 83, 21, null);
        ordered.verify(callback).onSegmentStart(DiffSegmentType.CONTEXT);
        ordered.verify(callback, times(10)).onSegmentLine(anyString(), anyBoolean());
        ordered.verify(callback).onSegmentEnd(true);
        ordered.verify(callback).onHunkEnd(true);
        ordered.verify(callback).onDiffEnd(true);
        verifyNoMoreInteractions(callback);
    }

    private void process(String filename) {
        process(filename, 10, 100);
    }
    
    private void process(String filename, int maxLines, int maxLineLength) {
        DiffStdioHandler handler = new DiffStdioHandler(callback, null, maxLines, maxLineLength);
        //200 here is large enough to get multiple diff lines in a single read(ByteBuffer) result but small enough
        //we end up using multiple reads to stream the entire InputStream, which ensures we test handling for
        //"split buffers" (diffs whose bytes are spread across multiple buffers)
        handleStdout(handler, filename, 200);
    }
}
