package invalid.lena.scrcpy;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;

import org.junit.Test;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;

public class VideoStreamTest {

    // ---- recording sink ----

    private static final class RecordingFrames implements VideoFrames {
        static final class Cfg  { final int fourcc, w, h; Cfg(int f,int w,int h){this.fourcc=f;this.w=w;this.h=h;} }
        static final class Feed { final byte[] data; final long ptsUs; final boolean isCfg;
                                  Feed(byte[] d,long p,boolean c){data=d;ptsUs=p;isCfg=c;} }

        final List<Cfg>  configs    = new ArrayList<>();
        final List<Cfg>  reconfigs  = new ArrayList<>();
        final List<Feed> feeds      = new ArrayList<>();
        int releases = 0;

        @Override public void configure(int f, int w, int h)    { configs.add(new Cfg(f, w, h)); }
        @Override public void reconfigure(int f, int w, int h)  { reconfigs.add(new Cfg(f, w, h)); }
        @Override public void feed(byte[] d, long pts, boolean c){ feeds.add(new Feed(d, pts, c)); }
        @Override public void release() { releases++; }
    }

    // ---- byte builders ----

    private static byte[] fourcc(int v) throws IOException {
        ByteArrayOutputStream bos = new ByteArrayOutputStream();
        new DataOutputStream(bos).writeInt(v);
        return bos.toByteArray();
    }

    private static byte[] sessionMeta(int w, int h, boolean clientResize) throws IOException {
        ByteArrayOutputStream bos = new ByteArrayOutputStream();
        DataOutputStream out = new DataOutputStream(bos);
        out.writeInt(0x80000000 | (clientResize ? 1 : 0));
        out.writeInt(w);
        out.writeInt(h);
        return bos.toByteArray();
    }

    private static byte[] frame(long ptsAndFlags, byte[] payload) throws IOException {
        ByteArrayOutputStream bos = new ByteArrayOutputStream();
        DataOutputStream out = new DataOutputStream(bos);
        out.writeLong(ptsAndFlags);
        out.writeInt(payload.length);
        out.write(payload);
        return bos.toByteArray();
    }

    private static byte[] cat(byte[]... parts) throws IOException {
        ByteArrayOutputStream bos = new ByteArrayOutputStream();
        for (byte[] p : parts) bos.write(p);
        return bos.toByteArray();
    }

    private static void runUntilEof(VideoStream s) {
        s.run(); // returns when the InputStream hits EOF (IOException)
    }

    // ---- tests ----

    @Test
    public void happyPath_configureCsdKeyframeDelta() throws Exception {
        byte[] csd      = new byte[]{0x67, 0x42, (byte)0xe0, 0x1e}; // bogus SPS-ish
        byte[] keyframe = new byte[]{1, 2, 3, 4, 5};
        byte[] delta    = new byte[]{6, 7, 8};

        long PTS_CFG  = 1L << 62;             // CONFIG
        long PTS_KEY  = (1L << 61) | 1_000_000L; // KEYFRAME, pts=1s
        long PTS_DELT = 2_000_000L;           // pts=2s

        byte[] bytes = cat(
                fourcc(Wire.CODEC_H264),
                sessionMeta(1080, 2400, false),
                frame(PTS_CFG,  csd),
                frame(PTS_KEY,  keyframe),
                frame(PTS_DELT, delta));

        RecordingFrames sink = new RecordingFrames();
        VideoStream s = new VideoStream(new ByteArrayInputStream(bytes), sink,
                (w, h) -> {});
        runUntilEof(s);

        assertEquals(1, sink.configs.size());
        assertEquals(Wire.CODEC_H264, sink.configs.get(0).fourcc);
        assertEquals(1080, sink.configs.get(0).w);
        assertEquals(2400, sink.configs.get(0).h);

        assertEquals(3, sink.feeds.size());
        assertTrue("first is config", sink.feeds.get(0).isCfg);
        assertEquals(0L, sink.feeds.get(0).ptsUs); // CONFIG strips pts to 0
        assertEquals(4,  sink.feeds.get(0).data.length);

        // keyframe: keyframe flag is stripped from ptsUs
        assertEquals(1_000_000L, sink.feeds.get(1).ptsUs);
        assertEquals(5,          sink.feeds.get(1).data.length);

        assertEquals(2_000_000L, sink.feeds.get(2).ptsUs);
    }

    @Test
    public void sessionMetaResizeTriggersReconfigure() throws Exception {
        byte[] bytes = cat(
                fourcc(Wire.CODEC_H264),
                sessionMeta(1080, 2400, false),
                frame(1L << 62, new byte[]{1}),     // config
                sessionMeta(720, 1600, true),       // resize
                frame(1L << 61, new byte[]{2}));    // keyframe at new size

        RecordingFrames sink = new RecordingFrames();
        new VideoStream(new ByteArrayInputStream(bytes), sink, null).run();

        assertEquals(1, sink.configs.size());
        assertEquals(1, sink.reconfigs.size());
        assertEquals(720,  sink.reconfigs.get(0).w);
        assertEquals(1600, sink.reconfigs.get(0).h);
    }

    @Test
    public void sizeListenerFiresOnEachSessionMeta() throws Exception {
        byte[] bytes = cat(
                fourcc(Wire.CODEC_H264),
                sessionMeta(1080, 2400, false),
                frame(1L << 62, new byte[]{1}),
                sessionMeta(720, 1600, false),
                frame(1L << 61, new byte[]{2}));

        List<int[]> sizes = new ArrayList<>();
        RecordingFrames sink = new RecordingFrames();
        new VideoStream(new ByteArrayInputStream(bytes), sink,
                (w, h) -> sizes.add(new int[]{w, h})).run();

        assertEquals(2, sizes.size());
        assertEquals(1080, sizes.get(0)[0]);
        assertEquals(720,  sizes.get(1)[0]);
    }

    @Test
    public void disabledCodecExits() throws Exception {
        byte[] bytes = fourcc(0);
        RecordingFrames sink = new RecordingFrames();
        new VideoStream(new ByteArrayInputStream(bytes), sink, null).run();
        assertEquals(0, sink.configs.size());
        assertEquals(0, sink.feeds.size());
    }

    @Test
    public void frameBeforeSessionMetaIsRejected() throws Exception {
        // A frame header arriving without a preceding session-meta should
        // raise - the stream is unconfigured.
        byte[] bytes = cat(
                fourcc(Wire.CODEC_H264),
                frame(1L << 62, new byte[]{1, 2, 3}));   // config-flag set, but no meta first
        RecordingFrames sink = new RecordingFrames();
        new VideoStream(new ByteArrayInputStream(bytes), sink, null).run();
        // The loop should have aborted without feeding anything.
        assertEquals(0, sink.feeds.size());
        assertEquals(0, sink.configs.size());
    }

    @Test
    public void recorderTapSeesMetaAndFramesWithFlags() throws Exception {
        byte[] csd      = new byte[]{0x67, 0x42, (byte)0xe0, 0x1e};
        byte[] keyframe = new byte[]{10, 11, 12};
        byte[] delta    = new byte[]{20, 21};
        long PTS_CFG  = 1L << 62;
        long PTS_KEY  = (1L << 61) | 1_000_000L;
        long PTS_DELT = 2_000_000L;

        byte[] bytes = cat(
                fourcc(Wire.CODEC_H264),
                sessionMeta(800, 600, false),
                frame(PTS_CFG,  csd),
                frame(PTS_KEY,  keyframe),
                frame(PTS_DELT, delta));

        RecordingFrames sink = new RecordingFrames();
        RecordingRecorder rec = new RecordingRecorder();
        VideoStream s = new VideoStream(new ByteArrayInputStream(bytes), sink, null);
        s.setRecorder(rec);
        s.run();

        assertEquals(1, rec.metas.size());
        assertEquals(800, rec.metas.get(0)[1]);
        assertEquals(600, rec.metas.get(0)[2]);

        assertEquals(3, rec.frames.size());
        assertTrue("cfg frame",  rec.frames.get(0).isConfig);
        assertTrue("key frame",  rec.frames.get(1).isKeyframe);
        assertTrue("delta is not config or key",
                !rec.frames.get(2).isConfig && !rec.frames.get(2).isKeyframe);
        assertEquals(csd.length,      rec.frames.get(0).data.length);
        assertEquals(keyframe.length, rec.frames.get(1).data.length);
        assertEquals(delta.length,    rec.frames.get(2).data.length);
    }

    private static final class RecordingRecorder implements VideoRecorder {
        static final class FrameRec {
            final byte[] data; final long ptsUs;
            final boolean isConfig, isKeyframe;
            FrameRec(byte[] d, long p, boolean c, boolean k) {
                data = d; ptsUs = p; isConfig = c; isKeyframe = k;
            }
        }
        final List<int[]>     metas  = new ArrayList<>();
        final List<FrameRec>  frames = new ArrayList<>();
        int closes = 0;
        @Override public void onMeta(int f, int w, int h) { metas.add(new int[]{f, w, h}); }
        @Override public void onFrame(byte[] d, long p, boolean c, boolean k) {
            frames.add(new FrameRec(d, p, c, k));
        }
        @Override public void close() { closes++; }
    }

    @Test
    public void payloadIntegrity() throws Exception {
        byte[] payload = new byte[1024];
        for (int i = 0; i < payload.length; i++) payload[i] = (byte) (i & 0xff);
        byte[] bytes = cat(
                fourcc(Wire.CODEC_H264),
                sessionMeta(640, 480, false),
                frame(1L << 62, payload));

        RecordingFrames sink = new RecordingFrames();
        new VideoStream(new ByteArrayInputStream(bytes), sink, null).run();
        assertNotNull(sink.feeds.isEmpty() ? null : sink.feeds.get(0));
        byte[] got = sink.feeds.get(0).data;
        assertEquals(payload.length, got.length);
        for (int i = 0; i < payload.length; i++) {
            assertEquals("byte " + i, payload[i], got[i]);
        }
    }
}
