package com.lingniu.ingest.inbound.xinda;

import com.lingniu.ingest.api.ProtocolId;
import com.lingniu.ingest.api.event.AlarmPayload;
import com.lingniu.ingest.api.event.VehicleEvent;
import com.lingniu.ingest.identity.InMemoryVehicleIdentityService;
import com.lingniu.ingest.identity.VehicleIdentityBinding;
import com.lingniu.ingest.identity.VehicleIdentityRegistry;
import com.lingniu.ingest.identity.VehicleRegistrationBinding;
import org.junit.jupiter.api.Test;

import java.util.List;

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

class XindaPushEventMapperTest {

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

    @Test
    void maps0200ToLocationEvent() {
        XindaPushMessage message = new XindaPushMessage("0200",
                "{\"vin\":\"LNVIN000000000101\",\"sim\":\"13800138000\",\"deviceId\":\"XD-101\",\"plateNo\":\"粤B10101\",\"lng\":116397128,\"lat\":39916527,\"speed\":52.3,\"drct\":90}");

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

        assertThat(events).hasSize(1);
        assertThat(events.get(0)).isInstanceOf(VehicleEvent.Location.class);
        VehicleEvent.Location loc = (VehicleEvent.Location) events.get(0);
        assertThat(loc.source()).isEqualTo(ProtocolId.XINDA_PUSH);
        assertThat(loc.vin()).isEqualTo("LNVIN000000000101");
        assertThat(loc.payload().longitude()).isEqualTo(116.397128);
        assertThat(loc.payload().latitude()).isEqualTo(39.916527);
        assertThat(loc.metadata())
                .containsEntry("vin", "LNVIN000000000101")
                .containsEntry("externalVin", "LNVIN000000000101")
                .containsEntry("phone", "13800138000")
                .containsEntry("deviceId", "XD-101")
                .containsEntry("plateNo", "粤B10101")
                .doesNotContainKey("rawJson");
    }

    @Test
    void maps0200MileageToInternalLocationPayload() {
        XindaPushMessage message = new XindaPushMessage("0200",
                "{\"vin\":\"LNVIN000000000101\",\"lng\":116397128,\"lat\":39916527,\"totalMileage\":12345.6}");

        VehicleEvent.Location loc = (VehicleEvent.Location) mapper.toEvents(message).getFirst();

        assertThat(loc.payload().totalMileageKm()).isEqualTo(12345.6);
    }

    @Test
    void mapsAlarmAndPassthroughEvents() {
        assertThat(mapper.toEvents(new XindaPushMessage("0300", "{\"plateNo\":\"粤B12345\"}")))
                .singleElement()
                .isInstanceOf(VehicleEvent.Alarm.class)
                .extracting(VehicleEvent::source)
                .isEqualTo(ProtocolId.XINDA_PUSH);

        assertThat(mapper.toEvents(new XindaPushMessage("0401", "{\"plateNo\":\"粤B12345\"}")))
                .singleElement()
                .isInstanceOf(VehicleEvent.Passthrough.class);
    }

    @Test
    void mapsHydrogenLeakAlarmToCriticalAlarmEvent() {
        identity.bind(new VehicleIdentityBinding(
                ProtocolId.XINDA_PUSH, "LNVIN000000H2LEAK", "", "XD-H2-001", ""));
        XindaPushMessage message = new XindaPushMessage("0300", """
                {
                  "deviceId": "XD-H2-001",
                  "alarmType": "HYDROGEN_LEAK",
                  "alarmName": "氢气泄漏",
                  "alarmLevel": 3,
                  "lng": 116397128,
                  "lat": 39916527
                }
                """);

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

        assertThat(events).singleElement().satisfies(event -> {
            assertThat(event).isInstanceOf(VehicleEvent.Alarm.class);
            VehicleEvent.Alarm alarm = (VehicleEvent.Alarm) event;
            assertThat(alarm.vin()).isEqualTo("LNVIN000000H2LEAK");
            assertThat(alarm.payload().level()).isEqualTo(AlarmPayload.AlarmLevel.CRITICAL);
            assertThat(alarm.payload().safetyCategory()).isEqualTo(AlarmPayload.SafetyCategory.HYDROGEN_LEAK);
            assertThat(alarm.payload().hydrogenLeakDetected()).isTrue();
            assertThat(alarm.payload().hydrogenLeakLevel()).isEqualTo(AlarmPayload.HydrogenLeakLevel.CRITICAL);
            assertThat(alarm.payload().hydrogenLeakActionRequired()).isTrue();
            assertThat(alarm.payload().longitude()).isEqualTo(116.397128);
            assertThat(alarm.payload().latitude()).isEqualTo(39.916527);
            assertThat(alarm.metadata())
                    .containsEntry("alarmType", "HYDROGEN_LEAK")
                    .containsEntry("alarmName", "氢气泄漏")
                    .containsEntry("hydrogenLeakDetected", "true");
        });
    }

    @Test
    void resolvesVehicleIdentityByPlate() {
        identity.bind(new VehicleIdentityBinding(
                ProtocolId.XINDA_PUSH, "LNVIN000000000202", "", "", "粤B12345"));

        List<VehicleEvent> events = mapper.toEvents(new XindaPushMessage("0200",
                "{\"plateNo\":\"粤B12345\",\"lng\":116397128,\"lat\":39916527}"));

        assertThat(events).singleElement()
                .satisfies(event -> {
                    assertThat(event.vin()).isEqualTo("LNVIN000000000202");
                    assertThat(event.metadata())
                            .containsEntry("vin", "LNVIN000000000202")
                            .containsEntry("identityResolved", "true");
                });
    }

    @Test
    void resolvesVehicleIdentityByCarNamePlateFromXindaPayload() {
        identity.bind(new VehicleIdentityBinding(
                ProtocolId.XINDA_PUSH, "LNVIN000000000203", "", "", "浙F00780F"));

        List<VehicleEvent> events = mapper.toEvents(new XindaPushMessage("0200",
                "{\"carName\":\"浙F00780F\",\"tmnKey\":\"13307811342\",\"simCode\":\"8986062582008805648\","
                        + "\"lng\":121.075223,\"lat\":30.583753}"));

        assertThat(events).singleElement()
                .satisfies(event -> {
                    assertThat(event.vin()).isEqualTo("LNVIN000000000203");
                    assertThat(event.metadata())
                            .containsEntry("vin", "LNVIN000000000203")
                            .containsEntry("plateNo", "浙F00780F")
                            .containsEntry("phone", "8986062582008805648")
                            .containsEntry("deviceId", "13307811342")
                            .containsEntry("identityResolved", "true");
                });
    }

    @Test
    void registersXindaIdentityFromCarNamePayload() {
        RecordingIdentityRegistry registry = new RecordingIdentityRegistry();
        identity.bind(new VehicleIdentityBinding(
                ProtocolId.XINDA_PUSH, "LA9GG68L8PBAF4776", "", "", "浙F00780F"));
        XindaPushEventMapper mapper = new XindaPushEventMapper(identity, registry);

        mapper.toEvents(new XindaPushMessage("0200",
                "{\"carName\":\"浙F00780F\",\"tmnKey\":\"13307811342\",\"simCode\":\"8986062582008805648\","
                        + "\"carColor\":\"93\",\"lng\":121.075223,\"lat\":30.583753}"));

        assertThat(registry.registration)
                .isNotNull()
                .satisfies(registration -> {
                    assertThat(registration.protocol()).isEqualTo(ProtocolId.XINDA_PUSH);
                    assertThat(registration.vin()).isEqualTo("LA9GG68L8PBAF4776");
                    assertThat(registration.phone()).isEqualTo("8986062582008805648");
                    assertThat(registration.deviceId()).isEqualTo("13307811342");
                    assertThat(registration.plate()).isEqualTo("浙F00780F");
                    assertThat(registration.plateColor()).isEqualTo(93);
                });
    }

    @Test
    void registersUnknownVinWhenXindaIdentityIsNotResolved() {
        RecordingIdentityRegistry registry = new RecordingIdentityRegistry();
        XindaPushEventMapper mapper = new XindaPushEventMapper(identity, registry);

        mapper.toEvents(new XindaPushMessage("0200",
                "{\"carName\":\"浙F00780F\",\"tmnKey\":\"13307811342\","
                        + "\"lng\":121.075223,\"lat\":30.583753}"));

        assertThat(registry.registration)
                .isNotNull()
                .satisfies(registration -> {
                    assertThat(registration.protocol()).isEqualTo(ProtocolId.XINDA_PUSH);
                    assertThat(registration.vin()).isEqualTo("unknown");
                    assertThat(registration.phone()).isEqualTo("13307811342");
                    assertThat(registration.deviceId()).isEqualTo("13307811342");
                    assertThat(registration.plate()).isEqualTo("浙F00780F");
                });
    }

    @Test
    void identityResolverFailureStillProducesLocationEventWithUnknownVin() {
        XindaPushEventMapper mapperWithFailingIdentity = new XindaPushEventMapper(lookup -> {
            throw new IllegalStateException("identity backend unavailable");
        });

        List<VehicleEvent> events = mapperWithFailingIdentity.toEvents(new XindaPushMessage("0200",
                "{\"deviceId\":\"XD-DEVICE-001\",\"lng\":116397128,\"lat\":39916527,\"speed\":52.3}"));

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

    @Test
    void malformedJsonFallsBackToPassthroughEvent() {
        List<VehicleEvent> events = mapper.toEvents(new XindaPushMessage("0200", "{bad-json"));

        assertThat(events).singleElement()
                .satisfies(event -> {
                    assertThat(event).isInstanceOf(VehicleEvent.Passthrough.class);
                    VehicleEvent.Passthrough passthrough = (VehicleEvent.Passthrough) event;
                    assertThat(passthrough.data()).containsExactly("{bad-json".getBytes(java.nio.charset.StandardCharsets.UTF_8));
                    assertThat(event.metadata())
                            .containsEntry("vin", "unknown")
                            .containsEntry("parseError", "true")
                            .containsKey("parseErrorMessage");
                    assertThat(event.metadata().get("parseErrorMessage")).contains("expect");
                });
    }

    @Test
    void unknownBusinessCommandFallsBackToPassthroughEvent() {
        List<VehicleEvent> events = mapper.toEvents(new XindaPushMessage("0500",
                "{\"vin\":\"LNVIN000000000303\",\"vendor\":\"extension\"}"));

        assertThat(events).singleElement()
                .satisfies(event -> {
                    assertThat(event).isInstanceOf(VehicleEvent.Passthrough.class);
                    assertThat(event.source()).isEqualTo(ProtocolId.XINDA_PUSH);
                    assertThat(event.vin()).isEqualTo("LNVIN000000000303");
                    assertThat(event.metadata())
                            .containsEntry("vin", "LNVIN000000000303")
                            .containsEntry("cmd", "0500")
                            .containsEntry("unknownCommand", "true")
                            .containsEntry("parseError", "false")
                            .containsEntry("externalVin", "LNVIN000000000303");
                });
    }

    @Test
    void malformedCommandFallsBackToPassthroughEventWithMetadata() {
        List<VehicleEvent> events = mapper.toEvents(new XindaPushMessage("bad-cmd",
                "{\"vin\":\"LNVIN000000000404\",\"vendor\":\"broken\"}"));

        assertThat(events).singleElement()
                .satisfies(event -> {
                    assertThat(event).isInstanceOf(VehicleEvent.Passthrough.class);
                    assertThat(event.source()).isEqualTo(ProtocolId.XINDA_PUSH);
                    assertThat(event.vin()).isEqualTo("LNVIN000000000404");
                    assertThat(((VehicleEvent.Passthrough) event).passthroughType()).isZero();
                    assertThat(event.metadata())
                            .containsEntry("vin", "LNVIN000000000404")
                            .containsEntry("cmd", "bad-cmd")
                            .containsEntry("unknownCommand", "true")
                            .containsEntry("malformedCommand", "true")
                            .containsEntry("parseError", "false");
                });
    }

    @Test
    void operationalFailureMessageBecomesPassthroughEventWithFailureMetadata() {
        List<VehicleEvent> events = mapper.toEvents(new XindaPushMessage("0000",
                "{\"operationalError\":true,\"phase\":\"startup\",\"reason\":\"host not configured\"}"));

        assertThat(events).singleElement()
                .satisfies(event -> {
                    assertThat(event).isInstanceOf(VehicleEvent.Passthrough.class);
                    assertThat(event.source()).isEqualTo(ProtocolId.XINDA_PUSH);
                    assertThat(event.vin()).isEqualTo("unknown");
                    assertThat(event.metadata())
                            .containsEntry("vin", "unknown")
                            .containsEntry("cmd", "0000")
                            .containsEntry("operationalError", "true")
                            .containsEntry("phase", "startup")
                            .containsEntry("reason", "host not configured")
                            .containsEntry("unknownCommand", "false")
                            .containsEntry("parseError", "false");
                });
    }

    private static final class RecordingIdentityRegistry implements VehicleIdentityRegistry {
        private VehicleRegistrationBinding registration;

        @Override
        public void bind(VehicleIdentityBinding binding) {
        }

        @Override
        public void register(VehicleRegistrationBinding registration) {
            this.registration = registration;
        }
    }
}
