package com.lingniu.ingest.protocol.jsatl12.inbound;

import com.lingniu.ingest.api.ProtocolId;
import com.lingniu.ingest.api.pipeline.RawFrame;
import com.lingniu.ingest.identity.InMemoryVehicleIdentityService;
import com.lingniu.ingest.identity.VehicleIdentityBinding;
import com.lingniu.ingest.protocol.jsatl12.codec.Jsatl12RawFrame;
import com.lingniu.ingest.sink.archive.ArchiveStore;
import io.netty.channel.embedded.EmbeddedChannel;
import org.junit.jupiter.api.Test;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;

import static org.assertj.core.api.Assertions.assertThat;

class Jsatl12ChannelHandlerTest {

    @Test
    void dataChunkArchivesPayloadAndDispatchesQueryableArchiveReference() {
        RecordingArchiveStore archive = new RecordingArchiveStore();
        List<RawFrame> dispatched = new ArrayList<>();
        Jsatl12ChannelHandler handler = new Jsatl12ChannelHandler(archive, null, dispatched::add);
        EmbeddedChannel channel = new EmbeddedChannel(handler);

        channel.writeInbound(new Jsatl12RawFrame(
                Jsatl12RawFrame.Type.DATA_CHUNK,
                dataPacket("ADAS_001.jpg", 0, new byte[]{0x01, 0x02, 0x03})));

        assertThat(archive.lastChunk).containsExactly(0x01, 0x02, 0x03);
        assertThat(dispatched).hasSize(1);
        RawFrame frame = dispatched.getFirst();
        assertThat(frame.protocolId()).isEqualTo(ProtocolId.JSATL12);
        assertThat(frame.command()).isEqualTo(Jsatl12MessageId.DATA_CHUNK_ARCHIVED);
        assertThat(frame.payload()).isInstanceOf(Jsatl12ArchivedChunk.class);
        Jsatl12ArchivedChunk chunk = (Jsatl12ArchivedChunk) frame.payload();
        assertThat(chunk.archiveRef()).isEqualTo("memory://" + archive.lastKey);
        assertThat(chunk.sizeBytes()).isEqualTo(3);
        assertThat(chunk.peer()).isEqualTo("unknown");
        assertThat(frame.sourceMeta())
                .containsEntry("source", "jsatl12-data-chunk")
                .containsEntry("fileName", "ADAS_001.jpg")
                .containsEntry("fileOffset", "0")
                .containsEntry("vin", "unknown")
                .containsEntry("identityResolved", "false")
                .containsEntry("identitySource", "UNKNOWN");
    }

    @Test
    void dataChunkUsesAttachmentListPhoneForVehicleIdentity() {
        RecordingArchiveStore archive = new RecordingArchiveStore();
        Jsatl12AttachmentTracker tracker = new Jsatl12AttachmentTracker();
        String phone = "123456789012";
        tracker.registerAttachmentList(phone, t1210Body("ADAS_001.jpg", 3));
        InMemoryVehicleIdentityService identities = new InMemoryVehicleIdentityService();
        identities.bind(new VehicleIdentityBinding(
                ProtocolId.JT808,
                "LNVIN00000001212",
                phone,
                "",
                ""));
        List<RawFrame> dispatched = new ArrayList<>();
        Jsatl12ChannelHandler handler = new Jsatl12ChannelHandler(
                archive,
                null,
                tracker,
                identities,
                dispatched::add);
        EmbeddedChannel channel = new EmbeddedChannel(handler);

        channel.writeInbound(new Jsatl12RawFrame(
                Jsatl12RawFrame.Type.DATA_CHUNK,
                dataPacket("ADAS_001.jpg", 0, new byte[]{0x01, 0x02, 0x03})));

        assertThat(dispatched).singleElement().satisfies(frame -> {
            assertThat(frame.sourceMeta())
                    .containsEntry("phone", phone)
                    .containsEntry("vin", "LNVIN00000001212")
                    .containsEntry("identityResolved", "true")
                    .containsEntry("identitySource", "BOUND_PHONE");
            assertThat(frame.payload()).isInstanceOf(Jsatl12ArchivedChunk.class);
            Jsatl12ArchivedChunk chunk = (Jsatl12ArchivedChunk) frame.payload();
            assertThat(chunk.phone()).isEqualTo(phone);
            assertThat(chunk.vin()).isEqualTo("LNVIN00000001212");
            assertThat(chunk.identityResolved()).isTrue();
            assertThat(chunk.identitySource()).isEqualTo("BOUND_PHONE");
        });
    }

    @Test
    void dataChunkStillArchivesWhenIdentityResolverFails() {
        RecordingArchiveStore archive = new RecordingArchiveStore();
        Jsatl12AttachmentTracker tracker = new Jsatl12AttachmentTracker();
        String phone = "123456789012";
        tracker.registerAttachmentList(phone, t1210Body("ADAS_001.jpg", 3));
        List<RawFrame> dispatched = new ArrayList<>();
        Jsatl12ChannelHandler handler = new Jsatl12ChannelHandler(
                archive,
                null,
                tracker,
                lookup -> {
                    throw new IllegalStateException("identity backend unavailable");
                },
                dispatched::add);
        EmbeddedChannel channel = new EmbeddedChannel(handler);
        byte[] frameBytes = dataPacket("ADAS_001.jpg", 0, new byte[]{0x01, 0x02, 0x03});

        channel.writeInbound(new Jsatl12RawFrame(Jsatl12RawFrame.Type.DATA_CHUNK, frameBytes));

        assertThat(archive.lastChunk).containsExactly(0x01, 0x02, 0x03);
        assertThat(dispatched).singleElement().satisfies(frame -> {
            assertThat(frame.protocolId()).isEqualTo(ProtocolId.JSATL12);
            assertThat(frame.command()).isEqualTo(Jsatl12MessageId.DATA_CHUNK_ARCHIVED);
            assertThat(frame.rawBytes()).isNull();
            assertThat(frame.payload()).isInstanceOf(Jsatl12ArchivedChunk.class);
            Jsatl12ArchivedChunk chunk = (Jsatl12ArchivedChunk) frame.payload();
            assertThat(chunk.phone()).isEqualTo(phone);
            assertThat(chunk.vin()).isEqualTo("unknown");
            assertThat(chunk.identityResolved()).isFalse();
            assertThat(chunk.identitySource()).isEqualTo("UNKNOWN");
            assertThat(frame.sourceMeta())
                    .containsEntry("phone", phone)
                    .containsEntry("vin", "unknown")
                    .containsEntry("identityResolved", "false")
                    .containsEntry("identitySource", "UNKNOWN")
                    .containsEntry("identityError", "true")
                    .containsEntry("identityErrorMessage", "identity backend unavailable")
                    .containsEntry("archiveRef", "memory://" + archive.lastKey);
        });
    }

    @Test
    void dataChunkArchiveFailureDispatchesPassthroughCandidateWithRawPayload() {
        FailingArchiveStore archive = new FailingArchiveStore();
        List<RawFrame> dispatched = new ArrayList<>();
        Jsatl12ChannelHandler handler = new Jsatl12ChannelHandler(archive, null, dispatched::add);
        EmbeddedChannel channel = new EmbeddedChannel(handler);
        byte[] frameBytes = dataPacket("ADAS_001.jpg", 0, new byte[]{0x01, 0x02, 0x03});

        channel.writeInbound(new Jsatl12RawFrame(Jsatl12RawFrame.Type.DATA_CHUNK, frameBytes));

        assertThat(dispatched).singleElement().satisfies(frame -> {
            assertThat(frame.protocolId()).isEqualTo(ProtocolId.JSATL12);
            assertThat(frame.command()).isEqualTo(Jsatl12MessageId.DATA_CHUNK_ARCHIVE_FAILED);
            assertThat(frame.rawBytes()).containsExactly(frameBytes);
            assertThat(frame.payload()).isInstanceOf(Jsatl12ArchiveFailure.class);
            Jsatl12ArchiveFailure failure = (Jsatl12ArchiveFailure) frame.payload();
            assertThat(failure.rawBytes()).containsExactly(frameBytes);
            assertThat(failure.sizeBytes()).isEqualTo(3);
            assertThat(failure.peer()).isEqualTo("unknown");
            assertThat(failure.errorMessage()).contains("archive backend unavailable");
            assertThat(frame.sourceMeta())
                    .containsEntry("source", "jsatl12-data-chunk")
                    .containsEntry("archiveError", "true")
                    .containsEntry("chunkSizeBytes", "3")
                    .containsEntry("vin", "unknown")
                    .containsEntry("identityResolved", "false")
                    .containsEntry("identitySource", "UNKNOWN");
        });
    }

    @Test
    void malformedFrameDispatchesPassthroughCandidateWithRawPayload() {
        RecordingArchiveStore archive = new RecordingArchiveStore();
        List<RawFrame> dispatched = new ArrayList<>();
        Jsatl12ChannelHandler handler = new Jsatl12ChannelHandler(archive, null, dispatched::add);
        EmbeddedChannel channel = new EmbeddedChannel(handler);
        byte[] raw = new byte[]{
                0x30, 0x31, 0x63, 0x64,
                0x00, 0x00, 0x00, 0x03,
                0x01, 0x02, 0x03
        };

        channel.writeInbound(new Jsatl12RawFrame(Jsatl12RawFrame.Type.MALFORMED, raw));

        assertThat(dispatched).singleElement().satisfies(frame -> {
            assertThat(frame.protocolId()).isEqualTo(ProtocolId.JSATL12);
            assertThat(frame.command()).isEqualTo(Jsatl12MessageId.MALFORMED_FRAME);
            assertThat(frame.rawBytes()).containsExactly(raw);
            assertThat(frame.payload()).isInstanceOf(Jsatl12MalformedFrame.class);
            Jsatl12MalformedFrame malformed = (Jsatl12MalformedFrame) frame.payload();
            assertThat(malformed.rawBytes()).containsExactly(raw);
            assertThat(malformed.peer()).isEqualTo("unknown");
            assertThat(malformed.errorMessage()).contains("malformed jsatl12 frame");
            assertThat(frame.sourceMeta())
                    .containsEntry("source", "jsatl12-frame")
                    .containsEntry("frameError", "true")
                    .containsEntry("frameSizeBytes", Integer.toString(raw.length))
                    .containsEntry("vin", "unknown")
                    .containsEntry("identityResolved", "false")
                    .containsEntry("identitySource", "UNKNOWN");
        });
    }

    private static byte[] dataPacket(String name, int offset, byte[] data) {
        byte[] out = new byte[62 + data.length];
        out[0] = 0x30;
        out[1] = 0x31;
        out[2] = 0x63;
        out[3] = 0x64;
        byte[] nameBytes = name.getBytes(StandardCharsets.US_ASCII);
        System.arraycopy(nameBytes, 0, out, 4, nameBytes.length);
        writeDword(out, 54, offset);
        writeDword(out, 58, data.length);
        System.arraycopy(data, 0, out, 62, data.length);
        return out;
    }

    private static byte[] t1210Body(String fileName, long size) {
        byte[] name = fileName.getBytes(StandardCharsets.US_ASCII);
        byte[] out = new byte[57 + 1 + name.length + 4];
        putAscii(out, 0, 7, "DEV0001");
        putAscii(out, 7, 7, "DEV0001");
        putAscii(out, 23, 32, "ALARM-NO-001");
        out[55] = 0;
        out[56] = 1;
        out[57] = (byte) name.length;
        System.arraycopy(name, 0, out, 58, name.length);
        writeDword(out, 58 + name.length, size);
        return out;
    }

    private static void putAscii(byte[] out, int offset, int len, String value) {
        byte[] bytes = value.getBytes(StandardCharsets.US_ASCII);
        System.arraycopy(bytes, 0, out, offset, Math.min(bytes.length, len));
    }

    private static void writeDword(byte[] out, int offset, long value) {
        out[offset] = (byte) ((value >>> 24) & 0xFF);
        out[offset + 1] = (byte) ((value >>> 16) & 0xFF);
        out[offset + 2] = (byte) ((value >>> 8) & 0xFF);
        out[offset + 3] = (byte) (value & 0xFF);
    }

    private static final class RecordingArchiveStore implements ArchiveStore {
        private String lastKey;
        private byte[] lastChunk;

        @Override
        public String put(String key, InputStream data, long length) {
            throw new UnsupportedOperationException();
        }

        @Override
        public String append(String key, byte[] chunk) {
            this.lastKey = key;
            this.lastChunk = chunk;
            return "memory://" + key;
        }

        @Override
        public InputStream get(String key) {
            return new ByteArrayInputStream(new byte[0]);
        }

        @Override
        public boolean exists(String key) {
            return false;
        }

        @Override
        public long size(String key) throws IOException {
            return lastChunk == null ? 0 : lastChunk.length;
        }
    }

    private static final class FailingArchiveStore implements ArchiveStore {
        @Override
        public String put(String key, InputStream data, long length) throws IOException {
            throw new IOException("archive backend unavailable");
        }

        @Override
        public String append(String key, byte[] chunk) throws IOException {
            throw new IOException("archive backend unavailable");
        }

        @Override
        public InputStream get(String key) {
            return new ByteArrayInputStream(new byte[0]);
        }

        @Override
        public boolean exists(String key) {
            return false;
        }

        @Override
        public long size(String key) throws IOException {
            throw new IOException("archive backend unavailable");
        }
    }
}
