package com.lingniu.ingest.inbound.xinda;

import com.gps31.push.netty.PushMsg;
import com.lingniu.ingest.api.ProtocolId;
import com.lingniu.ingest.api.pipeline.RawFrame;
import com.lingniu.ingest.core.dispatcher.Dispatcher;
import com.lingniu.ingest.identity.InMemoryVehicleIdentityService;
import com.lingniu.ingest.identity.VehicleIdentityBinding;
import com.lingniu.ingest.inbound.xinda.config.XindaPushProperties;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;

import java.nio.charset.StandardCharsets;

import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;

class XindaPushClientTest {

    @Test
    void missingHostIsDispatchedAsOperationalPassthroughCandidate() {
        Dispatcher dispatcher = mock(Dispatcher.class);
        XindaPushProperties props = new XindaPushProperties();
        props.setUsername("user");
        props.setPassword("pwd");
        XindaPushClient client = new XindaPushClient(props, dispatcher);

        client.start();

        ArgumentCaptor<RawFrame> captor = ArgumentCaptor.forClass(RawFrame.class);
        verify(dispatcher).dispatch(captor.capture());
        RawFrame frame = captor.getValue();
        assertThat(frame.protocolId()).isEqualTo(ProtocolId.XINDA_PUSH);
        assertThat(frame.command()).isZero();
        assertThat(frame.payload()).isInstanceOf(XindaPushMessage.class);
        assertThat(frame.rawBytes()).asString(StandardCharsets.UTF_8)
                .contains("\"operationalError\":true")
                .contains("\"phase\":\"startup\"")
                .contains("\"reason\":\"host not configured\"");
        assertThat(frame.sourceMeta())
                .containsEntry("source", "xinda-push")
                .containsEntry("operationalError", "true")
                .containsEntry("phase", "startup")
                .containsEntry("reason", "host not configured")
                .containsEntry("vin", "unknown")
                .containsEntry("identityResolved", "false")
                .containsEntry("identitySource", "UNKNOWN");
    }

    @Test
    void unknownBusinessCommandIsDispatchedForArchiveAndPassthrough() throws Exception {
        Dispatcher dispatcher = mock(Dispatcher.class);
        XindaPushClient client = new XindaPushClient(new XindaPushProperties(), dispatcher);
        PushMsg msg = new PushMsg();
        msg.init("0500", "{\"vin\":\"LNVIN000000000303\",\"vendor\":\"extension\"}");

        client.messageReceived(null, msg);

        ArgumentCaptor<RawFrame> captor = ArgumentCaptor.forClass(RawFrame.class);
        verify(dispatcher).dispatch(captor.capture());
        RawFrame frame = captor.getValue();
        assertThat(frame.protocolId()).isEqualTo(ProtocolId.XINDA_PUSH);
        assertThat(frame.command()).isEqualTo(0x0500);
        assertThat(frame.payload()).isEqualTo(new XindaPushMessage("0500",
                "{\"vin\":\"LNVIN000000000303\",\"vendor\":\"extension\"}"));
        assertThat(frame.rawBytes()).containsExactly(
                "{\"vin\":\"LNVIN000000000303\",\"vendor\":\"extension\"}".getBytes(StandardCharsets.UTF_8));
        assertThat(frame.sourceMeta())
                .containsEntry("source", "xinda-push")
                .containsEntry("cmd", "0500")
                .containsEntry("vin", "LNVIN000000000303")
                .containsEntry("unknownCommand", "true");
    }

    @Test
    void businessMessageRawFrameUsesResolvedVehicleIdentity() throws Exception {
        Dispatcher dispatcher = mock(Dispatcher.class);
        InMemoryVehicleIdentityService identities = new InMemoryVehicleIdentityService();
        identities.bind(new VehicleIdentityBinding(
                ProtocolId.XINDA_PUSH,
                "LNVIN000000XINDA1",
                "",
                "XD-DEVICE-001",
                "粤B12345"));
        XindaPushClient client = new XindaPushClient(new XindaPushProperties(), identities, dispatcher);
        PushMsg msg = new PushMsg();
        msg.init("0200", "{\"deviceId\":\"XD-DEVICE-001\",\"plateNo\":\"粤B12345\",\"lng\":113.1,\"lat\":23.1}");

        client.messageReceived(null, msg);

        ArgumentCaptor<RawFrame> captor = ArgumentCaptor.forClass(RawFrame.class);
        verify(dispatcher).dispatch(captor.capture());
        RawFrame frame = captor.getValue();
        assertThat(frame.sourceMeta())
                .containsEntry("vin", "LNVIN000000XINDA1")
                .containsEntry("deviceId", "XD-DEVICE-001")
                .containsEntry("plateNo", "粤B12345")
                .containsEntry("identityResolved", "true")
                .containsEntry("identitySource", "BOUND_DEVICE_ID");
    }

    @Test
    void businessMessageRawFrameResolvesIdentityByCarNamePlateAlias() throws Exception {
        Dispatcher dispatcher = mock(Dispatcher.class);
        InMemoryVehicleIdentityService identities = new InMemoryVehicleIdentityService();
        identities.bind(new VehicleIdentityBinding(
                ProtocolId.XINDA_PUSH,
                "LB9A32A24P0LS1270",
                "",
                "",
                "粤BD12345"));
        XindaPushClient client = new XindaPushClient(new XindaPushProperties(), identities, dispatcher);
        PushMsg msg = new PushMsg();
        msg.init("0200", "{\"carName\":\"粤BD12345\",\"tmnKey\":\"XD-TMN-001\",\"lng\":113.1,\"lat\":23.1}");

        client.messageReceived(null, msg);

        ArgumentCaptor<RawFrame> captor = ArgumentCaptor.forClass(RawFrame.class);
        verify(dispatcher).dispatch(captor.capture());
        RawFrame frame = captor.getValue();
        assertThat(frame.sourceMeta())
                .containsEntry("vin", "LB9A32A24P0LS1270")
                .containsEntry("plateNo", "粤BD12345")
                .containsEntry("deviceId", "XD-TMN-001")
                .containsEntry("raw.carName", "粤BD12345")
                .containsEntry("raw.tmnKey", "XD-TMN-001")
                .containsEntry("raw.lng", "113.1")
                .containsEntry("identityResolved", "true")
                .containsEntry("identitySource", "BOUND_PLATE");
    }

    @Test
    void malformedCommandIsStillDispatchedForArchiveAndPassthrough() throws Exception {
        Dispatcher dispatcher = mock(Dispatcher.class);
        XindaPushClient client = new XindaPushClient(new XindaPushProperties(), dispatcher);
        PushMsg msg = new PushMsg();
        msg.init("bad-cmd", "{\"vin\":\"LNVIN000000000404\",\"vendor\":\"broken\"}");

        client.messageReceived(null, msg);

        ArgumentCaptor<RawFrame> captor = ArgumentCaptor.forClass(RawFrame.class);
        verify(dispatcher).dispatch(captor.capture());
        RawFrame frame = captor.getValue();
        assertThat(frame.protocolId()).isEqualTo(ProtocolId.XINDA_PUSH);
        assertThat(frame.command()).isZero();
        assertThat(frame.payload()).isEqualTo(new XindaPushMessage("bad-cmd",
                "{\"vin\":\"LNVIN000000000404\",\"vendor\":\"broken\"}"));
        assertThat(frame.rawBytes()).containsExactly(
                "{\"vin\":\"LNVIN000000000404\",\"vendor\":\"broken\"}".getBytes(StandardCharsets.UTF_8));
        assertThat(frame.sourceMeta())
                .containsEntry("source", "xinda-push")
                .containsEntry("cmd", "bad-cmd")
                .containsEntry("vin", "LNVIN000000000404")
                .containsEntry("unknownCommand", "true")
                .containsEntry("malformedCommand", "true");
    }

    @Test
    void malformedJsonBusinessMessageMarksRawFrameForArchiveDiagnostics() throws Exception {
        Dispatcher dispatcher = mock(Dispatcher.class);
        XindaPushClient client = new XindaPushClient(new XindaPushProperties(), dispatcher);
        PushMsg msg = new PushMsg();
        msg.init("0200", "{bad-json");

        client.messageReceived(null, msg);

        ArgumentCaptor<RawFrame> captor = ArgumentCaptor.forClass(RawFrame.class);
        verify(dispatcher).dispatch(captor.capture());
        RawFrame frame = captor.getValue();
        assertThat(frame.protocolId()).isEqualTo(ProtocolId.XINDA_PUSH);
        assertThat(frame.command()).isEqualTo(0x0200);
        assertThat(frame.rawBytes()).containsExactly("{bad-json".getBytes(StandardCharsets.UTF_8));
        assertThat(frame.sourceMeta())
                .containsEntry("source", "xinda-push")
                .containsEntry("cmd", "0200")
                .containsEntry("vin", "unknown")
                .containsEntry("identityResolved", "false")
                .containsEntry("identitySource", "UNKNOWN")
                .containsEntry("parseError", "true")
                .containsKey("parseErrorMessage");
        assertThat(frame.sourceMeta().get("parseErrorMessage")).contains("expect");
    }

    @Test
    void identityResolverFailureStillDispatchesBusinessMessageWithUnknownIdentity() throws Exception {
        Dispatcher dispatcher = mock(Dispatcher.class);
        XindaPushClient client = new XindaPushClient(
                new XindaPushProperties(),
                lookup -> {
                    throw new IllegalStateException("identity backend unavailable");
                },
                dispatcher);
        PushMsg msg = new PushMsg();
        msg.init("0200", "{\"deviceId\":\"XD-DEVICE-001\",\"lng\":113.1,\"lat\":23.1}");

        client.messageReceived(null, msg);

        ArgumentCaptor<RawFrame> captor = ArgumentCaptor.forClass(RawFrame.class);
        verify(dispatcher).dispatch(captor.capture());
        RawFrame frame = captor.getValue();
        assertThat(frame.protocolId()).isEqualTo(ProtocolId.XINDA_PUSH);
        assertThat(frame.command()).isEqualTo(0x0200);
        assertThat(frame.rawBytes()).containsExactly(
                "{\"deviceId\":\"XD-DEVICE-001\",\"lng\":113.1,\"lat\":23.1}".getBytes(StandardCharsets.UTF_8));
        assertThat(frame.sourceMeta())
                .containsEntry("source", "xinda-push")
                .containsEntry("cmd", "0200")
                .containsEntry("vin", "unknown")
                .containsEntry("identityResolved", "false")
                .containsEntry("identitySource", "UNKNOWN")
                .containsEntry("identityError", "true")
                .containsEntry("identityErrorMessage", "identity backend unavailable");
        assertThat(frame.sourceMeta()).doesNotContainKeys("operationalError", "phase", "reason");
    }

    @Test
    void rejectedLoginAckIsDispatchedAsOperationalPassthroughCandidate() throws Exception {
        Dispatcher dispatcher = mock(Dispatcher.class);
        XindaPushClient client = new XindaPushClient(new XindaPushProperties(), dispatcher);
        PushMsg msg = new PushMsg();
        msg.init("8001", "{\"rspResult\":\"1\",\"message\":\"bad credentials\"}");

        client.messageReceived(null, msg);

        ArgumentCaptor<RawFrame> captor = ArgumentCaptor.forClass(RawFrame.class);
        verify(dispatcher).dispatch(captor.capture());
        RawFrame frame = captor.getValue();
        assertThat(frame.protocolId()).isEqualTo(ProtocolId.XINDA_PUSH);
        assertThat(frame.command()).isZero();
        assertThat(frame.rawBytes()).asString(StandardCharsets.UTF_8)
                .contains("\"operationalError\":true")
                .contains("\"phase\":\"login\"")
                .contains("login rejected rspResult=1");
        assertThat(frame.sourceMeta())
                .containsEntry("source", "xinda-push")
                .containsEntry("operationalError", "true")
                .containsEntry("phase", "login")
                .containsEntry("vin", "unknown")
                .containsEntry("identityResolved", "false")
                .containsEntry("identitySource", "UNKNOWN");
        assertThat(frame.sourceMeta().get("reason")).contains("login rejected rspResult=1");
    }

    @Test
    void malformedLoginAckIsDispatchedAsOperationalPassthroughCandidate() throws Exception {
        Dispatcher dispatcher = mock(Dispatcher.class);
        XindaPushClient client = new XindaPushClient(new XindaPushProperties(), dispatcher);
        PushMsg msg = new PushMsg();
        msg.init("8001", "{bad-json");

        client.messageReceived(null, msg);

        ArgumentCaptor<RawFrame> captor = ArgumentCaptor.forClass(RawFrame.class);
        verify(dispatcher).dispatch(captor.capture());
        RawFrame frame = captor.getValue();
        assertThat(frame.protocolId()).isEqualTo(ProtocolId.XINDA_PUSH);
        assertThat(frame.command()).isZero();
        assertThat(frame.rawBytes()).asString(StandardCharsets.UTF_8)
                .contains("\"operationalError\":true")
                .contains("\"phase\":\"login\"")
                .contains("login ack parse failed");
        assertThat(frame.sourceMeta())
                .containsEntry("source", "xinda-push")
                .containsEntry("operationalError", "true")
                .containsEntry("phase", "login")
                .containsEntry("vin", "unknown")
                .containsEntry("identityResolved", "false")
                .containsEntry("identitySource", "UNKNOWN");
        assertThat(frame.sourceMeta().get("reason")).contains("login ack parse failed");
    }
}
