package com.atlassian.bitbucket.mesh.git.lstree;

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.builder.lstree.DefaultDirectoryCreator;
import com.atlassian.bitbucket.mesh.git.builder.lstree.DirectoryCreator;
import com.atlassian.bitbucket.mesh.git.builder.lstree.DirectoryLsTreeStdioHandler;
import com.atlassian.bitbucket.mesh.git.content.Directory;
import com.atlassian.bitbucket.mesh.git.content.DirectoryCallback;
import com.atlassian.bitbucket.mesh.git.content.File;
import com.atlassian.bitbucket.mesh.git.exception.CommandFailedException;
import com.atlassian.bitbucket.mesh.git.exception.PathNotFoundException;
import com.atlassian.bitbucket.mesh.git.submodule.Submodule;
import com.atlassian.bitbucket.mesh.git.submodule.Submodules;
import com.atlassian.bitbucket.mesh.grpc.FragmentResponseObserver;
import com.atlassian.bitbucket.mesh.repository.Repository;
import com.atlassian.bitbucket.mesh.rpc.v1.PlumbingServiceGrpc;
import com.atlassian.bitbucket.mesh.rpc.v1.RpcLsTreeRequest;
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.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.Mock;
import org.mockito.Spy;

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

import static org.mockito.Mockito.*;

//Parallels LsTreeCommandTest in the Git SCM
public class LsTreeRequestTest extends AbstractGrpcTest {

    @Spy
    private final DirectoryCreator directoryCreator = new DefaultDirectoryCreator();

    @Mock
    private DirectoryCallback callback;
    @Captor
    private ArgumentCaptor<Directory> directoryCaptor;
    @Captor
    private ArgumentCaptor<File> fileCaptor;
    @Resource
    private ManagedChannel managedChannel;
    @Repo(GitRepositories.QA_RESOURCES)
    private Repository qaResources;
    @Repo
    private Repository repository;
    @Captor
    private ArgumentCaptor<Submodule> submoduleCaptor;
    @Mock
    private Submodules submodules;
    @Repo(GitRepositories.SUBREPOSITORIES)
    private Repository subrepositories;
    private PlumbingServiceGrpc.PlumbingServiceStub stub;

    @Before
    public void setup() {
        setupCallback();

        stub = PlumbingServiceGrpc.newStub(managedChannel);
    }

    @Test
    public void testLsTree() {
        RpcLsTreeRequest request = RpcLsTreeRequest.newBuilder()
                .setNullTerminated(true)
                .setTreeish(ByteString.copyFromUtf8("7a18441e9b7a2fd86287db2d219050a926113b2b"))
                .setRepository(repository.getId())
                .build();

        lsTree(request, 10, 0);

        verify(callback, times(9)).onDirectory(directoryCaptor.capture());
        verify(callback, never()).onFile(fileCaptor.capture());
        verify(callback, never()).onSubmodule(submoduleCaptor.capture());

        List<Directory> directories = directoryCaptor.getAllValues();
        assertEquals(9, directories.size());
        assertDirectory(directories.get(0), "add_file", "b2b3ad5a3bd8bddb201b1d2677f6322dfe9b0973");
        assertDirectory(directories.get(1), "basic_branching", "07cc2c8ac9f3987120fcbe1f891a349de432f8c1");
        assertDirectory(directories.get(2), "binary", "f79fda8638b00cf1f457f7f516b2981ea9e74305");
        assertDirectory(directories.get(3), "branch_mod_merge", "b6b1c6d78bdc5a7a792f74f5502233759c1e37b8");
        assertDirectory(directories.get(4), "modification", "bc09116d8cc0595f4aa1e303f151a245217337ad");
        assertDirectory(directories.get(5), "moved_dir", "4c80c3cf158048d4d47457e9aed4d23fabf96595");
        assertDirectory(directories.get(6), "mv_file", "1ff86b241903dab46340611c450069ebdcaa3d2c");
        assertDirectory(directories.get(7), "out_of_order", "0dadaa05e68f84a47221327729cf25b8ae6aa9aa");
        assertDirectory(directories.get(8), "symlink", "4c54863857092fad52e494e1b3db94fe4e3b136e");
    }

    @Test
    public void testLsTreeWithPaths() {
        RpcLsTreeRequest request = RpcLsTreeRequest.newBuilder()
                .addPaths(ByteString.copyFromUtf8("binary"))
                .addPaths(ByteString.copyFromUtf8("does_not_match"))
                .addPaths(ByteString.copyFromUtf8("moved_dir/file.txt"))
                .setNullTerminated(true)
                .setRepository(repository.getId())
                .setTreeish(ByteString.copyFromUtf8("7a18441e9b7a2fd86287db2d219050a926113b2b"))
                .build();

        lsTree(request, 10, 0);

        verify(callback).onDirectory(directoryCaptor.capture());
        verify(callback).onFile(fileCaptor.capture());
        verify(callback, never()).onSubmodule(submoduleCaptor.capture());

        List<Directory> directories = directoryCaptor.getAllValues();
        assertEquals(1, directories.size());
        assertDirectory(directories.get(0), "binary", "f79fda8638b00cf1f457f7f516b2981ea9e74305");

        List<File> files = fileCaptor.getAllValues();
        assertEquals(1, files.size());
        assertFile(files.get(0), "moved_dir/file.txt", "46fa903c8af8a92c2714a47ae5a341a99c2ee3b2");
    }

    @Test
    public void testLsTreeForPathWithTrailingWhitespace() {
        RpcLsTreeRequest request = RpcLsTreeRequest.newBuilder()
                .setNullTerminated(true)
                .setRepository(qaResources.getId())
                .setTreeish(ByteString.copyFromUtf8("master:spaces/trailing-space "))
                .build();

        lsTree(request, 10, 0);

        verify(callback, never()).onDirectory(directoryCaptor.capture());
        verify(callback).onFile(fileCaptor.capture());
        verify(callback, never()).onSubmodule(submoduleCaptor.capture());

        List<File> files = fileCaptor.getAllValues();
        assertEquals(1, files.size());
        assertFile(files.get(0), "file.txt", "48dc8eadc74441d64c0def3d7ccfbb1fb3fcf572");
    }

    @Test
    public void testLsTreeRecursive() {
        RpcLsTreeRequest request = RpcLsTreeRequest.newBuilder()
                .setNullTerminated(true)
                .setRecursive(true)
                .setRepository(repository.getId())
                .setTreeish(ByteString.copyFromUtf8("7a18441e9b7a2fd86287db2d219050a926113b2b"))
                .build();

        lsTree(request, 100, 0);

        verify(callback, never()).onDirectory(directoryCaptor.capture());
        verify(callback, times(12)).onFile(fileCaptor.capture());
        verify(callback, never()).onSubmodule(submoduleCaptor.capture());

        List<File> files = fileCaptor.getAllValues();
        assertEquals(12, files.size());
        assertFile(files.get(0), "add_file/add_file.txt", "493021b1c9e0ecfebcd5ebce646a44138bc60b39");
        assertFile(files.get(1), "basic_branching/file.txt", "d8c00a7fe6eb6a00378e103a46e3c472c084a949");
        assertFile(files.get(2), "binary/B.zip", "927b01c060cf0dc5036e19abeab8e332d9075ea0");
        assertFile(files.get(3), "binary/C.zip", "72555d02a139700be930a6734b57c5fab84512b9");
        assertFile(files.get(4), "binary/D.zip", "72555d02a139700be930a6734b57c5fab84512b9");
        assertFile(files.get(5), "branch_mod_merge/file.txt", "668076c308990933c2df66efb87b946704252486");
        assertFile(files.get(6), "modification/mod_file.txt", "b1d99e581f0bb8138b8997f9268294e41faee05b");
        assertFile(files.get(7), "moved_dir/file.txt", "46fa903c8af8a92c2714a47ae5a341a99c2ee3b2");
        assertFile(files.get(8), "mv_file/moved_file.txt", "f49abb7d6c366e6f83470e36439bc162234d7963");
        assertFile(files.get(9), "out_of_order/file.txt", "2e6b03cbbe90d3c9afcfc98967a5a71b4c9cff1a");
        assertFile(files.get(10), "symlink/link.txt", "4cbb553f3f4ac2ee7b01ff6c951d6bf583c39c15");
        assertFile(files.get(11), "symlink/target.txt", "57a3592bb2cf8e76e2faef7660945a2fe0e24360");
    }

    @Test
    public void testLsTreeSized() {
        RpcLsTreeRequest request = RpcLsTreeRequest.newBuilder()
                .setNullTerminated(true)
                .setRecursive(true)
                .setRepository(repository.getId())
                .setSizes(true)
                .setTreeish(ByteString.copyFromUtf8("master:binary"))
                .build();

        lsTree(request, 100, 0);

        verify(callback, never()).onDirectory(directoryCaptor.capture());
        verify(callback, times(3)).onFile(fileCaptor.capture());
        verify(callback, never()).onSubmodule(submoduleCaptor.capture());

        List<File> files = fileCaptor.getAllValues();
        assertEquals(3, files.size());
        assertFile(files.get(0), "B.zip", "927b01c060cf0dc5036e19abeab8e332d9075ea0", 167L);
        assertFile(files.get(1), "C.zip", "72555d02a139700be930a6734b57c5fab84512b9", 167L);
        assertFile(files.get(2), "D.zip", "72555d02a139700be930a6734b57c5fab84512b9", 167L);
    }

    @Test
    public void testLsTreePaged() {
        RpcLsTreeRequest request = RpcLsTreeRequest.newBuilder()
                .setNullTerminated(true)
                .setRepository(repository.getId())
                .setTreeish(ByteString.copyFromUtf8("ed69be6064dcef2d0b08b35192f581613e48411b"))
                .build();

        lsTree(request, 3, 0);

        verify(callback, times(3)).onDirectory(directoryCaptor.capture());
        verify(callback, never()).onFile(fileCaptor.capture());
        verify(callback, never()).onSubmodule(submoduleCaptor.capture());

        List<Directory> directories = directoryCaptor.getAllValues();
        assertEquals(3, directories.size());
        assertDirectory(directories.get(0), "add_file", "b2b3ad5a3bd8bddb201b1d2677f6322dfe9b0973");
        assertDirectory(directories.get(1), "modification", "bc09116d8cc0595f4aa1e303f151a245217337ad");
        assertDirectory(directories.get(2), "mv_dir", "4c80c3cf158048d4d47457e9aed4d23fabf96595");

        setupCallback();
        //Retrieve the next page using the same request
        lsTree(request, 6, 3);

        directoryCaptor = ArgumentCaptor.forClass(Directory.class);
        verify(callback, times(3)).onDirectory(directoryCaptor.capture());
        verify(callback, never()).onFile(fileCaptor.capture());
        verify(callback, never()).onSubmodule(submoduleCaptor.capture());

        directories = directoryCaptor.getAllValues();
        assertEquals(3, directories.size());
        assertDirectory(directories.get(0), "mv_file", "1ff86b241903dab46340611c450069ebdcaa3d2c");
        assertDirectory(directories.get(1), "out_of_order", "0dadaa05e68f84a47221327729cf25b8ae6aa9aa");
        assertDirectory(directories.get(2), "symlink", "4c54863857092fad52e494e1b3db94fe4e3b136e");
    }

    @Test
    public void testLsTreeOnFile() {
        RpcLsTreeRequest.Builder builder = RpcLsTreeRequest.newBuilder()
                .setNullTerminated(true)
                .setRepository(repository.getId())
                .setTreeish(ByteString.copyFromUtf8("bf3346620ec9161977fc23dee1240a7991e64ca1"));

        lsTree(builder.build(), 10, 0);

        verify(callback, times(2)).onDirectory(directoryCaptor.capture());
        verify(callback, never()).onFile(fileCaptor.capture());
        verify(callback, never()).onSubmodule(submoduleCaptor.capture());

        List<Directory> directories = directoryCaptor.getAllValues();
        assertEquals(2, directories.size());
        assertDirectory(directories.get(0), "modification", "bc09116d8cc0595f4aa1e303f151a245217337ad");
        assertDirectory(directories.get(1), "out_of_order", "0dadaa05e68f84a47221327729cf25b8ae6aa9aa");

        builder = RpcLsTreeRequest.newBuilder()
                .setNullTerminated(true)
                .setRepository(repository.getId())
                .setTreeish(ByteString.copyFromUtf8("7a18441e9b7a2fd86287db2d219050a926113b2b:binary"));

        setupCallback();
        lsTree(builder.build(), 10, 0);

        verify(callback, never()).onDirectory(directoryCaptor.capture());
        verify(callback, times(3)).onFile(fileCaptor.capture());
        verify(callback, never()).onSubmodule(submoduleCaptor.capture());

        List<File> files = fileCaptor.getAllValues();
        assertEquals(3, files.size());
        assertFile(files.get(0), "B.zip", "927b01c060cf0dc5036e19abeab8e332d9075ea0");
        assertFile(files.get(1), "C.zip", "72555d02a139700be930a6734b57c5fab84512b9");
        assertFile(files.get(2), "D.zip", "72555d02a139700be930a6734b57c5fab84512b9");
    }

    @Test
    public void testLsTreeRecursiveOnFile() {
        RpcLsTreeRequest.Builder builder = RpcLsTreeRequest.newBuilder()
                .setNullTerminated(true)
                .setRecursive(true)
                .setRepository(repository.getId())
                .setTreeish(ByteString.copyFromUtf8("bf3346620ec9161977fc23dee1240a7991e64ca1"));

        lsTree(builder.build(), 10, 0);

        verify(callback, never()).onDirectory(directoryCaptor.capture());
        verify(callback, times(2)).onFile(fileCaptor.capture());
        verify(callback, never()).onSubmodule(submoduleCaptor.capture());

        List<File> files = fileCaptor.getAllValues();
        assertEquals(2, files.size());
        assertFile(files.get(0), "modification/mod_file.txt", "b1d99e581f0bb8138b8997f9268294e41faee05b");
        assertFile(files.get(1), "out_of_order/file.txt", "2e6b03cbbe90d3c9afcfc98967a5a71b4c9cff1a");

        builder = RpcLsTreeRequest.newBuilder()
                .setNullTerminated(true)
                .setRecursive(true)
                .setRepository(repository.getId())
                .setTreeish(ByteString.copyFromUtf8("7a18441e9b7a2fd86287db2d219050a926113b2b:binary"));

        setupCallback();
        lsTree(builder.build(), 10, 0);

        fileCaptor = ArgumentCaptor.forClass(File.class);
        verify(callback, never()).onDirectory(directoryCaptor.capture());
        verify(callback, times(3)).onFile(fileCaptor.capture());
        verify(callback, never()).onSubmodule(submoduleCaptor.capture());

        files = fileCaptor.getAllValues();
        assertEquals(3, files.size());
        assertFile(files.get(0), "B.zip", "927b01c060cf0dc5036e19abeab8e332d9075ea0");
        assertFile(files.get(1), "C.zip", "72555d02a139700be930a6734b57c5fab84512b9");
        assertFile(files.get(2), "D.zip", "72555d02a139700be930a6734b57c5fab84512b9");
    }

    @Test
    public void testLsTreeOnNonExistentTree() {
        RpcLsTreeRequest request = RpcLsTreeRequest.newBuilder()
                .setNullTerminated(true)
                .setRepository(repository.getId())
                .setTreeish(ByteString.copyFromUtf8("1234567890abcdef1234567890abcdef12345678"))
                .build();

        CommandFailedException thrown =
                assertThrows(CommandFailedException.class, () -> lsTree(request, 10, 0));
        assertSame(Status.Code.INTERNAL, thrown.getStatus().getCode());
    }

    @Test
    public void testLsTreeOnNonExistentPath() {
        RpcLsTreeRequest request = RpcLsTreeRequest.newBuilder()
                .setNullTerminated(true)
                .setRepository(repository.getId())
                .setTreeish(ByteString.copyFromUtf8("bf3346620ec9161977fc23dee1240a7991e64ca1:path-to-nowhere"))
                .build();

        PathNotFoundException thrown =
                assertThrows(PathNotFoundException.class, () -> lsTree(request, 10, 0));
        assertSame(Status.Code.NOT_FOUND, thrown.getStatus().getCode());
    }

    @Test
    public void testLsTreeWithInvalidTree() {
        RpcLsTreeRequest request = RpcLsTreeRequest.newBuilder()
                .addPaths(ByteString.copyFromUtf8("master"))
                .setRepository(repository.getId())
                .setTreeish(ByteString.copyFromUtf8("--full-tree"))
                .build();

        StatusRuntimeException thrown =
                assertThrows(StatusRuntimeException.class, () -> lsTree(request, 10, 0));
        assertSame(Status.Code.INVALID_ARGUMENT, thrown.getStatus().getCode());
        assertThat(thrown, HasGitError.invalidTreeish("--full-tree"));
    }

    @Test
    public void testLsTreeWithInvalidTreeAndLeadingSpace() {
        RpcLsTreeRequest request = RpcLsTreeRequest.newBuilder()
                .addPaths(ByteString.copyFromUtf8("master"))
                .setRepository(repository.getId())
                .setTreeish(ByteString.copyFromUtf8(" --full-tree"))
                .build();

        StatusRuntimeException thrown =
                assertThrows(StatusRuntimeException.class, () -> lsTree(request, 10, 0));
        assertSame(Status.Code.INVALID_ARGUMENT, thrown.getStatus().getCode());
        assertThat(thrown, HasGitError.invalidTreeish("--full-tree"));
    }

    @Test
    public void testSubmodules() {
        RpcLsTreeRequest request = RpcLsTreeRequest.newBuilder()
                .setNullTerminated(true)
                .setRepository(subrepositories.getId())
                .setTreeish(ByteString.copyFromUtf8("ecf42d8adc4d7eb4386e37882d053704067712d4:submodule"))
                .build();

        lsTree(request, 10, 0, LsTreeRequestTest::submodule);

        verify(callback, never()).onDirectory(directoryCaptor.capture());
        verify(callback, never()).onFile(fileCaptor.capture());
        verify(callback, times(3)).onSubmodule(submoduleCaptor.capture());

        List<Submodule> submodules = submoduleCaptor.getAllValues();
        assertEquals(3, submodules.size());
        assertSubmodule(submodules.get(0), "atlassian-refapp", "0ac826865ce7e8bc76022d49424f52e09b16dc87");
        assertSubmodule(submodules.get(1), "bamboo-git-plugin", "92b18c97d9d92d2bd02cfd1d268f22fddddafb67");
        assertSubmodule(submodules.get(2), "stash", "996b4f5a7e242a29fe573582d98f5174142bb97c");
    }

    private static void assertDirectory(Directory directory, String path, String contentId) {
        assertEquals(path, directory.getPath());
        assertEquals(contentId, directory.getContentId());
    }

    private static void assertFile(File file, String path, String contentId) {
        assertEquals(contentId, file.getContentId());
        assertEquals(path, file.getPath());
    }

    private static void assertFile(File file, String path, String contentId, long size) {
        assertFile(file, path, contentId);
        assertEquals((Long) size, file.getSize());
    }

    private static void assertSubmodule(Submodule submodule, String path, String contentId) {
        assertEquals(path, submodule.getPath());
        assertEquals(contentId, submodule.getContentId());
    }

    private static Submodule submodule(String name, String contentId) {
        return new Submodule.Builder(name, contentId)
                .url("/home/something")
                .build();
    }

    private void lsTree(RpcLsTreeRequest request, int limit, int start) {
        lsTree(request, limit, start, submodules);
    }

    private void lsTree(RpcLsTreeRequest request, int limit, int start, Submodules submodules) {
        DirectoryLsTreeStdioHandler handler =
                new DirectoryLsTreeStdioHandler(callback, directoryCreator, new PageRange(limit, start), submodules);

        FragmentResponseObserver<Void> observer =
                new FragmentResponseObserver<>(handler, new GitProcessCompletionHandler());

        stub.lsTree(request, observer);
        observer.asResult();
    }

    private void setupCallback() {
        reset(callback);
        when(callback.onDirectory(any(Directory.class))).thenReturn(true);
        when(callback.onFile(any(File.class))).thenReturn(true);
        when(callback.onSubmodule(any(Submodule.class))).thenReturn(true);
    }
}
