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

import org.apache.commons.lang3.StringUtils;
import org.junit.Test;

import java.io.*;
import java.nio.ByteBuffer;
import java.nio.channels.Channels;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

import static com.atlassian.bitbucket.mesh.git.hook.GitHookInputParser.ChunkType.HOOK_REQUEST_ID;
import static com.atlassian.bitbucket.mesh.git.hook.GitHookOutputWriter.SOCKET_OUTPUT_MAX;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;

public class GitHookOutputWriterTest {

    // U+1F600 "GRINNING FACE" has length of 2 in String, but 4 bytes encoded as UTF-8
    private static final String SUPPLEMENTARY = "\uD83D\uDE00";

    @Test
    public void testWriteExitCode() throws IOException {
        byte[] bytes = withWriter(output -> output.writeExitCode(1));

        DataInputStream inputStream = new DataInputStream(new ByteArrayInputStream(bytes));
        assertEquals('X', inputStream.readByte());
        assertEquals(1, inputStream.readUnsignedShort());
    }

    @Test
    public void testWriteStderrBytes() throws IOException {
        byte[] result = withWriter(output -> output.writeStderr(toBuffer("test")));
        assertEquals("test", assembleChunks(result));
    }

    @Test
    public void testWriteStderrTextDoesNotSplitCharacters() throws IOException {
        // Naive splitting at the byte level will result in the Unicode character being split after first byte
        String part1 = StringUtils.repeat("a", SOCKET_OUTPUT_MAX - 1);
        String part2 = SUPPLEMENTARY;
        String input = part1 + part2;
        assertByteLength(SOCKET_OUTPUT_MAX + 3, input);
        byte[] result = withWriter(output -> output.writeStderr(input));

        List<String> chunks = readChunks(result);
        assertEquals(part1, chunks.get(0));
        assertEquals(part2, chunks.get(1));

        ByteArrayOutputStream b = new ByteArrayOutputStream();
        DataOutputStream s = new DataOutputStream(b);
        s.write('E');
        s.writeShort(SOCKET_OUTPUT_MAX - 1);
        s.write(part1.getBytes(StandardCharsets.UTF_8));
        s.write('E');
        s.writeShort(4);
        s.write(part2.getBytes(StandardCharsets.UTF_8));
        assertArrayEquals(b.toByteArray(), result);
    }

    @Test
    public void testWriteStdoutBytes() throws IOException {
        byte[] result = withWriter(output -> output.writeStdout(toBuffer("test")));
        assertEquals("test", assembleChunks(result));
    }

    @Test
    public void testWriteStdoutBytesChunksIfNecessary() throws IOException {
        String part1 = StringUtils.repeat("a", SOCKET_OUTPUT_MAX);
        String part2 = StringUtils.repeat("b", SOCKET_OUTPUT_MAX);
        String input = part1 + part2;

        byte[] result = withWriter(output -> output.writeStdout(toBuffer(input)));
        assertEquals(input, assembleChunks(result));
    }

    @Test
    public void testWriteStdoutBytesWithUnicode() throws IOException {
        String input = StringUtils.repeat(SUPPLEMENTARY, SOCKET_OUTPUT_MAX);
        assertByteLength(SOCKET_OUTPUT_MAX * 4, input);

        byte[] result = withWriter(output -> output.writeStdout(toBuffer(input)));
        assertEquals(input, assembleChunks(result));
    }

    @Test
    public void testWriteStdoutTextChunksUnicode() throws IOException {
        String input = StringUtils.repeat(SUPPLEMENTARY, SOCKET_OUTPUT_MAX);
        assertByteLength(SOCKET_OUTPUT_MAX * 4, input);

        byte[] result = withWriter(output -> output.writeStdout(input));

        List<String> chunks = readChunks(result);
        assertEquals(5, chunks.size());
        assertEquals(StringUtils.repeat(SUPPLEMENTARY, 16383), chunks.get(0));
        assertEquals(StringUtils.repeat(SUPPLEMENTARY, 16383), chunks.get(1));
        assertEquals(StringUtils.repeat(SUPPLEMENTARY, 16383), chunks.get(2));
        assertEquals(StringUtils.repeat(SUPPLEMENTARY, 16383), chunks.get(3));
        assertEquals(StringUtils.repeat(SUPPLEMENTARY, 3), chunks.get(4));
    }

    private static String assembleChunks(byte[] bytes) {
        ByteArrayOutputStream buffer = new ByteArrayOutputStream(bytes.length);
        int index = 0;
        while (index < bytes.length) {
            int length = ByteBuffer.wrap(bytes, index + 1, 2).getShort() & 0xFFFF;
            index += 3; //1 for type + 2 for length

            buffer.write(bytes, index, length);
            index += length;
        }

        return new String(buffer.toByteArray(), StandardCharsets.UTF_8);
    }

    private static void assertByteLength(int expectedLength, String input) {
        assertEquals(expectedLength, input.getBytes(StandardCharsets.UTF_8).length);
    }

    private static List<String> readChunks(byte[] inputBytes) throws IOException {
        //Create a copy of the data array, and then walk over it and rewrite all of the types
        //with X, allowing us to use GitHookInputParser.readChunk(HOOK_REQUEST_ID) to read them back
        byte[] bytes = Arrays.copyOf(inputBytes, inputBytes.length);

        int index = 0;
        while (index < bytes.length) {
            bytes[index] = 'X';
            index += ByteBuffer.wrap(bytes, index + 1, 2).getShort() & 0xFFFF;
            index += 3; //1 for type + 2 for length
        }

        ByteArrayInputStream inputStream = new ByteArrayInputStream(bytes);
        GitHookInputParser parser = new GitHookInputParser(Channels.newChannel(inputStream), null, Integer.MAX_VALUE);

        List<String> chunks = new ArrayList<>();
        while (inputStream.available() > 0) {
            chunks.add(parser.readChunk(HOOK_REQUEST_ID));
        }

        return chunks;
    }

    private static ByteBuffer toBuffer(String message) {
        return ByteBuffer.wrap(message.getBytes(StandardCharsets.UTF_8));
    }

    private static byte[] withWriter(OutputConsumer consumer) throws IOException {
        ByteArrayOutputStream buffer = new ByteArrayOutputStream();

        GitHookOutputWriter writer = new GitHookOutputWriter(Channels.newChannel(buffer));
        consumer.withWriter(writer);

        return buffer.toByteArray();
    }

    @FunctionalInterface
    private interface OutputConsumer {

        void withWriter(GitHookOutputWriter output) throws IOException;
    }
}
