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

import org.junit.Before;
import org.junit.Test;
import org.springframework.core.io.ClassPathResource;

import javax.annotation.Nonnull;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStreamWriter;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.List;
import java.util.zip.GZIPOutputStream;

import static java.nio.charset.StandardCharsets.UTF_8;
import static org.junit.Assert.*;

public class GitPacketStreamTest {

    private GitPacketStream stream;
    private CapturingListener listener;

    @Before
    public void setUp() {
        stream = new GitPacketStream();
        listener = new CapturingListener();

        stream.addListener(listener);
    }

    @Test
    public void testAbort() {
        stream.append("00000008abc".getBytes(UTF_8));
        assertPackets("0000"); // 0008abc in buffer
        assertFalse(listener.aborted);

        stream.abort();
        stream.append("d0000".getBytes(UTF_8));
        assertTrue(listener.aborted);
    }

    @Test
    public void testAppendByteBufferWhenByteBufferSizeExceedsInternalBufferSize() {
        stream = new GitPacketStream(true, 8);
        stream.addListener(listener);

        ByteBuffer buffer = ByteBuffer.wrap("0006aa00000008abcd".getBytes(UTF_8));
        stream.append(buffer);

        assertPackets("0006aa", "0000", "0008abcd");
        assertFalse(listener.aborted);
        assertFalse(stream.isInPackSection());
    }

    @Test
    public void testAppendByteBufferWhenInternalBufferSizeExceedsByteBufferSize() {
        stream = new GitPacketStream(true, 1024);
        stream.addListener(listener);

        ByteBuffer buffer = ByteBuffer.wrap("0006aa00000008abcd".getBytes(UTF_8));
        stream.append(buffer);

        assertPackets("0006aa", "0000", "0008abcd");
        assertFalse(listener.aborted);
        assertFalse(stream.isInPackSection());
    }

    @Test
    public void testAppendFlushPacket() {
        byte[] bytes = "0006aa00000008abcd".getBytes(UTF_8);
        stream.append(bytes);

        assertPackets("0006aa", "0000", "0008abcd");
        assertFalse(stream.isInPackSection());
    }

    @Test(expected = IllegalArgumentException.class)
    public void testAppendNonPacketData() {
        byte[] bytes = "not a packet stream".getBytes(UTF_8);
        try {
            stream.append(bytes);
        } finally {
            assertTrue(listener.aborted);
        }
    }

    @Test
    public void testAppendWithOffsetAndLength() {
        byte[] bytes = "ignored0005a0005bignored".getBytes(UTF_8);
        int prefixLength = "ignored".length();

        // read the first packet + 1 byte of the next packet
        stream.append(bytes, prefixLength, 6);
        assertPackets("0005a");

        stream.append(bytes, prefixLength + 6, 4);
        assertPackets("0005a", "0005b");
        assertFalse(listener.aborted);
    }

    @Test
    public void testGzipDetection() throws IOException {
        ByteArrayOutputStream bytes = new ByteArrayOutputStream();
        try (OutputStreamWriter writer = new OutputStreamWriter(new GZIPOutputStream(bytes), UTF_8)) {
            //The packet is valid, but because it's gzip-compressed the packet stream shouldn't
            //try to parse it
            writer.write("0089want 090ac3c4f4afc608fab0b21c3341da96333ec587 multi_ack_detailed " +
                    "no-done side-band-64k thin-pack ofs-delta agent=git/2.3.7.windows.1\n");
        }

        byte[] data = bytes.toByteArray();
        stream.append(data);
        assertPackets();
        assertTrue(listener.aborted);
        assertTrue(stream.isAborted());
        assertFalse(stream.isInPackSection());
    }

    @Test(expected = IllegalArgumentException.class)
    public void testNonPacketStream() {
        byte[] data = "This is not a packet stream, so it should not be possible to parse this as git packets".getBytes(UTF_8);
        try {
            stream.append("0008aaaa".getBytes(UTF_8)); // start with a valid packet
            stream.append(data); // now an invalid packet
        } finally {
            assertPackets("0008aaaa");
            assertTrue(listener.aborted);
        }
    }

    @Test
    public void testRealResponseV1() throws IOException {
        byte[] buffer = new byte[2048];
        try (InputStream in = new ClassPathResource("git-upload-pack.v1.response", GitPacketStreamTest.class)
                .getInputStream()) {
            int read;
            while ((read = in.read(buffer)) != -1) {
                stream.append(buffer, 0, read);
            }
        }

        assertPackets(
                "0100f67e0909450521d4c95a4d03292913bfa4002f73 HEAD\0multi_ack thin-pack side-band side-band-64k " +
                        "ofs-delta shallow deepen-since deepen-not deepen-relative no-progress include-tag " +
                        "multi_ack_detailed symref=HEAD:refs/heads/master agent=git/github-g329139f472a1\n",
                "003ff67e0909450521d4c95a4d03292913bfa4002f73 refs/heads/master\n",
                "0000",
                "0008NAK\n",
                "0023\2Counting objects: 3115, done.\n");
        assertTrue(stream.isInPackSection());
        assertTrue(listener.aborted);
    }

    @Test
    public void testRealResponseV2() throws IOException {
        byte[] buffer = new byte[2048];
        try (InputStream in = new ClassPathResource("git-upload-pack.v2.response", GitPacketStreamTest.class)
                .getInputStream()) {
            int read;
            while ((read = in.read(buffer)) != -1) {
                stream.append(buffer, 0, read);
            }
        }

        assertPackets(
                "000eversion 2\n",
                "0019agent=git/2.18.0.rc2\n",
                "000cls-refs\n",
                "0012fetch=shallow\n",
                "0012server-option\n",
                "0000",
                "00527c397f7e5285f7d1658ca1979d30af2b5bf32bad HEAD symref-target:refs/heads/master\n",
                "003f7c397f7e5285f7d1658ca1979d30af2b5bf32bad refs/heads/master\n",
                "0000",
                "000dpackfile\n");
        assertTrue(stream.isInPackSection());
        assertTrue(listener.aborted);
    }

    @Test
    public void testRealResponseV2WhenAbortOnPackSectionIsFalse() throws IOException {
        byte[] buffer = new byte[2048];
        try (InputStream in = new ClassPathResource("git-upload-pack.v2.response", GitPacketStreamTest.class)
                .getInputStream()) {
            int read;
            stream = new GitPacketStream(false, 1024);
            stream.addListener(listener);
            while ((read = in.read(buffer)) != -1) {
                stream.append(buffer, 0, read);
            }
        }

        assertPackets(
                "000eversion 2\n",
                "0019agent=git/2.18.0.rc2\n",
                "000cls-refs\n",
                "0012fetch=shallow\n",
                "0012server-option\n",
                "0000",
                "00527c397f7e5285f7d1658ca1979d30af2b5bf32bad HEAD symref-target:refs/heads/master\n",
                "003f7c397f7e5285f7d1658ca1979d30af2b5bf32bad refs/heads/master\n",
                "0000",
                "000dpackfile\n",
                new byte[] {'0', '0', '1', '0', 1, 'P', 'A', 'C', 'K', 0, 0, 0, 2, 0, 0, 1},
                "0000");
        assertTrue(stream.isInPackSection());
        assertFalse(listener.aborted);
    }

    @Test
    public void testReceivePackRealResponse() {
        stream.append(("00968621398e3d405821832d0830fc53c560d427b9fb d98685844c5146e89b2eb1c734ad788a0d8501ca " +
                        "refs/heads/master\0 report-status side-band-64k agent=git/2.18.0\n0000PACK0000").getBytes(UTF_8));

        assertPackets(
                "00968621398e3d405821832d0830fc53c560d427b9fb d98685844c5146e89b2eb1c734ad788a0d8501ca " +
                        "refs/heads/master\u0000 report-status side-band-64k agent=git/2.18.0\n",
                "0000");
        assertTrue(listener.aborted);
        assertTrue(stream.isInPackSection());
    }

    @Test
    public void testPartialPackets() {
        stream.append("00".getBytes(UTF_8));
        assertPackets(); // 00 in buffer

        stream.append("08a".getBytes(UTF_8));
        assertPackets(); // 0008a in buffer

        stream.append("bcd00".getBytes(UTF_8));
        assertPackets("0008abcd"); // 00 in buffer

        stream.append("00".getBytes(UTF_8));
        assertPackets("0008abcd", "0000");

        stream.append("000".getBytes(UTF_8));
        assertPackets("0008abcd", "0000"); // 000 in buffer

        stream.append("5a".getBytes(UTF_8)); // 0005a
        assertPackets("0008abcd", "0000", "0005a");
        assertFalse(listener.aborted);
    }

    private void assertPackets(Object... expectedPackets) {
        assertEquals("Mismatch in number of packets", expectedPackets.length, listener.packets.size());
        for (int i = 0; i < expectedPackets.length; ++i) {
            byte[] expected = expectedPackets[i] instanceof byte[] ? (byte[]) expectedPackets[i] :
                    expectedPackets[i].toString().getBytes(UTF_8);
            assertArrayEquals("packet " + i, expected, listener.packets.get(i));
        }
    }

    private static class CapturingListener implements GitPacketListener {

        private final List<byte[]> packets = new ArrayList<>();

        private boolean aborted;

        @Override
        public void onAborted() {
            aborted = true;
        }

        @Override
        public void onPacket(@Nonnull byte[] packet) {
            packets.add(packet);
        }
    }
}
