package com.lingniu.ingest.inbound.mqtt;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.lingniu.ingest.api.ProtocolId;
import com.lingniu.ingest.api.event.VehicleEvent;
import com.lingniu.ingest.api.sink.EventSink;
import com.lingniu.ingest.core.concurrency.AsyncBatchExecutor;
import com.lingniu.ingest.core.concurrency.DisruptorEventBus;
import com.lingniu.ingest.core.dispatcher.Dispatcher;
import com.lingniu.ingest.core.dispatcher.HandlerDefinition;
import com.lingniu.ingest.core.dispatcher.HandlerInvoker;
import com.lingniu.ingest.core.dispatcher.HandlerRegistry;
import com.lingniu.ingest.core.pipeline.InterceptorChain;
import com.lingniu.ingest.identity.InMemoryVehicleIdentityService;
import com.lingniu.ingest.identity.VehicleIdentity;
import com.lingniu.ingest.identity.VehicleIdentityBinding;
import com.lingniu.ingest.identity.VehicleIdentityLookup;
import com.lingniu.ingest.identity.VehicleIdentityResolver;
import com.lingniu.ingest.identity.VehicleIdentitySource;
import com.lingniu.ingest.inbound.mqtt.client.MqttEndpointManager;
import com.lingniu.ingest.inbound.mqtt.config.MqttInboundProperties;
import com.lingniu.ingest.inbound.mqtt.handler.MqttRealtimeHandler;
import com.lingniu.ingest.inbound.mqtt.mapper.YutongEventMapper;
import com.lingniu.ingest.inbound.mqtt.model.MqttPayload;
import com.lingniu.ingest.inbound.mqtt.parser.MqttPayloadParser;
import com.lingniu.ingest.inbound.mqtt.profile.MqttProfileRegistry;
import com.lingniu.ingest.inbound.mqtt.profile.YutongMqttProfile;
import org.junit.jupiter.api.Test;

import java.lang.reflect.Method;
import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;

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

class MqttEndpointManagerTest {

    @Test
    void inboundMessageRawArchiveUsesResolvedVehicleIdentity() throws Exception {
        MqttInboundProperties.Endpoint endpoint = new MqttInboundProperties.Endpoint();
        endpoint.setName("endpoint-a");
        endpoint.setTopic("/ytforward/#");
        endpoint.setProfile("yutong");
        byte[] payload = """
                {"device":"YT-DEVICE-001","time":"20260622150000","data":{"METER_SPEED":31,"TOTAL_MILEAGE":1024}}
                """.trim().getBytes(StandardCharsets.UTF_8);
        InMemoryVehicleIdentityService identities = new InMemoryVehicleIdentityService();
        identities.bind(new VehicleIdentityBinding(
                ProtocolId.MQTT_YUTONG,
                "LNVIN000000MQTT01",
                "",
                "YT-DEVICE-001",
                ""));
        RecordingSink sink = new RecordingSink(2);
        DisruptorEventBus eventBus = new DisruptorEventBus(1024, "blocking", List.of(sink));
        AsyncBatchExecutor batchExecutor = new AsyncBatchExecutor(eventBus::publish);
        MqttProfileRegistry profileRegistry = new MqttProfileRegistry(List.of(
                new YutongMqttProfile(
                        new MqttPayloadParser(new ObjectMapper()),
                        new YutongEventMapper(identities))));
        Dispatcher dispatcher = new Dispatcher(
                registry(profileRegistry),
                new InterceptorChain(List.of()),
                new HandlerInvoker(),
                eventBus,
                batchExecutor);
        MqttEndpointManager manager = new MqttEndpointManager(
                new MqttInboundProperties(), profileRegistry, identities, dispatcher);

        invokeOnMessage(manager, endpoint, "/ytforward/vehicle/YT-DEVICE-001", payload);

        assertThat(sink.await()).isTrue();
        assertThat(sink.events).anySatisfy(event -> {
            assertThat(event).isInstanceOf(VehicleEvent.RawArchive.class);
            VehicleEvent.RawArchive archive = (VehicleEvent.RawArchive) event;
            assertThat(archive.vin()).isEqualTo("LNVIN000000MQTT01");
            assertThat(archive.metadata())
                    .containsEntry("vin", "LNVIN000000MQTT01")
                    .containsEntry("mqttDeviceId", "YT-DEVICE-001")
                    .containsEntry("identityResolved", "true")
                    .containsEntry("identitySource", "BOUND_DEVICE_ID");
        });

        batchExecutor.close();
        eventBus.close();
    }

    @Test
    void inboundMessageReusesResolvedIdentityForRawArchiveAndMappedEvents() throws Exception {
        MqttInboundProperties.Endpoint endpoint = new MqttInboundProperties.Endpoint();
        endpoint.setName("endpoint-a");
        endpoint.setTopic("/ytforward/#");
        endpoint.setProfile("yutong");
        byte[] payload = """
                {"device":"YT-DEVICE-001","time":"20260622150000","data":{"METER_SPEED":31,"TOTAL_MILEAGE":1024}}
                """.trim().getBytes(StandardCharsets.UTF_8);
        CountingIdentityResolver identities = new CountingIdentityResolver(new VehicleIdentity(
                "LNVIN000000MQTT01", true, VehicleIdentitySource.BOUND_DEVICE_ID));
        RecordingSink sink = new RecordingSink(2);
        DisruptorEventBus eventBus = new DisruptorEventBus(1024, "blocking", List.of(sink));
        AsyncBatchExecutor batchExecutor = new AsyncBatchExecutor(eventBus::publish);
        MqttProfileRegistry profileRegistry = new MqttProfileRegistry(List.of(
                new YutongMqttProfile(
                        new MqttPayloadParser(new ObjectMapper()),
                        new YutongEventMapper(identities))));
        Dispatcher dispatcher = new Dispatcher(
                registry(profileRegistry),
                new InterceptorChain(List.of()),
                new HandlerInvoker(),
                eventBus,
                batchExecutor);
        MqttEndpointManager manager = new MqttEndpointManager(
                new MqttInboundProperties(), profileRegistry, identities, dispatcher);

        invokeOnMessage(manager, endpoint, "/ytforward/vehicle/YT-DEVICE-001", payload);

        assertThat(sink.await()).isTrue();
        assertThat(identities.calls()).isEqualTo(1);
        assertThat(sink.events)
                .extracting(VehicleEvent::vin)
                .contains("LNVIN000000MQTT01");
        assertThat(sink.events).anySatisfy(event -> {
            assertThat(event).isInstanceOf(VehicleEvent.RawArchive.class);
            VehicleEvent.RawArchive archive = (VehicleEvent.RawArchive) event;
            assertThat(archive.parsedJson())
                    .contains("\"deviceId\":\"YT-DEVICE-001\"")
                    .doesNotContain("resolvedVin")
                    .doesNotContain("identityResolved")
                    .doesNotContain("identitySource")
                    .doesNotContain("identityErrorMessage");
        });

        batchExecutor.close();
        eventBus.close();
    }

    @Test
    void malformedInboundMessageArchivesParseFailureMetadataAndPassthrough() throws Exception {
        MqttInboundProperties.Endpoint endpoint = new MqttInboundProperties.Endpoint();
        endpoint.setName("endpoint-a");
        endpoint.setTopic("/ytforward/#");
        endpoint.setProfile("yutong");
        byte[] badPayload = "{bad-json".getBytes(StandardCharsets.UTF_8);
        RecordingSink sink = new RecordingSink(2);
        DisruptorEventBus eventBus = new DisruptorEventBus(1024, "blocking", List.of(sink));
        AsyncBatchExecutor batchExecutor = new AsyncBatchExecutor(eventBus::publish);
        MqttProfileRegistry profileRegistry = new MqttProfileRegistry(List.of(
                new YutongMqttProfile(
                        new MqttPayloadParser(new ObjectMapper()),
                        new YutongEventMapper(new InMemoryVehicleIdentityService()))));
        Dispatcher dispatcher = new Dispatcher(
                registry(profileRegistry),
                new InterceptorChain(List.of()),
                new HandlerInvoker(),
                eventBus,
                batchExecutor);
        MqttEndpointManager manager = new MqttEndpointManager(
                new MqttInboundProperties(), profileRegistry, new InMemoryVehicleIdentityService(), dispatcher);

        invokeOnMessage(manager, endpoint, "/ytforward/bad", badPayload);

        assertThat(sink.await()).isTrue();
        assertThat(sink.events).anySatisfy(event -> {
            assertThat(event).isInstanceOf(VehicleEvent.RawArchive.class);
            VehicleEvent.RawArchive archive = (VehicleEvent.RawArchive) event;
            assertThat(archive.source()).isEqualTo(ProtocolId.MQTT_YUTONG);
            assertThat(archive.rawBytes()).containsExactly(badPayload);
            assertThat(archive.metadata())
                    .containsEntry("endpoint", "endpoint-a")
                    .containsEntry("topic", "/ytforward/bad")
                    .containsEntry("profile", "yutong")
                    .containsEntry("parseError", "true")
                    .containsEntry("vin", "unknown");
            assertThat(archive.metadata().get("parseErrorMessage"))
                    .contains("mqtt payload parse failed");
        });
        assertThat(sink.events).anySatisfy(event -> {
            assertThat(event).isInstanceOf(VehicleEvent.Passthrough.class);
            VehicleEvent.Passthrough passthrough = (VehicleEvent.Passthrough) event;
            assertThat(passthrough.source()).isEqualTo(ProtocolId.MQTT_YUTONG);
            assertThat(passthrough.data()).containsExactly(badPayload);
            assertThat(passthrough.metadata())
                    .containsEntry("parseError", "true")
                    .containsEntry("endpoint", "endpoint-a")
                    .containsEntry("profile", "yutong");
        });

        batchExecutor.close();
        eventBus.close();
    }

    @Test
    void endpointInitializationFailureIsArchivedAndDispatchedAsPassthrough() throws Exception {
        MqttInboundProperties props = new MqttInboundProperties();
        MqttInboundProperties.Endpoint endpoint = new MqttInboundProperties.Endpoint();
        endpoint.setName("bad-endpoint");
        endpoint.setUri("://bad-uri");
        endpoint.setTopic("/vehicles/#");
        endpoint.setProfile("yutong");
        props.setEndpoints(List.of(endpoint));
        RecordingSink sink = new RecordingSink(2);
        DisruptorEventBus eventBus = new DisruptorEventBus(1024, "blocking", List.of(sink));
        AsyncBatchExecutor batchExecutor = new AsyncBatchExecutor(eventBus::publish);
        MqttProfileRegistry profileRegistry = new MqttProfileRegistry(List.of());
        Dispatcher dispatcher = new Dispatcher(
                registry(profileRegistry),
                new InterceptorChain(List.of()),
                new HandlerInvoker(),
                eventBus,
                batchExecutor);
        MqttEndpointManager manager = new MqttEndpointManager(
                props, profileRegistry, new InMemoryVehicleIdentityService(), dispatcher);

        manager.start();

        assertThat(sink.await()).isTrue();
        assertThat(sink.events).anySatisfy(event -> {
            assertThat(event).isInstanceOf(VehicleEvent.RawArchive.class);
            VehicleEvent.RawArchive archive = (VehicleEvent.RawArchive) event;
            assertThat(archive.source()).isEqualTo(ProtocolId.MQTT_YUTONG);
            assertThat(archive.metadata())
                    .containsEntry("endpoint", "bad-endpoint")
                    .containsEntry("profile", "yutong")
                    .containsEntry("operationalError", "true")
                    .containsEntry("phase", "init")
                    .containsEntry("identityResolved", "false")
                    .containsEntry("identitySource", "UNKNOWN");
        });
        assertThat(sink.events).anySatisfy(event -> {
            assertThat(event).isInstanceOf(VehicleEvent.Passthrough.class);
            VehicleEvent.Passthrough passthrough = (VehicleEvent.Passthrough) event;
            assertThat(passthrough.source()).isEqualTo(ProtocolId.MQTT_YUTONG);
            assertThat(passthrough.metadata())
                    .containsEntry("operationalError", "true")
                    .containsEntry("endpoint", "bad-endpoint")
                    .containsEntry("profile", "yutong")
                    .containsEntry("phase", "init");
        });

        batchExecutor.close();
        eventBus.close();
    }

    @Test
    void profileRuntimeFailureIsArchivedAndDispatchedAsPassthrough() throws Exception {
        MqttInboundProperties.Endpoint endpoint = new MqttInboundProperties.Endpoint();
        endpoint.setName("endpoint-a");
        endpoint.setTopic("/vehicles/#");
        endpoint.setProfile("broken");
        byte[] payload = "{\"device\":\"YT-DEVICE-001\"}".getBytes(StandardCharsets.UTF_8);
        RecordingSink sink = new RecordingSink(2);
        DisruptorEventBus eventBus = new DisruptorEventBus(1024, "blocking", List.of(sink));
        AsyncBatchExecutor batchExecutor = new AsyncBatchExecutor(eventBus::publish);
        MqttProfileRegistry profileRegistry = new MqttProfileRegistry(List.of(new BrokenProfile()));
        Dispatcher dispatcher = new Dispatcher(
                registry(profileRegistry),
                new InterceptorChain(List.of()),
                new HandlerInvoker(),
                eventBus,
                batchExecutor);
        MqttEndpointManager manager = new MqttEndpointManager(
                new MqttInboundProperties(), profileRegistry, new InMemoryVehicleIdentityService(), dispatcher);

        invokeOnMessage(manager, endpoint, "/vehicles/broken", payload);

        assertThat(sink.await()).isTrue();
        assertThat(sink.events).anySatisfy(event -> {
            assertThat(event).isInstanceOf(VehicleEvent.RawArchive.class);
            VehicleEvent.RawArchive archive = (VehicleEvent.RawArchive) event;
            assertThat(archive.source()).isEqualTo(ProtocolId.MQTT_YUTONG);
            assertThat(archive.rawBytes()).containsExactly(payload);
            assertThat(archive.metadata())
                    .containsEntry("endpoint", "endpoint-a")
                    .containsEntry("topic", "/vehicles/broken")
                    .containsEntry("profile", "broken")
                    .containsEntry("operationalError", "true")
                    .containsEntry("phase", "message")
                    .containsEntry("identityResolved", "false")
                    .containsEntry("identitySource", "UNKNOWN");
            assertThat(archive.metadata().get("reason")).contains("profile blew up");
        });
        assertThat(sink.events).anySatisfy(event -> {
            assertThat(event).isInstanceOf(VehicleEvent.Passthrough.class);
            VehicleEvent.Passthrough passthrough = (VehicleEvent.Passthrough) event;
            assertThat(passthrough.source()).isEqualTo(ProtocolId.MQTT_YUTONG);
            assertThat(passthrough.data()).containsExactly(payload);
            assertThat(passthrough.metadata())
                    .containsEntry("operationalError", "true")
                    .containsEntry("endpoint", "endpoint-a")
                    .containsEntry("profile", "broken")
                    .containsEntry("phase", "message");
        });

        batchExecutor.close();
        eventBus.close();
    }

    @Test
    void rejectsMissingIdentityResolver() throws Exception {
        MqttProfileRegistry profileRegistry = new MqttProfileRegistry(List.of());
        DisruptorEventBus eventBus = new DisruptorEventBus(1024, "blocking", List.of());
        AsyncBatchExecutor batchExecutor = new AsyncBatchExecutor(eventBus::publish);
        Dispatcher dispatcher = new Dispatcher(
                registry(profileRegistry),
                new InterceptorChain(List.of()),
                new HandlerInvoker(),
                eventBus,
                batchExecutor);

        assertThatThrownBy(() -> new MqttEndpointManager(
                new MqttInboundProperties(), profileRegistry, null, dispatcher))
                .isInstanceOf(IllegalArgumentException.class)
                .hasMessageContaining("identityResolver");

        batchExecutor.close();
        eventBus.close();
    }

    @Test
    void identityResolverFailureStillArchivesAndDispatchesRealtimeWithUnknownIdentity() throws Exception {
        MqttInboundProperties.Endpoint endpoint = new MqttInboundProperties.Endpoint();
        endpoint.setName("endpoint-a");
        endpoint.setTopic("/ytforward/#");
        endpoint.setProfile("yutong");
        byte[] payload = """
                {"device":"YT-DEVICE-001","time":"20260622150000","data":{"METER_SPEED":31,"TOTAL_MILEAGE":1024}}
                """.trim().getBytes(StandardCharsets.UTF_8);
        var failingIdentity = (com.lingniu.ingest.identity.VehicleIdentityResolver) lookup -> {
            throw new IllegalStateException("identity backend unavailable");
        };
        RecordingSink sink = new RecordingSink(2);
        DisruptorEventBus eventBus = new DisruptorEventBus(1024, "blocking", List.of(sink));
        AsyncBatchExecutor batchExecutor = new AsyncBatchExecutor(eventBus::publish);
        MqttProfileRegistry profileRegistry = new MqttProfileRegistry(List.of(
                new YutongMqttProfile(
                        new MqttPayloadParser(new ObjectMapper()),
                        new YutongEventMapper(failingIdentity))));
        Dispatcher dispatcher = new Dispatcher(
                registry(profileRegistry),
                new InterceptorChain(List.of()),
                new HandlerInvoker(),
                eventBus,
                batchExecutor);
        MqttEndpointManager manager = new MqttEndpointManager(
                new MqttInboundProperties(), profileRegistry, failingIdentity, dispatcher);

        invokeOnMessage(manager, endpoint, "/ytforward/vehicle/YT-DEVICE-001", payload);

        assertThat(sink.await()).isTrue();
        assertThat(sink.events).anySatisfy(event -> {
            assertThat(event).isInstanceOf(VehicleEvent.RawArchive.class);
            VehicleEvent.RawArchive archive = (VehicleEvent.RawArchive) event;
            assertThat(archive.source()).isEqualTo(ProtocolId.MQTT_YUTONG);
            assertThat(archive.vin()).isEqualTo("unknown");
            assertThat(archive.rawBytes()).containsExactly(payload);
            assertThat(archive.metadata())
                    .containsEntry("vin", "unknown")
                    .containsEntry("mqttDeviceId", "YT-DEVICE-001")
                    .containsEntry("identityResolved", "false")
                    .containsEntry("identitySource", "UNKNOWN")
                    .containsEntry("identityError", "true")
                    .containsEntry("identityErrorMessage", "identity backend unavailable");
        });
        assertThat(sink.events).anySatisfy(event -> {
            assertThat(event).isInstanceOf(VehicleEvent.Realtime.class);
            assertThat(event.vin()).isEqualTo("unknown");
            assertThat(event.metadata())
                    .containsEntry("vin", "unknown")
                    .containsEntry("mqttDeviceId", "YT-DEVICE-001")
                    .containsEntry("identityResolved", "false")
                    .containsEntry("identitySource", "UNKNOWN")
                    .containsEntry("identityError", "true")
                    .containsEntry("identityErrorMessage", "identity backend unavailable");
        });

        batchExecutor.close();
        eventBus.close();
    }

    private static HandlerRegistry registry(MqttProfileRegistry profileRegistry) throws NoSuchMethodException {
        HandlerRegistry registry = new HandlerRegistry();
        MqttRealtimeHandler handler = new MqttRealtimeHandler(profileRegistry);
        registry.register(new HandlerDefinition(
                ProtocolId.MQTT_YUTONG,
                0,
                0,
                "test mqtt",
                handler,
                MqttRealtimeHandler.class.getMethod("onMqtt", MqttPayload.class),
                MqttPayload.class,
                null,
                null,
                null));
        return registry;
    }

    private static void invokeOnMessage(MqttEndpointManager manager,
                                        MqttInboundProperties.Endpoint endpoint,
                                        String topic,
                                        byte[] payload) throws Exception {
        Method method = MqttEndpointManager.class.getDeclaredMethod(
                "onMessage", MqttInboundProperties.Endpoint.class, String.class, byte[].class);
        method.setAccessible(true);
        method.invoke(manager, endpoint, topic, payload);
    }

    private static final class RecordingSink implements EventSink {
        private final List<VehicleEvent> events = new CopyOnWriteArrayList<>();
        private final CountDownLatch latch;

        private RecordingSink(int expectedEvents) {
            this.latch = new CountDownLatch(expectedEvents);
        }

        @Override
        public String name() {
            return "recording";
        }

        @Override
        public CompletableFuture<Void> publish(VehicleEvent event) {
            events.add(event);
            latch.countDown();
            return CompletableFuture.completedFuture(null);
        }

        private boolean await() throws InterruptedException {
            return latch.await(3, TimeUnit.SECONDS);
        }
    }

    private static final class CountingIdentityResolver implements VehicleIdentityResolver {
        private final VehicleIdentity identity;
        private final AtomicInteger calls = new AtomicInteger();

        private CountingIdentityResolver(VehicleIdentity identity) {
            this.identity = identity;
        }

        @Override
        public VehicleIdentity resolve(VehicleIdentityLookup lookup) {
            calls.incrementAndGet();
            return identity;
        }

        private int calls() {
            return calls.get();
        }
    }

    private static final class BrokenProfile implements com.lingniu.ingest.inbound.mqtt.profile.MqttProfile {
        @Override
        public String name() {
            return "broken";
        }

        @Override
        public MqttPayload parse(String endpointName, String topic, byte[] bytes) {
            throw new IllegalStateException("profile blew up");
        }

        @Override
        public List<VehicleEvent> toEvents(MqttPayload payload) {
            return List.of();
        }
    }
}
