package com.atlassian.stash.internal.hook;

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

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

import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;

public class SocketTransferOutputTest {

    private static final int MAX_BYTES = SocketTransferOutput.SOCKET_OUTPUT_MAX;

    // 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 testSimple() throws IOException {
        byte[] result = write("test");

        List<SocketTransferInput.Chunk> chunks = readChunksAsStdin(result);
        assertEquals(chunks.get(0).getValue(), "test");
    }

    @Test
    public void testTwoChunksNecessary() throws IOException {
        String part1 = StringUtils.repeat("a", MAX_BYTES);
        String part2 = StringUtils.repeat("b", MAX_BYTES);
        String input = part1 + part2;
        byte[] result = write(input);

        List<SocketTransferInput.Chunk> chunks = readChunksAsStdin(result);
        assertEquals(2, chunks.size());
        assertEquals(part1, chunks.get(0).getValue());
        assertEquals(part2, chunks.get(1).getValue());
    }

    @Test
    public void testSupplementaryChunks() throws IOException {
        String input = StringUtils.repeat(SUPPLEMENTARY, MAX_BYTES);
        assertByteLength(MAX_BYTES * 4, input);
        byte[] result = write(input);

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

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

        List<SocketTransferInput.Chunk> chunks = readChunksAsStdin(result);
        assertEquals(part1, chunks.get(0).getValue());
        assertEquals(part2, chunks.get(1).getValue());

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

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

    private static byte[] write(String s) throws IOException {
        try (ByteArrayOutputStream out = new ByteArrayOutputStream();
             SocketTransferOutput output = new SocketTransferOutput(out)) {
            output.writeStdOut(s);
            output.flush();
            return out.toByteArray();
        }
    }

    private List<SocketTransferInput.Chunk> readChunksAsStdin(byte[] inputBytes) throws IOException {
        byte[] buffer = Arrays.copyOf(inputBytes, inputBytes.length);
        try (ByteArrayInputStream inputStream = new ByteArrayInputStream(buffer);
             SocketTransferInput input = new SocketTransferInput(inputStream)) {
            List<SocketTransferInput.Chunk> chunks = new ArrayList<>();
            while (true) {
                try {
                    // When the bytes were written with SocketTransferOutput, the channel was 'O' (stdout). To be able
                    // to read it back in, change it to 'I' (stdin).
                    if (inputStream.available() > 0) {
                        buffer[buffer.length - inputStream.available()] = 'I';
                    }
                    SocketTransferInput.Chunk chunk = input.readChunk();
                    chunks.add(chunk);
                } catch (EOFException e) {
                    return chunks;
                }
            }
        }
    }

}
