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

import com.atlassian.bitbucket.mesh.git.content.Blame;
import com.atlassian.bitbucket.mesh.git.content.PorcelainBlameCallback;
import com.atlassian.bitbucket.mesh.util.PageRange;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.InOrder;
import org.mockito.junit.MockitoJUnitRunner;

import java.util.List;

import static com.atlassian.bitbucket.mesh.util.ProcessStdioHandlerUtils.handleStdout;
import static com.google.common.collect.Iterables.getLast;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.hasSize;
import static org.junit.Assert.assertEquals;
import static org.mockito.Mockito.*;

@RunWith(MockitoJUnitRunner.Silent.class)
public class PorcelainBlameStdioHandlerTest {

    @Captor
    private ArgumentCaptor<Blame> blameCaptor;

    @Test
    public void testBinary() {
        PorcelainBlameCallback callback = mockCallback(PorcelainBlameCallback.class);

        PorcelainBlameStdioHandler handler = new PorcelainBlameStdioHandler(callback, new PageRange(10, 0), 100);
        process(handler, "git-blame-porcelain-binary.out", 64 * 1024);

        verify(callback).onBinary();
    }

    @Test
    public void testEmpty() {
        PorcelainBlameCallback callback = mockCallback(PorcelainBlameCallback.class);

        PorcelainBlameStdioHandler handler = new PorcelainBlameStdioHandler(callback, new PageRange(500, 0), 100);
        process(handler, "git-blame-porcelain-empty.out");

        verifyNoInteractions(callback);
    }

    @Test
    public void testExecuteDroppingLine() {
        PorcelainBlameCallback callback = mockCallback(PorcelainBlameCallback.class);

        //This will exercise the code path where the extra line from the PageRange adds an extra line to the final
        //multiline span which must be dropped to ensure correct blame
        PorcelainBlameStdioHandler handler = new PorcelainBlameStdioHandler(callback, new PageRange(500, 500), 2000);
        process(handler, "git-blame-porcelain-paged-trim-span.out");

        verify(callback, atLeastOnce()).onBlame(blameCaptor.capture());

        Blame lastBlame = getLast(blameCaptor.getAllValues());
        assertEquals(34, lastBlame.getSpannedLines()); //The actual span in the output covers 35 lines
    }

    @Test
    public void testExecuteTruncated() {
        PorcelainBlameCallback callback = mockCallback(PorcelainBlameCallback.class);

        PorcelainBlameStdioHandler handler = new PorcelainBlameStdioHandler(callback, new PageRange(1, 0), 6);
        process(handler, "git-blame-porcelain-paged.out");

        verify(callback, atLeastOnce()).onLine("import", 11, true);
    }

    @Test
    public void testExecuteDroppingSpan() {
        PorcelainBlameCallback callback = mockCallback(PorcelainBlameCallback.class);

        //This will exercise the code path where the extra line from the PageRequest brings in a new span which must
        //be dropped to ensure correct blame
        PorcelainBlameStdioHandler handler = new PorcelainBlameStdioHandler(callback, new PageRange(10, 10), 100);
        process(handler, "git-blame-porcelain-paged.out");

        InOrder ordered = inOrder(callback);
        ordered.verify(callback).onLine(eq("import java.io.OutputStream;"), eq(11), eq(false));
        ordered.verify(callback).onLine(eq(""), eq(12), eq(false));
        ordered.verify(callback).onLine(eq("public interface ScmClient {"), eq(13), eq(false));
        ordered.verify(callback).onLine(eq("    Command<Page<Branch>> getBranchesCommand(Repository repository, PageRequest pageRequest, String "), eq(14), eq(true));
        ordered.verify(callback).onLine(eq("    Command<FileTreeNodeRevision.Type> getTypeCommand(Repository repository, String commitId, String"), eq(15), eq(true));
        ordered.verify(callback).onLine(eq("    Command<Void> getFileStreamCommand(Repository repository, String objectId, String path, PageRequ"), eq(16), eq(true));
        ordered.verify(callback).onLine(eq("    Command<Void> getFileStreamCommand(Repository repository, String objectId, String path, OutputSt"), eq(17), eq(true));
        ordered.verify(callback).onLine(eq("    Command<Void> getCloneCommand(Repository repository, Repository fork);"), eq(18), eq(false));
        ordered.verify(callback).onLine(eq("    Command<Void> getConfigCommand(Repository repository, String key, String value);"), eq(19), eq(false));
        ordered.verify(callback).onLine(eq("    Command<Branch> getDefaultBranchCommand(Repository repository);"), eq(20), eq(false));
        ordered.verify(callback, atLeastOnce()).onBlame(blameCaptor.capture());
        ordered.verifyNoMoreInteractions();

        List<Blame> blames = blameCaptor.getAllValues();
        assertBlame("529d5911080d80064a289d030fe60f28e665e3c2", 11, blames.get(0));
        assertBlame("959ef44bec5ec7200dcb8bc40aa62c271e459942", 12, 2, blames.get(1));
        assertBlame("5049e2ee264cd792f51e0bbe04d52871a245136a", 14, blames.get(2));
        assertBlame("e066aace2b950f770872f8ca90f4b760165f3099", 15, blames.get(3));
        assertBlame("6b52185c5cf8da1b0f8df4ef79611d94c52f8a6b", 16, blames.get(4));
        assertBlame("529d5911080d80064a289d030fe60f28e665e3c2", 17, blames.get(5));
        assertBlame("6076ae70d29564dabddb5d6e0430545f7aea954f", 18, 3, blames.get(6));
    }

    @Test
    public void testExecuteDroppingSpanWithConfusingCommitMessage() {
        PorcelainBlameCallback callback = mockCallback(PorcelainBlameCallback.class);

        //This will exercise the code path where the extra line from the PageRequest brings in a new span which must
        //be dropped to ensure correct blame
        PorcelainBlameStdioHandler handler = new PorcelainBlameStdioHandler(callback, new PageRange(10, 10), 100);
        process(handler, "git-blame-porcelain-paged-confusing-commit-message.out");

        verify(callback, atLeastOnce()).onBlame(blameCaptor.capture());

        List<Blame> blames = blameCaptor.getAllValues();
        assertThat(blames, hasSize(7));
        assertBlame("529d5911080d80064a289d030fe60f28e665e3c2", 11, blames.get(0));
        assertBlame("959ef44bec5ec7200dcb8bc40aa62c271e459942", 12, 2, blames.get(1));
        assertBlame("5049e2ee264cd792f51e0bbe04d52871a245136a", 14, blames.get(2));
        assertBlame("e066aace2b950f770872f8ca90f4b760165f3099", 15, blames.get(3));
        assertBlame("6b52185c5cf8da1b0f8df4ef79611d94c52f8a6b", 16, blames.get(4));
        assertBlame("529d5911080d80064a289d030fe60f28e665e3c2", 17, blames.get(5));
        assertBlame("6076ae70d29564dabddb5d6e0430545f7aea954f", 18, 3, blames.get(6));
    }

    private static void assertBlame(String hash, int lineNumber, Blame blame) {
        assertBlame(hash, lineNumber, 1, blame);
    }

    private static void assertBlame(String hash, int lineNumber, int spannedLines, Blame blame) {
        assertEquals(hash, blame.getCommitId());
        assertEquals(lineNumber, blame.getLineNumber());
        assertEquals(spannedLines, blame.getSpannedLines());
    }

    private static <T extends PorcelainBlameCallback> T mockCallback(Class<T> callbackClass) {
        T callback = mock(callbackClass);
        doReturn(true).when(callback).onBlame(any());
        doReturn(true).when(callback).onLine(anyString(), anyInt(), anyBoolean());

        return callback;
    }

    private static void process(PorcelainBlameStdioHandler handler, String resourceName) {
        handleStdout(handler, resourceName);
    }

    private static void process(PorcelainBlameStdioHandler handler, String resourceName, int buffSize) {
        handleStdout(handler, resourceName, buffSize);
    }
}
