package com.atlassian.bitbucket.mesh.process.handler;

import com.google.common.collect.ImmutableList;
import org.junit.Test;
import org.mockito.InOrder;

import javax.annotation.Nonnull;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;

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

public class LineDecoderTest {

    private ListLineSink lines = new ListLineSink();

    @Test
    public void testForEachBrokenMultibyte() {
        byte[] bytes = toBytes("\u5718\u96c6");

        LineDecoder decoder = new LineDecoder(StandardCharsets.UTF_8);
        //Pass 5 bytes to get the complete first character and part of the second one
        decoder.forEach(ByteBuffer.wrap(bytes, 0, 5), true, lines);

        //Verify the incomplete character is replaced
        assertEquals(ImmutableList.of("\u5718\uFFFD"), lines.get());
    }

    @Test
    public void testForEachCrAtTheEndOfStream() {
        LineDecoder pump = new LineDecoder(StandardCharsets.UTF_8);
        assertTrue(pump.forEach(toByteBuffer("CR at the end of line\r"), true, lines));
        assertEquals(ImmutableList.of("CR at the end of line"), lines.get());
    }

    @Test
    public void testForEachCrCrLfHandling() {
        LineDecoder decoder = new LineDecoder(StandardCharsets.UTF_8);
        assertTrue(decoder.forEach(toByteBuffer("First\r\r\nSecond\r\r\nThird\r\r\n"), true, lines));
        assertEquals(ImmutableList.of("First\r", "Second\r", "Third\r"), lines.get());
    }

    @Test
    public void testForEachCrCrLfHandlingSpanningBuffers() {
        LineDecoder decoder = new LineDecoder(StandardCharsets.UTF_8);
        assertFalse(decoder.forEach(toByteBuffer("First\r\r\nSecond\r"), false, lines));
        assertTrue(decoder.forEach(toByteBuffer("\r\nThird\r\r\n"), true, lines));
        assertEquals(ImmutableList.of("First\r", "Second\r", "Third\r"), lines.get());
    }

    @Test
    public void testForEachCrLfCrOrLfHandling() {
        LineDecoder pump = new LineDecoder(StandardCharsets.UTF_8, LineDecoder.Mode.CRLF_CR_OR_LF);
        assertTrue(pump.forEach(toByteBuffer("First\rSecond\nThird\r\nFourth"), true, lines));
        assertEquals(ImmutableList.of("First", "Second", "Third", "Fourth"), lines.get());
    }

    @Test
    public void testForEachCrLfHandling() {
        LineDecoder decoder = new LineDecoder(StandardCharsets.UTF_8);
        assertTrue(decoder.forEach(toByteBuffer("First\r\nSecond\nThird\r\n"), true, lines));
        assertEquals(ImmutableList.of("First", "Second", "Third"), lines.get());
    }

    @Test
    public void testForEachLfHandling() {
        LineDecoder decoder = new LineDecoder(StandardCharsets.UTF_8, LineDecoder.Mode.LF);
        assertTrue(decoder.forEach(toByteBuffer("First\r\nSecond\nThird\r\n"), true, lines));
        assertEquals(ImmutableList.of("First\r", "Second", "Third\r"), lines.get());
    }

    @Test
    public void testForEachNullHandling() {
        LineDecoder decoder = new LineDecoder(StandardCharsets.UTF_8, LineDecoder.Mode.NULL);
        assertTrue(decoder.forEach(toByteBuffer("First\u0000Second\u0000Third\u0000"), true, lines));
        assertEquals(ImmutableList.of("First", "Second", "Third"), lines.get());
    }

    @Test
    public void testForEachSpanningBuffers() {
        LineDecoder decoder = new LineDecoder(StandardCharsets.UTF_8);
        assertFalse(decoder.forEach(toByteBuffer("This is the first part of a line"), false, lines));
        assertTrue(decoder.forEach(toByteBuffer(" and this is the second"), true, lines));
        assertEquals(ImmutableList.of("This is the first part of a line and this is the second"), lines.get());
    }

    @Test
    public void testForEachSplitMultibyte() {
        String cjk = "\u5718\u96c6";

        ByteBuffer buffer = toByteBuffer(cjk);
        //Simulate a buffer with a split multibyte character
        buffer.limit(2); //Only allow the first 2 characters

        LineDecoder decoder = new LineDecoder(StandardCharsets.UTF_8);
        assertFalse(decoder.forEach(buffer, false, lines));
        assertEquals("The partial character should not have been consumed", 0, buffer.position());

        //In NuProcess, any bytes remaining in the stdout buffer when onStdout returns are still in
        //the buffer on the next onStdout call. Reset the limit to the buffer's capacity to simulate
        //the second call having the first call's 2 bytes, plus more
        buffer.limit(buffer.capacity());
        assertTrue(decoder.forEach(buffer, true, lines));

        assertEquals(ImmutableList.of(cjk), lines.get());
    }

    @Test
    public void testForEachStopsEarly() {
        lines = new ListLineSink(3);

        LineDecoder decoder = new LineDecoder(StandardCharsets.UTF_8);
        assertTrue(decoder.forEach(toByteBuffer("1\n2\n3\n4\n5\n"), false, lines));
        assertEquals(ImmutableList.of("1", "2", "3"), lines.get());
    }

    @Test
    public void testForEachTruncates() {
        LineSink sink = mock(LineSink.class);
        doReturn(true).when(sink).accept(anyString(), anyBoolean());

        LineDecoder decoder = new LineDecoder(StandardCharsets.UTF_8, LineDecoder.Mode.LF, 10);
        assertTrue(decoder.forEach(toByteBuffer("This line will be truncated\n" +
                                                "This won't\n" +
                                                "This line will be truncated\n" +
                                                "This won't\n"), true, sink));

        InOrder ordered = inOrder(sink);
        ordered.verify(sink).accept(eq("This line "), eq(true));
        ordered.verify(sink).accept(eq("This won't"), eq(false));
        ordered.verify(sink).accept(eq("This line "), eq(true));
        ordered.verify(sink).accept(eq("This won't"), eq(false));
        ordered.verifyNoMoreInteractions();
    }

    @Test
    public void testForEachTruncationHandlesSplitCrLf() {
        LineSink sink = mock(LineSink.class);
        doReturn(true).when(sink).accept(anyString(), anyBoolean());

        LineDecoder decoder = new LineDecoder(StandardCharsets.UTF_8, LineDecoder.Mode.CRLF_OR_LF, 13);
        assertFalse(decoder.forEach(toByteBuffer("Not truncated\r"), false, sink));
        assertTrue(decoder.forEach(toByteBuffer("\n"), true, sink));

        verify(sink).accept(eq("Not truncated"), eq(false));
    }

    @Test
    public void testForEachTruncationHandlesTrailingCr() {
        LineSink sink = mock(LineSink.class);
        doReturn(true).when(sink).accept(anyString(), anyBoolean());

        LineDecoder decoder = new LineDecoder(StandardCharsets.UTF_8, LineDecoder.Mode.CRLF_OR_LF, 10);
        assertTrue(decoder.forEach(toByteBuffer("This line will be truncated\r\n" +
                                                "This won't\r\n" +
                                                "This line will be truncated\r\n" +
                                                "This won't\r\n"), true, sink));

        InOrder ordered = inOrder(sink);
        ordered.verify(sink).accept(eq("This line "), eq(true));
        ordered.verify(sink).accept(eq("This won't"), eq(false));
        ordered.verify(sink).accept(eq("This line "), eq(true));
        ordered.verify(sink).accept(eq("This won't"), eq(false));
        ordered.verifyNoMoreInteractions();
    }

    @Test
    public void testForEachWithNoTrailingEol() {
        LineDecoder decoder = new LineDecoder(StandardCharsets.UTF_8);
        assertTrue(decoder.forEach(toByteBuffer("First\r\nSecond\r\nThird"), true, lines));
        assertEquals(ImmutableList.of("First", "Second", "Third"), lines.get());
    }

    @Test
    public void testForEachWithPartialLine() {
        LineDecoder decoder = new LineDecoder(StandardCharsets.UTF_8);
        assertFalse(decoder.forEach(toByteBuffer("Full line\nPartial"), false, lines));
        assertEquals(ImmutableList.of("Full line"), lines.get());

        assertTrue(decoder.forEach(toByteBuffer(" line\n"), true, lines));
        assertEquals(ImmutableList.of("Full line", "Partial line"), lines.get());
    }

    @Test
    public void testForEachWithTrailingCr() {
        LineDecoder decoder = new LineDecoder(StandardCharsets.UTF_8);
        assertTrue(decoder.forEach(toByteBuffer("First\r\nSecond\r"), true, lines));
        assertEquals(ImmutableList.of("First", "Second"), lines.get());
    }

    private static ByteBuffer toByteBuffer(String value) {
        return ByteBuffer.wrap(toBytes(value));
    }

    private static byte[] toBytes(String value) {
        return value.getBytes(StandardCharsets.UTF_8);
    }

    private static class ListLineSink implements LineSink {

        private final List<String> lines;
        private final int maxLines;

        ListLineSink() {
            this(-1);
        }

        ListLineSink(int maxLines) {
            this.maxLines = maxLines;

            lines = new ArrayList<>();
        }

        @Override
        public boolean accept(@Nonnull String line, boolean truncated) {
            lines.add(line);

            return maxLines == -1 || lines.size() < maxLines;
        }

        List<String> get() {
            return lines;
        }
    }
}