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

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.submodule.Submodule;
import com.atlassian.bitbucket.mesh.git.submodule.Submodules;
import com.atlassian.bitbucket.mesh.rpc.v1.RpcDirectory;
import com.atlassian.bitbucket.mesh.rpc.v1.RpcFile;
import com.atlassian.bitbucket.mesh.rpc.v1.RpcListDirectoryResponse;
import com.atlassian.bitbucket.mesh.rpc.v1.RpcSubmodule;
import com.atlassian.bitbucket.mesh.util.PageRange;
import com.google.common.collect.ImmutableList;
import com.google.protobuf.ByteString;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.Spy;
import org.mockito.junit.MockitoJUnitRunner;

import javax.annotation.Nonnull;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.ByteBuffer;
import java.nio.channels.Channels;
import java.nio.channels.ReadableByteChannel;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;

import static org.junit.Assert.*;
import static org.mockito.Mockito.*;

@RunWith(MockitoJUnitRunner.class)
public class DirectoryLsTreeStdioHandlerTest {

    private static final List<RpcListDirectoryResponse> RESPONSES = new ImmutableList.Builder<RpcListDirectoryResponse>()
            .add(asFile(".gitignore", "9199324334b1ddceb32280cfa01eee83ce0478f8"))
            .add(asDirectory("api", "75ea10e063546e9fc9baba2e5c7de436ca7c8b55"))
            .add(asDirectory("bundled-plugins", "1b6692dffbd427b298ede4787ed5b09c59e0839d"))
            .add(asDirectory("core", "968372063c38ac0f6aef349feb6ff34cd24d29e9"))
            .add(asDirectory("crowd-password-encoders", "ef97f78417fbe0ebd191cbbd9b15d68504d3e9d8"))
            .add(asDirectory("distribution", "5278864384f5325bf29f2ce11c7d732655cc9de1"))
            .add(asDirectory("func-tests-base", "46628f99325cc765e7fee1cc23f183d9a18db4da"))
            .add(asDirectory("func-tests-browser", "426c38f3db0830bdfc0f87dac16c64c519173bce"))
            .add(asDirectory("func-tests-rest", "369659bb22fc05f81ecfd6adee659c80b2a1e412"))
            .add(asDirectory("i18n-maven-plugin", "6a85943a0997a339e90cdec44637f2cf67624a6b"))
            .add(asDirectory("l10n", "59a8a865c32875f6ed41db19e38d2150ecbd7b89"))
            .add(asDirectory("page-objects", "045bd860f856896d990d56caf080e641d1030fa6"))
            .add(asDirectory("plugin-test-resources", "5e4d94d9300163949879b96ae65dc2173ff515c8"))
            .add(asDirectory("rest", "f1d06a6807c0a1ae3df689112bf79454b8ffc902"))
            .add(asDirectory("sal", "004a2c563044e0d7fd1e0beae043f23e30bd3944"))
            .add(asDirectory("security", "2424c3a1db1abb1824081ce0a5afb775b081c75c"))
            .add(asDirectory("services", "0374eba38b39c968be1c7719f680c7afc2dc2749"))
            .add(asDirectory("util", "135a9145c1c00f4e9b59e7d1ed3a58ed0a1f9ef9"))
            .add(asDirectory("velocity-helper-plugin", "c4054fd6b2816f8a447aa5a8863ed7f86742f529"))
            .add(asDirectory("webapp", "915d5b15c07b824a9e91a94004ca3cca076aea2b"))
            .add(asFile("checkstyle.xml", "f9133e1d633ad0575238119b45771cdd0fed198f"))
            .add(asFile("pom.xml", "cd674e8b1defc9373c9c742adf68d78fa0522019"))
            .build();

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

    @Mock
    private Submodules submodules;

    @Test
    public void testOnExit() {
        createHandler(0, 10).onExit(0);

        //Verify onExit closes the DirectoryCreator. This is important for CollapsingDirectoryCreator,
        //which may have a git cat-file --batch process running
        verify(directoryCreator).close();
    }

    @Test
    public void testParse() throws Exception {
        List<RpcListDirectoryResponse> responses = parse(0, 15);
        assertEquals(15, responses.size());
        for (int i = 0; i < responses.size(); i++) {
            assertEquals(RESPONSES.get(i), responses.get(i));
        }

        responses = parse(16, 15);
        assertEquals(RESPONSES.size() - 16, responses.size());
        for (int i = 0; i < responses.size(); i++) {
            assertEquals(RESPONSES.get(16 + i), responses.get(i));
        }
    }

    @Test
    public void testParseBinaryFile() throws Exception {
        List<RpcListDirectoryResponse> responses = parse(10,
                "100755 blob eabd299520d0ffcff6afaa3892669871554f8f81      22\thello.sh\u0000");
        assertEquals(1, responses.size());

        RpcListDirectoryResponse response = responses.get(0);
        assertSame(RpcListDirectoryResponse.ResponseOneofCase.FILE, response.getResponseOneofCase());

        RpcFile actual = response.getFile();
        assertEquals("eabd299520d0ffcff6afaa3892669871554f8f81", actual.getContentId());
        assertEquals("hello.sh", actual.getPath().toStringUtf8());
        assertTrue("Missing file size", actual.hasSize());
        assertEquals(22L, actual.getSize());
        assertTrue(actual.getExecutable());
    }

    @Test
    public void testParseNegativeStart() throws Exception {
        List<RpcListDirectoryResponse> responses = parse(-5, 5);
        assertNotNull("failed to parse output", responses);
        assertEquals("wrong number of files parsed", 5, responses.size());
        for (int i = 0; i < 5; i++) {
            assertEquals(RESPONSES.get(i), responses.get(i));
        }
    }

    @Test
    public void testLateStart() throws Exception {
        List<RpcListDirectoryResponse> responses = parse(25, 5);
        assertTrue(responses.isEmpty());
    }

    @Test
    public void testParseWithoutOutput() throws Exception {
        List<RpcListDirectoryResponse> responses = parse(10, "");
        assertTrue(responses.isEmpty());
    }

    @Test
    public void testParseSubmodule() throws Exception {
        String contentId = "0a943a29376f2336b78312d99e65da17048951db";
        String path = "submodule";
        Submodule expected = new Submodule.Builder(path, contentId)
                .branch("main")
                .url("test")
                .build();

        when(submodules.get(eq(path), eq(contentId))).thenReturn(expected);

        List<RpcListDirectoryResponse> responses = parse(1, "160000 commit " + contentId + "\t" + path);
        assertEquals(1, responses.size());

        RpcListDirectoryResponse response = responses.get(0);
        assertSame(RpcListDirectoryResponse.ResponseOneofCase.SUBMODULE, response.getResponseOneofCase());

        RpcSubmodule actual = response.getSubmodule();
        assertEquals(expected.getBranch(), actual.getBranch().toStringUtf8());
        assertEquals(expected.getContentId(), actual.getContentId());
        assertEquals(expected.getPath(), actual.getPath().toStringUtf8());
        assertEquals(expected.getUrl(), actual.getUrl());
        assertTrue(actual.getActive());
    }

    private static RpcListDirectoryResponse asDirectory(String path, String contentId) {
        return RpcListDirectoryResponse.newBuilder()
                .setDirectory(RpcDirectory.newBuilder()
                        .setContentId(contentId)
                        .setPath(ByteString.copyFromUtf8(path)))
                .build();
    }

    private static RpcListDirectoryResponse asFile(String path, String contentId) {
        return RpcListDirectoryResponse.newBuilder()
                .setFile(RpcFile.newBuilder()
                        .setContentId(contentId)
                        .setPath(ByteString.copyFromUtf8(path)))
                .build();
    }

    private DirectoryLsTreeStdioHandler createHandler(int start, int limit) {
        return new DirectoryLsTreeStdioHandler(callback, directoryCreator, new PageRange(limit, start), submodules);
    }

    private List<RpcListDirectoryResponse> parse(int start, int limit) throws Exception {
        return parse(start, limit, getClass().getResourceAsStream("git-ls-tree-z-995755f6.out"));
    }

    private List<RpcListDirectoryResponse> parse(int start, int limit, InputStream in) throws IOException {
        //Stream the provided input through the handler, and then call onExit to ensure any trailing
        //fragment is sent to onNext
        DirectoryLsTreeStdioHandler handler = createHandler(start, limit);
        try (ReadableByteChannel channel = Channels.newChannel(in)) {
            ByteBuffer buffer = ByteBuffer.allocate(25);

            boolean closed;
            do {
                closed = channel.read(buffer) == -1;
                buffer.flip();

                handler.onStdout(buffer, closed);
                buffer.compact();
            } while (!closed);
        }
        handler.onExit(0);

        return callback.getResponses();
    }

    private List<RpcListDirectoryResponse> parse(int limit, String data) throws IOException {
        return parse(0, limit, new ByteArrayInputStream(data.getBytes(StandardCharsets.UTF_8)));
    }

    private static class CollectingDirectoryCallback implements DirectoryCallback {

        private final List<RpcListDirectoryResponse> responses;

        CollectingDirectoryCallback() {
            responses = new ArrayList<>();
        }

        @Override
        public boolean onDirectory(@Nonnull Directory directory) {
            responses.add(RpcListDirectoryResponse.newBuilder()
                    .setDirectory(directory.toDirectory())
                    .build());
            return true;
        }

        @Override
        public boolean onFile(@Nonnull File file) {
            responses.add(RpcListDirectoryResponse.newBuilder()
                    .setFile(file.toFile())
                    .build());
            return true;
        }

        @Override
        public boolean onSubmodule(@Nonnull Submodule submodule) {
            responses.add(RpcListDirectoryResponse.newBuilder()
                    .setSubmodule(submodule.toSubmodule())
                    .build());
            return true;
        }

        List<RpcListDirectoryResponse> getResponses() {
            List<RpcListDirectoryResponse> copy = ImmutableList.copyOf(responses);
            responses.clear();

            return copy;
        }
    }
}