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

import com.atlassian.bitbucket.mesh.AbstractGrpcTest;
import com.atlassian.bitbucket.mesh.git.GitProcessCompletionHandler;
import com.atlassian.bitbucket.mesh.git.GitRepositories;
import com.atlassian.bitbucket.mesh.git.content.Blame;
import com.atlassian.bitbucket.mesh.git.content.PorcelainBlameCallback;
import com.atlassian.bitbucket.mesh.grpc.FragmentResponseObserver;
import com.atlassian.bitbucket.mesh.process.ProcessStdioHandler;
import com.atlassian.bitbucket.mesh.repository.Repository;
import com.atlassian.bitbucket.mesh.rpc.v1.PorcelainServiceGrpc;
import com.atlassian.bitbucket.mesh.rpc.v1.RpcBlameRange;
import com.atlassian.bitbucket.mesh.rpc.v1.RpcBlameRequest;
import com.atlassian.bitbucket.mesh.test.HasGitError;
import com.atlassian.bitbucket.mesh.test.annotation.Repo;
import com.atlassian.bitbucket.mesh.util.PageRange;
import com.google.common.collect.Iterables;
import com.google.protobuf.ByteString;
import io.grpc.ManagedChannel;
import io.grpc.Status;
import io.grpc.StatusRuntimeException;
import org.junit.Before;
import org.junit.Test;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.InOrder;

import javax.annotation.Resource;
import java.util.List;

import static com.atlassian.bitbucket.mesh.rpc.v1.RpcBlameType.INCREMENTAL;
import static com.atlassian.bitbucket.mesh.rpc.v1.RpcBlameType.PORCELAIN;
import static org.hamcrest.Matchers.hasSize;
import static org.mockito.Mockito.*;

public class BlameRequestTest extends AbstractGrpcTest {

    private static final RpcBlameRange.Builder RANGE_REQUEST = RpcBlameRange.newBuilder()
            .setStart(0)
            .setOffset(50);

    @Captor
    private ArgumentCaptor<Blame> blameCaptor;
    @Resource
    private ManagedChannel managedChannel;
    @Repo(GitRepositories.QA_RESOURCES)
    private Repository qaResources;
    @Repo
    private Repository repository;
    private PorcelainServiceGrpc.PorcelainServiceStub stub;

    @Before
    public void setup() {
        stub = PorcelainServiceGrpc.newStub(managedChannel);
    }

    @Test
    public void testBlame() {
        RpcBlameRequest request = RpcBlameRequest.newBuilder()
                .setPath(ByteString.copyFromUtf8("modification/mod_file.txt"))
                .setRange(RANGE_REQUEST)
                .setRepository(repository.getId())
                .setRev(ByteString.copyFromUtf8("master"))
                .setType(INCREMENTAL)
                .build();

        List<Blame> blames = blame(request, new IncrementalBlameStdioHandler());
        assertThat(blames, hasSize(2));
        assertBlame("e26cab95a50f6e99d2c46c498a23cee307cc8e27", blames.get(0));
        assertBlame("bf3346620ec9161977fc23dee1240a7991e64ca1", blames.get(1));
    }

    @Test
    public void testBlameForPathWithTrailingWhitespace() {
        RpcBlameRequest request = RpcBlameRequest.newBuilder()
                .setPath(ByteString.copyFromUtf8("spaces/file.txt ")) //If the path is trimmed, this will fail
                .setRange(RpcBlameRange.newBuilder()
                        .setStart(0)
                        .setEnd(1))
                .setRepository(qaResources.getId())
                .setRev(ByteString.copyFromUtf8("4eae372719e92c48ee67819a5adfa13cc25b59c7"))
                .setType(INCREMENTAL)
                .build();

        Blame blame =
                Iterables.getOnlyElement(blame(request, new IncrementalBlameStdioHandler()));
        assertEquals("4eae372719e92c48ee67819a5adfa13cc25b59c7", blame.getCommitId());
        assertEquals("spaces/file.txt ", blame.getFileName());
        assertEquals(1, blame.getLineNumber());
        assertEquals(1, blame.getSpannedLines());
    }

    @Test
    public void testIncrementalBlameCanAcceptFilesWithLongHistory() {
        RpcBlameRequest request = RpcBlameRequest.newBuilder()
                .setPath(ByteString.copyFromUtf8("Calculator/pom.xml"))
                .setRange(RANGE_REQUEST)
                .setRepository(qaResources.getId())
                .setRev(ByteString.copyFromUtf8("master"))
                .setType(INCREMENTAL)
                .build();

        List<Blame> blames = blame(request, new IncrementalBlameStdioHandler());

        assertNotNull(blames);
        assertThat(blames, hasSize(15));

        assertBlame("0e2dc34332d8c0d754e3f23a939fb4561fa5ad32", blames.get(0));
        assertBlame("7ffb90157ea17ddaf8a619ff41ce5de30250e1c2", blames.get(1));
        assertBlame("295421dabb68cef6a14b1c0ff11768da3a10901d", blames.get(2));
        assertBlame("45ce01986e74aa9431a52fa62de7ed36bf6ab2f4", blames.get(3));
        assertBlame("0e2dc34332d8c0d754e3f23a939fb4561fa5ad32", blames.get(4));
        assertBlame("5688b4f5447ff198ff61ad5ad82b4881745ba12b", blames.get(5));
        assertBlame("5edf6faf5dd39503d04d1538fcaf99d313a43708", blames.get(6));
        assertBlame("0e2dc34332d8c0d754e3f23a939fb4561fa5ad32", blames.get(7));
        assertBlame("584f2854efc3844d348a4ea6f0194f103c70071b", blames.get(8));
        assertBlame("0e2dc34332d8c0d754e3f23a939fb4561fa5ad32", blames.get(9));
        assertBlame("584f2854efc3844d348a4ea6f0194f103c70071b", blames.get(10));
        assertBlame("5c1ccdec5f84aa149a4978f729fdda70769f942f", blames.get(11));
        assertBlame("0e2dc34332d8c0d754e3f23a939fb4561fa5ad32", blames.get(12));
        assertBlame("02c78bc39ac6192623bf080e3e2ac892a4f5764c", blames.get(13));
        assertBlame("0e2dc34332d8c0d754e3f23a939fb4561fa5ad32", blames.get(14));
    }

    @Test
    public void testInvalidRev() {
        RpcBlameRequest request = RpcBlameRequest.newBuilder()
                .setPath(ByteString.copyFromUtf8("binary/B.zip"))
                .setRange(RANGE_REQUEST)
                .setRepository(repository.getId())
                .setRev(ByteString.copyFromUtf8("--progress"))
                .setType(INCREMENTAL)
                .build();

        StatusRuntimeException thrown =
                assertThrows(StatusRuntimeException.class, () -> blame(request, new IncrementalBlameStdioHandler()));
        assertSame(Status.Code.INVALID_ARGUMENT, thrown.getStatus().getCode());
        assertThat(thrown, HasGitError.invalidCommitish("--progress"));
    }

    @Test
    public void testInvalidRevWithLeadingSpace() {
        RpcBlameRequest request = RpcBlameRequest.newBuilder()
                .setPath(ByteString.copyFromUtf8("binary/B.zip"))
                .setRange(RANGE_REQUEST)
                .setRepository(repository.getId())
                .setRev(ByteString.copyFromUtf8(" --progress"))
                .setType(INCREMENTAL)
                .build();

        StatusRuntimeException thrown =
                assertThrows(StatusRuntimeException.class, () -> blame(request, new IncrementalBlameStdioHandler()));
        assertSame(Status.Code.INVALID_ARGUMENT, thrown.getStatus().getCode());
        assertThat(thrown, HasGitError.invalidCommitish("--progress"));
    }

    @Test
    public void testPorcelainBlame() {
        PorcelainBlameCallback callback = mock(PorcelainBlameCallback.class);
        doReturn(true).when(callback).onLine(anyString(), anyInt(), anyBoolean());

        RpcBlameRequest request = RpcBlameRequest.newBuilder()
                .setPath(ByteString.copyFromUtf8("modification/mod_file.txt"))
                .setRange(RANGE_REQUEST)
                .setRepository(repository.getId())
                .setRev(ByteString.copyFromUtf8("master"))
                .setType(PORCELAIN)
                .build();

        PorcelainBlameStdioHandler handler =
                new PorcelainBlameStdioHandler(callback, new PageRange(50, 0), 15);

        blame(request, handler);

        InOrder ordered = inOrder(callback);
        ordered.verify(callback).onLine(eq("this is a test "), eq(1), eq(true));
        ordered.verify(callback).onLine(eq("another line"), eq(2), eq(false));
        ordered.verify(callback, times(2)).onBlame(blameCaptor.capture());
        ordered.verifyNoMoreInteractions();

        List<Blame> blames = blameCaptor.getAllValues();
        assertNotNull(blames);
        assertThat(blames, hasSize(2));
        assertBlame("e26cab95a50f6e99d2c46c498a23cee307cc8e27", blames.get(0));
        assertBlame("bf3346620ec9161977fc23dee1240a7991e64ca1", blames.get(1));
    }

    @Test
    public void testPorcelainBlameWithBinaryFile() {
        PorcelainBlameCallback callback = mock(PorcelainBlameCallback.class);

        RpcBlameRequest request = RpcBlameRequest.newBuilder()
                .setPath(ByteString.copyFromUtf8("binary/B.zip"))
                .setRange(RANGE_REQUEST)
                .setRepository(repository.getId())
                .setRev(ByteString.copyFromUtf8("master"))
                .setType(PORCELAIN)
                .build();

        PorcelainBlameStdioHandler handler =
                new PorcelainBlameStdioHandler(callback, new PageRange(50, 0), 15);

        blame(request, handler);

        InOrder ordered = inOrder(callback);
        ordered.verify(callback).onBinary();
        ordered.verifyNoMoreInteractions();
    }

    private static void assertBlame(String commitId, Blame blame) {
        assertEquals(commitId, blame.getCommitId());
    }

    private <T> T blame(RpcBlameRequest request, ProcessStdioHandler<T> handler) {
        FragmentResponseObserver<T> responseObserver =
                new FragmentResponseObserver<>(handler, new GitProcessCompletionHandler());

        stub.blame(request, responseObserver);

        return responseObserver.asResult();
    }
}
