package com.lingniu.ingest.protocol.jt808.codec;

import com.lingniu.ingest.codec.BccChecksum;
import com.lingniu.ingest.codec.BcdCodec;
import com.lingniu.ingest.protocol.jt808.codec.parser.HeartbeatBodyParser;
import com.lingniu.ingest.protocol.jt808.codec.parser.AuthBodyParser;
import com.lingniu.ingest.protocol.jt808.codec.parser.LocationBodyParser;
import com.lingniu.ingest.protocol.jt808.codec.parser.LocationBatchBodyParser;
import com.lingniu.ingest.protocol.jt808.codec.parser.MediaEventBodyParser;
import com.lingniu.ingest.protocol.jt808.codec.parser.MediaUploadBodyParser;
import com.lingniu.ingest.protocol.jt808.codec.parser.PassthroughBodyParser;
import com.lingniu.ingest.protocol.jt808.codec.parser.TerminalAttrsBodyParser;
import com.lingniu.ingest.protocol.jt808.codec.parser.TerminalParamsReportBodyParser;
import com.lingniu.ingest.protocol.jt808.codec.parser.UnregisterBodyParser;
import com.lingniu.ingest.protocol.jt808.codec.parser.RegisterBodyParser;
import com.lingniu.ingest.protocol.jt808.model.Jt808Body;
import com.lingniu.ingest.protocol.jt808.model.Jt808LocationAdditionalInfo;
import com.lingniu.ingest.protocol.jt808.model.Jt808Message;
import com.lingniu.ingest.protocol.jt808.model.Jt808MessageId;
import org.junit.jupiter.api.Test;

import java.io.ByteArrayOutputStream;
import java.nio.ByteBuffer;
import java.util.List;

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

class Jt808DecoderTest {

    private final Jt808MessageDecoder decoder = new Jt808MessageDecoder(
            new BodyParserRegistry(List.of(
                    new RegisterBodyParser(),
                    new AuthBodyParser(),
                    new LocationBodyParser(),
                    new LocationBatchBodyParser(),
                    new UnregisterBodyParser(),
                    new TerminalParamsReportBodyParser(),
                    new TerminalAttrsBodyParser(),
                    new MediaEventBodyParser(),
                    new MediaUploadBodyParser(),
                    new PassthroughBodyParser(),
                    new HeartbeatBodyParser())));

    @Test
    void decodesSyntheticLocationFrame() {
        byte[] body = buildLocationBody();
        byte[] frame = buildFrame(Jt808MessageId.TERMINAL_LOCATION, "123456789012", 0x0001, body);
        Jt808Message msg = decoder.decode(ByteBuffer.wrap(frame));

        assertThat(msg.header().messageId()).isEqualTo(Jt808MessageId.TERMINAL_LOCATION);
        assertThat(msg.header().phone()).isEqualTo("123456789012");
        assertThat(msg.header().serialNo()).isEqualTo(1);

        assertThat(msg.body()).isInstanceOf(Jt808Body.Location.class);
        Jt808Body.Location loc = (Jt808Body.Location) msg.body();
        assertThat(loc.longitude()).isEqualTo(116.397128, org.assertj.core.data.Offset.offset(0.000001));
        assertThat(loc.latitude()).isEqualTo(39.916527, org.assertj.core.data.Offset.offset(0.000001));
        assertThat(loc.speedKmh()).isEqualTo(52.3, org.assertj.core.data.Offset.offset(0.01));
    }

    @Test
    void decodesLocationExtensionItems() {
        byte[] body = bytes(os -> {
            os.write(buildLocationBody(), 0, buildLocationBody().length);
            os.write(0x01); // mileage, DWORD, 0.1 km
            os.write(4);
            writeU32(os, 12345);
            os.write(0x30); // wireless signal strength
            os.write(1);
            os.write(88);
        });

        Jt808Body.Location loc = (Jt808Body.Location) decoder.decode(ByteBuffer.wrap(buildFrame(
                Jt808MessageId.TERMINAL_LOCATION, "123456789012", 9, body))).body();

        assertThat(loc.extensionItems()).containsOnlyKeys(0x01, 0x30);
        assertThat(loc.extensionItems().get(0x01)).containsExactly(0x00, 0x00, 0x30, 0x39);
        assertThat(loc.extensionItems().get(0x30)).containsExactly(88);
    }

    @Test
    void decodesForwardedLocationFrameGpsMileageExtension() {
        byte[] framed = hex("""
                7E020000320133077954250001000000000048000301D2C4C707376139000A00E6004F260630162357
                01040001900C2504000000000202000030011F31010F867E
                """);
        byte[] unescaped = Jt808Escape.unescape(framed, 1, framed.length - 2);

        Jt808Body.Location loc = (Jt808Body.Location) decoder.decode(ByteBuffer.wrap(unescaped)).body();

        assertThat(loc.longitude()).isEqualTo(121.069881);
        assertThat(loc.latitude()).isEqualTo(30.590151);
        assertThat(loc.speedKmh()).isEqualTo(23.0);
        assertThat(loc.deviceTime().toString()).isEqualTo("2026-06-30T08:23:57Z");
        assertThat(loc.extensionItems()).containsOnlyKeys(0x01, 0x25, 0x02, 0x30, 0x31);
        assertThat(loc.extensionItems().get(0x01)).containsExactly(0x00, 0x01, (byte) 0x90, 0x0C);
        assertThat(Jt808LocationAdditionalInfo.mileageKm(loc.extensionItems())).isEqualTo(10241.2);
        assertThat(Jt808LocationAdditionalInfo.decode(loc.extensionItems()))
                .containsEntry("jt808.extra.mileage_km", "10241.2")
                .containsEntry("jt808.extra.vehicle_signal_raw", "0")
                .containsEntry("jt808.extra.fuel_l", "0")
                .containsEntry("jt808.extra.network_signal_strength", "31")
                .containsEntry("jt808.extra.gnss_satellite_count", "15");
    }

    @Test
    void decodesSyntheticHeartbeatFrame() {
        byte[] frame = buildFrame(Jt808MessageId.TERMINAL_HEARTBEAT, "123456789012", 0x0002, new byte[0]);
        Jt808Message msg = decoder.decode(ByteBuffer.wrap(frame));
        assertThat(msg.header().messageId()).isEqualTo(Jt808MessageId.TERMINAL_HEARTBEAT);
        assertThat(msg.body()).isInstanceOf(Jt808Body.Heartbeat.class);
    }

    @Test
    void decodesAuthFrameAndExtractsImeiWhenPresent() {
        byte[] authBody = "AUTH-CODE,123456789012345,SW-1".getBytes(java.nio.charset.StandardCharsets.US_ASCII);

        Jt808Body.Auth auth = (Jt808Body.Auth) decoder.decode(ByteBuffer.wrap(buildFrame(
                Jt808MessageId.TERMINAL_AUTH, "123456789012", 10, authBody))).body();

        assertThat(auth.token()).isEqualTo("AUTH-CODE,123456789012345,SW-1");
        assertThat(auth.imei()).isEqualTo("123456789012345");
        assertThat(auth.softwareVersion()).isEqualTo("SW-1");
    }

    @Test
    void decodesLogoutParamsAttrsMediaAndPassthroughFrames() {
        assertThat(decoder.decode(ByteBuffer.wrap(buildFrame(
                Jt808MessageId.TERMINAL_UNREGISTER, "123456789012", 3, new byte[0]))).body())
                .isInstanceOf(Jt808Body.Unregister.class);

        byte[] paramsBody = bytes(os -> {
            writeU16(os, 7);
            os.write(1);
            writeU32(os, 0x00000080L);
            os.write(1);
            os.write(5);
        });
        Jt808Body.ParamsReport params = (Jt808Body.ParamsReport) decoder.decode(ByteBuffer.wrap(buildFrame(
                Jt808MessageId.TERMINAL_PARAMS_REPORT, "123456789012", 4, paramsBody))).body();
        assertThat(params.responseSerialNo()).isEqualTo(7);
        assertThat(params.parameters()).containsKey(0x00000080L);

        byte[] attrsBody = bytes(os -> {
            writeU16(os, 0x1234);
            writeAscii(os, "MAKER", 5);
            writeAscii(os, "MODEL-X", 20);
            writeAscii(os, "DEV1234", 7);
            os.write(BcdCodec.encode("89860012345678901234"), 0, 10);
            writeAscii(os, "HW1", 10);
            writeAscii(os, "SW1", 10);
            os.write(1);
            os.write(2);
        });
        Jt808Body.TerminalAttrs attrs = (Jt808Body.TerminalAttrs) decoder.decode(ByteBuffer.wrap(buildFrame(
                Jt808MessageId.TERMINAL_ATTRS_REPORT, "123456789012", 5, attrsBody))).body();
        assertThat(attrs.maker()).isEqualTo("MAKER");
        assertThat(attrs.terminalId()).isEqualTo("DEV1234");

        byte[] mediaEventBody = bytes(os -> {
            writeU32(os, 42);
            os.write(0);
            os.write(1);
            os.write(2);
            os.write(3);
        });
        assertThat(decoder.decode(ByteBuffer.wrap(buildFrame(
                Jt808MessageId.TERMINAL_MEDIA_EVENT, "123456789012", 6, mediaEventBody))).body())
                .isInstanceOf(Jt808Body.MediaEvent.class);

        byte[] passthroughBody = bytes(os -> {
            os.write(0x41);
            os.write(new byte[]{0x01, 0x02, 0x03}, 0, 3);
        });
        Jt808Body.Passthrough passthrough = (Jt808Body.Passthrough) decoder.decode(ByteBuffer.wrap(buildFrame(
                Jt808MessageId.TERMINAL_PASSTHROUGH, "123456789012", 7, passthroughBody))).body();
        assertThat(passthrough.passthroughType()).isEqualTo(0x41);
        assertThat(passthrough.data()).containsExactly(0x01, 0x02, 0x03);
    }

    @Test
    void decodesLocationBatchFrame() {
        byte[] location = buildLocationBody();
        byte[] batchBody = bytes(os -> {
            writeU16(os, 2);
            os.write(0);
            writeU16(os, location.length);
            os.write(location, 0, location.length);
            writeU16(os, location.length);
            os.write(location, 0, location.length);
        });

        Jt808Body.LocationBatch batch = (Jt808Body.LocationBatch) decoder.decode(ByteBuffer.wrap(buildFrame(
                Jt808MessageId.TERMINAL_LOCATION_BATCH, "123456789012", 8, batchBody))).body();

        assertThat(batch.batchType()).isEqualTo(0);
        assertThat(batch.locations()).hasSize(2);
        assertThat(batch.locations().getFirst().longitude()).isEqualTo(116.397128);
    }

    // ===== helpers =====

    private static byte[] buildLocationBody() {
        ByteArrayOutputStream os = new ByteArrayOutputStream();
        writeU32(os, 0);                  // alarmFlag
        writeU32(os, 0x00030000);         // statusFlag
        writeU32(os, 39_916_527L);        // lat
        writeU32(os, 116_397_128L);       // lon
        writeU16(os, 50);                 // altitude
        writeU16(os, 523);                // speed 52.3
        writeU16(os, 90);                 // direction
        byte[] bcd = BcdCodec.encode("240102030405");
        os.write(bcd, 0, 6);
        return os.toByteArray();
    }

    private static byte[] bytes(IoConsumer<ByteArrayOutputStream> writer) {
        ByteArrayOutputStream os = new ByteArrayOutputStream();
        writer.accept(os);
        return os.toByteArray();
    }

    private static void writeAscii(ByteArrayOutputStream os, String value, int len) {
        byte[] out = new byte[len];
        byte[] raw = value.getBytes(java.nio.charset.StandardCharsets.US_ASCII);
        System.arraycopy(raw, 0, out, 0, Math.min(raw.length, out.length));
        os.write(out, 0, out.length);
    }

    @FunctionalInterface
    private interface IoConsumer<T> {
        void accept(T value);
    }

    private static byte[] buildFrame(int msgId, String phone, int serial, byte[] body) {
        // header: msgId(2) attrs(2) phone(6 BCD) serial(2)
        ByteArrayOutputStream os = new ByteArrayOutputStream();
        writeU16(os, msgId);
        int attrs = body.length & 0x03FF; // 2013 版，无子包、无加密
        writeU16(os, attrs);
        byte[] phoneBcd = BcdCodec.encode(phone);
        os.write(phoneBcd, 0, 6);
        writeU16(os, serial);
        os.write(body, 0, body.length);
        byte[] raw = os.toByteArray();
        byte bcc = BccChecksum.compute(raw, 0, raw.length);

        ByteArrayOutputStream framed = new ByteArrayOutputStream();
        framed.write(raw, 0, raw.length);
        framed.write(bcc & 0xFF);
        return framed.toByteArray();
    }

    private static byte[] hex(String value) {
        String compact = value.replaceAll("\\s+", "");
        byte[] out = new byte[compact.length() / 2];
        for (int i = 0; i < out.length; i++) {
            out[i] = (byte) Integer.parseInt(compact.substring(i * 2, i * 2 + 2), 16);
        }
        return out;
    }

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

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