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

import com.lingniu.ingest.api.ProtocolId;
import com.lingniu.ingest.api.pipeline.RawFrame;
import com.lingniu.ingest.codec.BccChecksum;
import com.lingniu.ingest.codec.BcdCodec;
import com.lingniu.ingest.identity.InMemoryVehicleIdentityService;
import com.lingniu.ingest.identity.VehicleIdentityBinding;
import com.lingniu.ingest.protocol.jt808.codec.BodyParserRegistry;
import com.lingniu.ingest.protocol.jt808.codec.Jt808Escape;
import com.lingniu.ingest.protocol.jt808.codec.Jt808FrameEncoder;
import com.lingniu.ingest.protocol.jt808.codec.Jt808MessageDecoder;
import com.lingniu.ingest.protocol.jt808.codec.parser.HeartbeatBodyParser;
import com.lingniu.ingest.protocol.jt808.codec.parser.PassthroughBodyParser;
import com.lingniu.ingest.protocol.jt808.model.Jt808Body;
import com.lingniu.ingest.protocol.jt808.model.Jt808MessageId;
import org.junit.jupiter.api.Test;

import java.io.ByteArrayOutputStream;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;

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

class Jsatl12JtMessageBridgeTest {

    @Test
    void decodesJtMessageAndDispatchesAsJt808RawFrame() {
        List<RawFrame> dispatched = new ArrayList<>();
        InMemoryVehicleIdentityService identities = new InMemoryVehicleIdentityService();
        identities.bind(new VehicleIdentityBinding(
                ProtocolId.JT808,
                "LNVIN00000001212",
                "123456789012",
                "",
                ""));
        Jsatl12JtMessageBridge bridge = new Jsatl12JtMessageBridge(
                new Jt808MessageDecoder(new BodyParserRegistry(List.of(new HeartbeatBodyParser()))),
                identities,
                dispatched::add);

        bridge.dispatch(buildFrame(Jt808MessageId.TERMINAL_HEARTBEAT, "123456789012", 3), "127.0.0.1:7612");

        assertThat(dispatched).hasSize(1);
        RawFrame frame = dispatched.get(0);
        assertThat(frame.protocolId()).isEqualTo(ProtocolId.JT808);
        assertThat(frame.command()).isEqualTo(Jt808MessageId.TERMINAL_HEARTBEAT);
        assertThat(frame.sourceMeta())
                .containsEntry("vin", "LNVIN00000001212")
                .containsEntry("phone", "123456789012")
                .containsEntry("identityResolved", "true")
                .containsEntry("identitySource", "BOUND_PHONE");
        assertThat(frame.payload()).isInstanceOf(com.lingniu.ingest.protocol.jt808.model.Jt808Message.class);
        assertThat(((com.lingniu.ingest.protocol.jt808.model.Jt808Message) frame.payload()).body())
                .isInstanceOf(Jt808Body.Heartbeat.class);
    }

    @Test
    void bridgeUsesExtendedJt808Parsers() {
        List<RawFrame> dispatched = new ArrayList<>();
        Jsatl12JtMessageBridge bridge = new Jsatl12JtMessageBridge(
                new Jt808MessageDecoder(new BodyParserRegistry(List.of(new PassthroughBodyParser()))),
                dispatched::add);
        byte[] body = new byte[]{0x41, 0x01, 0x02};

        bridge.dispatch(buildFrame(Jt808MessageId.TERMINAL_PASSTHROUGH, "123456789012", 4, body),
                "127.0.0.1:7612");

        assertThat(dispatched).hasSize(1);
        assertThat(dispatched.get(0).command()).isEqualTo(Jt808MessageId.TERMINAL_PASSTHROUGH);
        assertThat(((com.lingniu.ingest.protocol.jt808.model.Jt808Message) dispatched.get(0).payload()).body())
                .isInstanceOf(Jt808Body.Passthrough.class);
    }

    @Test
    void malformedJtMessageIsDispatchedAsJsatl12PassthroughCandidate() {
        List<RawFrame> dispatched = new ArrayList<>();
        Jsatl12JtMessageBridge bridge = new Jsatl12JtMessageBridge(
                new Jt808MessageDecoder(new BodyParserRegistry(List.of(new HeartbeatBodyParser()))),
                dispatched::add);
        byte[] malformed = new byte[]{0x01, 0x02, 0x03};

        bridge.dispatch(malformed, "127.0.0.1:7612");

        assertThat(dispatched).hasSize(1);
        RawFrame frame = dispatched.getFirst();
        assertThat(frame.protocolId()).isEqualTo(ProtocolId.JSATL12);
        assertThat(frame.command()).isEqualTo(Jsatl12MessageId.MALFORMED_JT_MESSAGE);
        assertThat(frame.rawBytes()).containsExactly(malformed);
        assertThat(frame.sourceMeta())
                .containsEntry("peer", "127.0.0.1:7612")
                .containsEntry("parseError", "true")
                .containsEntry("source", "jsatl12-jt-message")
                .containsEntry("vin", "unknown")
                .containsEntry("identityResolved", "false")
                .containsEntry("identitySource", "UNKNOWN");
        assertThat(frame.payload()).isInstanceOf(Jsatl12MalformedJtMessage.class);
    }

    @Test
    void identityResolverFailureStillDispatchesDecodedJt808MessageWithUnknownIdentity() {
        List<RawFrame> dispatched = new ArrayList<>();
        Jsatl12JtMessageBridge bridge = new Jsatl12JtMessageBridge(
                new Jt808MessageDecoder(new BodyParserRegistry(List.of(new HeartbeatBodyParser()))),
                lookup -> {
                    throw new IllegalStateException("identity backend unavailable");
                },
                dispatched::add);
        byte[] raw = buildFrame(Jt808MessageId.TERMINAL_HEARTBEAT, "123456789012", 3);

        bridge.dispatch(raw, "127.0.0.1:7612");

        assertThat(dispatched).singleElement().satisfies(frame -> {
            assertThat(frame.protocolId()).isEqualTo(ProtocolId.JT808);
            assertThat(frame.command()).isEqualTo(Jt808MessageId.TERMINAL_HEARTBEAT);
            assertThat(frame.rawBytes()).isNotNull();
            assertThat(frame.payload()).isInstanceOf(com.lingniu.ingest.protocol.jt808.model.Jt808Message.class);
            assertThat(frame.sourceMeta())
                    .containsEntry("source", "jsatl12-signaling")
                    .containsEntry("peer", "127.0.0.1:7612")
                    .containsEntry("vin", "unknown")
                    .containsEntry("identityResolved", "false")
                    .containsEntry("identitySource", "UNKNOWN")
                    .containsEntry("identityError", "true")
                    .containsEntry("identityErrorMessage", "identity backend unavailable");
        });
    }

    @Test
    void uploadCompletionReturnsT9212RetransmissionAck() {
        List<RawFrame> dispatched = new ArrayList<>();
        Jsatl12AttachmentTracker tracker = new Jsatl12AttachmentTracker();
        Jsatl12JtMessageBridge bridge = new Jsatl12JtMessageBridge(
                new Jt808MessageDecoder(new BodyParserRegistry(List.of())),
                tracker,
                dispatched::add);
        String phone = "123456789012";
        String fileName = "ADAS_001.jpg";

        Optional<byte[]> t1210Ack = bridge.dispatch(buildFrame(0x1210, phone, 10, t1210Body(fileName, 6)),
                "127.0.0.1:7612");
        tracker.recordDataPacket(phone, dataPacket(fileName, 0, new byte[]{0x01, 0x02, 0x03}));
        Optional<byte[]> t9212 = bridge.dispatch(buildFrame(0x1212, phone, 11, t1212Body(fileName, 2, 6)),
                "127.0.0.1:7612");

        assertThat(t1210Ack).isEmpty();
        assertThat(t9212).isPresent();
        byte[] unescaped = Jt808Escape.unescape(t9212.get(), 1, t9212.get().length - 2);
        var ack = new Jt808MessageDecoder(new BodyParserRegistry(List.of()))
                .decode(ByteBuffer.wrap(unescaped));
        assertThat(ack.header().messageId()).isEqualTo(0x9212);
        assertThat(ack.header().phone()).isEqualTo(phone);
        assertThat(ack.header().serialNo()).isEqualTo(11);
        Jt808Body.Raw raw = (Jt808Body.Raw) ack.body();
        byte[] body = raw.bytes();
        int nameLen = body[0] & 0xFF;
        assertThat(new String(body, 1, nameLen, StandardCharsets.UTF_8)).isEqualTo(fileName);
        assertThat(body[1 + nameLen] & 0xFF).isEqualTo(2);
        assertThat(body[2 + nameLen] & 0xFF).isEqualTo(1);
        assertThat(body[3 + nameLen] & 0xFF).isEqualTo(1);
        assertThat(readDword(body, 4 + nameLen)).isEqualTo(3);
        assertThat(readDword(body, 8 + nameLen)).isEqualTo(3);
    }

    private static byte[] buildFrame(int msgId, String phone, int serial) {
        return buildFrame(msgId, phone, serial, new byte[0]);
    }

    private static byte[] buildFrame(int msgId, String phone, int serial, byte[] body) {
        byte[] framed = Jt808FrameEncoder.encode(msgId, phone, serial, body);
        return Jt808Escape.unescape(framed, 1, framed.length - 2);
    }

    private static void writeU16(ByteArrayOutputStream os, int v) {
        os.write((v >> 8) & 0xFF);
        os.write(v & 0xFF);
    }

    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 byte[] t1212Body(String fileName, int type, long size) {
        byte[] name = fileName.getBytes(StandardCharsets.US_ASCII);
        byte[] out = new byte[1 + name.length + 1 + 4];
        out[0] = (byte) name.length;
        System.arraycopy(name, 0, out, 1, name.length);
        out[1 + name.length] = (byte) type;
        writeDword(out, 2 + name.length, size);
        return out;
    }

    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;
        putAscii(out, 4, 50, name);
        writeDword(out, 54, offset);
        writeDword(out, 58, data.length);
        System.arraycopy(data, 0, out, 62, data.length);
        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 long readDword(byte[] raw, int offset) {
        return ((raw[offset] & 0xFFL) << 24)
                | ((raw[offset + 1] & 0xFFL) << 16)
                | ((raw[offset + 2] & 0xFFL) << 8)
                | (raw[offset + 3] & 0xFFL);
    }
}
