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

import com.lingniu.ingest.api.ProtocolId;
import com.lingniu.ingest.api.event.VehicleEvent;
import com.lingniu.ingest.identity.InMemoryVehicleIdentityService;
import com.lingniu.ingest.identity.VehicleIdentityBinding;
import com.lingniu.ingest.protocol.jt808.model.Jt808Body;
import com.lingniu.ingest.protocol.jt808.model.Jt808Header;
import com.lingniu.ingest.protocol.jt808.model.Jt808Message;
import com.lingniu.ingest.protocol.jt808.model.Jt808MessageId;
import org.junit.jupiter.api.Test;

import java.time.Instant;
import java.util.List;

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

class Jt808EventMapperTest {

    private final InMemoryVehicleIdentityService identity = new InMemoryVehicleIdentityService();
    private final Jt808EventMapper mapper = new Jt808EventMapper(identity);

    @Test
    void locationBodyProducesLocationEvent() {
        var header = new Jt808Header(
                Jt808MessageId.TERMINAL_LOCATION, 28, 0, false,
                Jt808Header.ProtocolVersion.V2013, "123456789012", 1, 0, 0);
        var body = new Jt808Body.Location(
                0, 0, 116.397128, 39.916527, 50, 52.3, 90, Instant.parse("2024-01-02T03:04:05Z"));
        var msg = new Jt808Message(header, body);

        List<VehicleEvent> events = mapper.toEvents(msg);

        assertThat(events).hasSize(1);
        assertThat(events.get(0)).isInstanceOf(VehicleEvent.Location.class);
        assertThat(events.get(0).source()).isEqualTo(ProtocolId.JT808);
        assertThat(events.get(0).vin()).isEqualTo("unknown");
        assertThat(events.get(0).metadata())
                .containsEntry("vin", "unknown")
                .containsEntry("phone", "123456789012")
                .containsEntry("jt808.status.acc_on", "false")
                .containsEntry("jt808.status.positioned", "false")
                .containsEntry("jt808.status.load_status", "EMPTY")
                .containsEntry("jt808.alarm.overspeed", "false")
                .containsEntry("identityResolved", "false")
                .containsEntry("identitySource", "FALLBACK_PHONE");
    }

    @Test
    void locationExtensionItemsAreExposedAsMetadata() {
        var header = new Jt808Header(
                Jt808MessageId.TERMINAL_LOCATION, 35, 0, false,
                Jt808Header.ProtocolVersion.V2013, "123456789012", 1, 0, 0);
        var body = new Jt808Body.Location(
                0, 0, 116.397128, 39.916527, 50, 52.3, 90,
                Instant.parse("2024-01-02T03:04:05Z"),
                java.util.Map.of(0x01, new byte[]{0x00, 0x00, 0x30, 0x39}, 0x30, new byte[]{0x58}));

        List<VehicleEvent> events = mapper.toEvents(new Jt808Message(header, body));

        assertThat(events).singleElement()
                .extracting(VehicleEvent::metadata)
                .satisfies(meta -> assertThat(meta)
                        .containsEntry("jt808.extra.0x01", "00003039")
                        .containsEntry("jt808.extra.0x30", "58")
                        .containsEntry("jt808.extra.mileage_km", "1234.5")
                        .containsEntry("jt808.extra.network_signal_strength", "88"));
    }

    @Test
    void locationAdditionalItemsAreDecodedAsTable27Fields() {
        var header = new Jt808Header(
                Jt808MessageId.TERMINAL_LOCATION, 64, 0, false,
                Jt808Header.ProtocolVersion.V2013, "123456789012", 1, 0, 0);
        var body = new Jt808Body.Location(
                0, 0, 116.397128, 39.916527, 50, 52.3, 90,
                Instant.parse("2024-01-02T03:04:05Z"),
                java.util.Map.ofEntries(
                        java.util.Map.entry(0x01, new byte[]{0x00, 0x00, 0x30, 0x39}),
                        java.util.Map.entry(0x02, new byte[]{0x00, 0x64}),
                        java.util.Map.entry(0x03, new byte[]{0x01, (byte) 0xF4}),
                        java.util.Map.entry(0x04, new byte[]{0x00, 0x02}),
                        java.util.Map.entry(0x05, tirePressureBytes()),
                        java.util.Map.entry(0x06, new byte[]{(byte) 0x80, 0x05}),
                        java.util.Map.entry(0x11, new byte[]{0x01, 0x00, 0x00, 0x00, 0x2A}),
                        java.util.Map.entry(0x12, new byte[]{0x04, 0x00, 0x00, 0x00, 0x2B, 0x01}),
                        java.util.Map.entry(0x13, new byte[]{0x00, 0x00, 0x00, 0x2C, 0x00, 0x78, 0x01}),
                        java.util.Map.entry(0x25, new byte[]{0x00, 0x00, 0x00, 0x05}),
                        java.util.Map.entry(0x2A, new byte[]{0x00, 0x03}),
                        java.util.Map.entry(0x2B, new byte[]{0x12, 0x34, 0x56, 0x78}),
                        java.util.Map.entry(0x30, new byte[]{0x1F}),
                        java.util.Map.entry(0x31, new byte[]{0x0C})));

        VehicleEvent.Location event = (VehicleEvent.Location) mapper.toEvents(new Jt808Message(header, body)).getFirst();

        assertThat(event.payload().totalMileageKm()).isEqualTo(1234.5);
        assertThat(event.metadata())
                .containsEntry("jt808.extra.mileage_km", "1234.5")
                .containsEntry("jt808.extra.fuel_l", "10")
                .containsEntry("jt808.extra.recorder_speed_kmh", "50")
                .containsEntry("jt808.extra.manual_alarm_event_id", "2")
                .containsEntry("jt808.extra.tire_pressure_pa_values", "32,33")
                .containsEntry("jt808.extra.tire_pressure_invalid_indexes", "2-29")
                .containsEntry("jt808.extra.compartment_temp_c", "-5")
                .containsEntry("jt808.extra.overspeed.position_type", "1")
                .containsEntry("jt808.extra.overspeed.area_route_id", "42")
                .containsEntry("jt808.extra.area_route_alarm.position_type", "4")
                .containsEntry("jt808.extra.area_route_alarm.area_route_id", "43")
                .containsEntry("jt808.extra.area_route_alarm.direction", "1")
                .containsEntry("jt808.extra.route_time.segment_id", "44")
                .containsEntry("jt808.extra.route_time.duration_seconds", "120")
                .containsEntry("jt808.extra.route_time.result", "1")
                .containsEntry("jt808.extra.vehicle_signal_raw", "5")
                .containsEntry("jt808.extra.vehicle_signal.low_beam", "true")
                .containsEntry("jt808.extra.vehicle_signal.high_beam", "false")
                .containsEntry("jt808.extra.vehicle_signal.right_turn", "true")
                .containsEntry("jt808.extra.io_status_raw", "3")
                .containsEntry("jt808.extra.io_status.deep_sleep", "true")
                .containsEntry("jt808.extra.io_status.sleep", "true")
                .containsEntry("jt808.extra.analog_raw", "305419896")
                .containsEntry("jt808.extra.analog_ad0", "22136")
                .containsEntry("jt808.extra.analog_ad1", "4660")
                .containsEntry("jt808.extra.network_signal_strength", "31")
                .containsEntry("jt808.extra.gnss_satellite_count", "12");
    }

    @Test
    void locationMileageExtensionIsMappedToInternalMileageField() {
        var header = new Jt808Header(
                Jt808MessageId.TERMINAL_LOCATION, 34, 0, false,
                Jt808Header.ProtocolVersion.V2013, "123456789012", 1, 0, 0);
        var body = new Jt808Body.Location(
                0, 0, 116.397128, 39.916527, 50, 52.3, 90,
                Instant.parse("2024-01-02T03:04:05Z"),
                java.util.Map.of(0x01, new byte[]{0x00, 0x00, 0x30, 0x39}));

        VehicleEvent.Location event = (VehicleEvent.Location) mapper.toEvents(new Jt808Message(header, body)).getFirst();

        assertThat(event.payload().totalMileageKm()).isEqualTo(1234.5);
        assertThat(event.metadata())
                .containsEntry("jt808.extra.0x01", "00003039")
                .containsEntry("jt808.extra.mileage_km", "1234.5")
                .containsEntry("total_mileage_km", "1234.5");
    }

    @Test
    void locationBodyUsesBoundVehicleIdentity() {
        identity.bind(new VehicleIdentityBinding(ProtocolId.JT808,
                "LNVIN000000000009", "123456789012", "DEV009", "粤B99999"));
        var header = new Jt808Header(
                Jt808MessageId.TERMINAL_LOCATION, 28, 0, false,
                Jt808Header.ProtocolVersion.V2013, "123456789012", 1, 0, 0);
        var body = new Jt808Body.Location(
                0, 0, 116.397128, 39.916527, 50, 52.3, 90, Instant.parse("2024-01-02T03:04:05Z"));

        List<VehicleEvent> events = mapper.toEvents(new Jt808Message(header, body));

        assertThat(events).hasSize(1);
        assertThat(events.get(0).vin()).isEqualTo("LNVIN000000000009");
        assertThat(events.get(0).metadata())
                .containsEntry("vin", "LNVIN000000000009")
                .containsEntry("identityResolved", "true");
    }

    @Test
    void identityResolverFailureStillProducesLocationEventWithUnknownVin() {
        Jt808EventMapper mapperWithFailingIdentity = new Jt808EventMapper(lookup -> {
            throw new IllegalStateException("identity backend unavailable");
        });
        var header = new Jt808Header(
                Jt808MessageId.TERMINAL_LOCATION, 28, 0, false,
                Jt808Header.ProtocolVersion.V2013, "123456789012", 1, 0, 0);
        var body = new Jt808Body.Location(
                0, 0, 116.397128, 39.916527, 50, 52.3, 90, Instant.parse("2024-01-02T03:04:05Z"));

        List<VehicleEvent> events = mapperWithFailingIdentity.toEvents(new Jt808Message(header, body));

        assertThat(events).singleElement()
                .satisfies(event -> {
                    assertThat(event).isInstanceOf(VehicleEvent.Location.class);
                    assertThat(event.vin()).isEqualTo("unknown");
                    assertThat(event.metadata())
                            .containsEntry("vin", "unknown")
                            .containsEntry("phone", "123456789012")
                            .containsEntry("identityResolved", "false")
                            .containsEntry("identitySource", "UNKNOWN")
                            .containsEntry("identityError", "true")
                            .containsEntry("identityErrorMessage", "identity backend unavailable");
                });
    }

    @Test
    void heartbeatBodyProducesHeartbeatEvent() {
        var header = new Jt808Header(
                Jt808MessageId.TERMINAL_HEARTBEAT, 0, 0, false,
                Jt808Header.ProtocolVersion.V2013, "123456789012", 1, 0, 0);
        var msg = new Jt808Message(header, new Jt808Body.Heartbeat());
        assertThat(mapper.toEvents(msg)).hasSize(1)
                .first().isInstanceOf(VehicleEvent.Heartbeat.class);
    }

    @Test
    void registerBodyPreservesRegistrationFieldsInMetadata() {
        var header = new Jt808Header(
                Jt808MessageId.TERMINAL_REGISTER, 37, 0, false,
                Jt808Header.ProtocolVersion.V2013, "123456789012", 1, 0, 0);
        var body = new Jt808Body.Register(
                44, 1, "G7", "MODEL-X", "DEV123456", 2, "粤B12345");

        List<VehicleEvent> events = mapper.toEvents(new Jt808Message(header, body));

        assertThat(events).singleElement()
                .satisfies(event -> {
                    assertThat(event).isInstanceOf(VehicleEvent.Login.class);
                    assertThat(event.metadata())
                            .containsEntry("jt808.register.province", "44")
                            .containsEntry("jt808.register.city", "1")
                            .containsEntry("jt808.register.maker", "G7")
                            .containsEntry("jt808.register.deviceType", "MODEL-X")
                            .containsEntry("jt808.register.deviceId", "DEV123456")
                            .containsEntry("jt808.register.plateColor", "2")
                            .containsEntry("jt808.register.plate", "粤B12345");
                });
    }

    @Test
    void unregisterBodyProducesLogoutEvent() {
        var header = new Jt808Header(
                Jt808MessageId.TERMINAL_UNREGISTER, 0, 0, false,
                Jt808Header.ProtocolVersion.V2013, "123456789012", 1, 0, 0);

        assertThat(mapper.toEvents(new Jt808Message(header, new Jt808Body.Unregister())))
                .singleElement()
                .isInstanceOf(VehicleEvent.Logout.class);
    }

    @Test
    void locationBatchProducesOneLocationEventPerPoint() {
        var header = new Jt808Header(
                Jt808MessageId.TERMINAL_LOCATION_BATCH, 0, 0, false,
                Jt808Header.ProtocolVersion.V2013, "123456789012", 1, 0, 0);
        var loc = new Jt808Body.Location(
                0, 0, 116.397128, 39.916527, 50, 52.3, 90, Instant.parse("2024-01-02T03:04:05Z"));

        List<VehicleEvent> events = mapper.toEvents(new Jt808Message(
                header, new Jt808Body.LocationBatch(0, List.of(loc, loc))));

        assertThat(events).hasSize(2)
                .allSatisfy(event -> assertThat(event).isInstanceOf(VehicleEvent.Location.class));
    }

    @Test
    void mediaAndPassthroughBodiesProduceQueryableEvents() {
        var header = new Jt808Header(
                Jt808MessageId.TERMINAL_MEDIA_EVENT, 0, 0, false,
                Jt808Header.ProtocolVersion.V2013, "123456789012", 1, 0, 0);
        assertThat(mapper.toEvents(new Jt808Message(
                header, new Jt808Body.MediaEvent(42, 0, 1, 2, 3))))
                .singleElement()
                .isInstanceOf(VehicleEvent.MediaMeta.class);

        var passHeader = new Jt808Header(
                Jt808MessageId.TERMINAL_PASSTHROUGH, 0, 0, false,
                Jt808Header.ProtocolVersion.V2013, "123456789012", 2, 0, 0);
        assertThat(mapper.toEvents(new Jt808Message(
                passHeader, new Jt808Body.Passthrough(0x41, new byte[]{1, 2, 3}))))
                .singleElement()
                .satisfies(event -> {
                    assertThat(event).isInstanceOf(VehicleEvent.Passthrough.class);
                    VehicleEvent.Passthrough passthrough = (VehicleEvent.Passthrough) event;
                    assertThat(passthrough.passthroughType()).isEqualTo(Jt808MessageId.TERMINAL_PASSTHROUGH);
                    assertThat(passthrough.metadata()).containsEntry("passthroughType", "0x41");
                    assertThat(passthrough.data()).containsExactly(1, 2, 3);
                });
    }

    @Test
    void rawBodyProducesPassthroughEventSoUnknownMessagesRemainQueryable() {
        var header = new Jt808Header(
                0x0F01, 3, 0, false,
                Jt808Header.ProtocolVersion.V2013, "123456789012", 9, 0, 0);

        assertThat(mapper.toEvents(new Jt808Message(
                header, new Jt808Body.Raw(0x0F01, new byte[]{0x11, 0x22, 0x33}))))
                .singleElement()
                .satisfies(event -> {
                    assertThat(event).isInstanceOf(VehicleEvent.Passthrough.class);
                    var passthrough = (VehicleEvent.Passthrough) event;
                    assertThat(passthrough.passthroughType()).isEqualTo(0x0F01);
                    assertThat(passthrough.data()).containsExactly(0x11, 0x22, 0x33);
                    assertThat(passthrough.metadata())
                            .containsEntry("vin", "unknown")
                            .containsEntry("phone", "123456789012")
                            .containsEntry("identityResolved", "false")
                            .containsEntry("identitySource", "FALLBACK_PHONE")
                            .containsEntry("rawBody", "true");
                });
    }

    @Test
    void malformedBodyPassthroughMetadataUsesUnknownInternalVin() {
        var header = new Jt808Header(
                0, 3, 0, false,
                Jt808Header.ProtocolVersion.V2013, "123456789012", 9, 0, 0);

        assertThat(mapper.toEvents(new Jt808Message(
                header, new Jt808Body.Malformed(new byte[]{0x01, 0x02, 0x03}, "127.0.0.1:7611", "decode failed"))))
                .singleElement()
                .satisfies(event -> {
                    assertThat(event).isInstanceOf(VehicleEvent.Passthrough.class);
                    assertThat(event.vin()).isEqualTo("unknown");
                    assertThat(event.metadata())
                            .containsEntry("vin", "unknown")
                            .containsEntry("identityResolved", "false")
                            .containsEntry("parseError", "true");
                });
    }

    private static byte[] tirePressureBytes() {
        byte[] bytes = new byte[30];
        java.util.Arrays.fill(bytes, (byte) 0xFF);
        bytes[0] = 32;
        bytes[1] = 33;
        return bytes;
    }
}
