package com.lingniu.ingest.core.dispatcher;

import com.lingniu.ingest.api.ProtocolId;
import com.lingniu.ingest.api.annotation.AsyncBatch;
import com.lingniu.ingest.api.annotation.MessageMapping;
import com.lingniu.ingest.api.annotation.ProtocolHandler;
import com.lingniu.ingest.api.event.LocationPayload;
import com.lingniu.ingest.api.event.VehicleEvent;
import com.lingniu.ingest.api.pipeline.IngestContext;
import com.lingniu.ingest.api.pipeline.RawFrame;
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.pipeline.InterceptorChain;
import org.junit.jupiter.api.Test;

import java.lang.reflect.Method;
import java.time.Instant;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CompletableFuture;

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

class DispatcherRawArchiveMetadataTest {

    @Test
    void enrichesBusinessEventsWithRawArchiveLookupMetadata() throws Exception {
        CollectingSink sink = new CollectingSink();
        DisruptorEventBus eventBus = new DisruptorEventBus(1024, "blocking", List.of(sink));
        AsyncBatchExecutor batchExecutor = new AsyncBatchExecutor(eventBus::publish);
        try {
            Dispatcher dispatcher = new Dispatcher(
                    registryWithLocationHandler(),
                    new InterceptorChain(List.of()),
                    new HandlerInvoker(),
                    eventBus,
                    batchExecutor);

            dispatcher.dispatch(new RawFrame(
                    ProtocolId.JT808,
                    0x0200,
                    0,
                    new SamplePayload("payload-0200", 7),
                    new byte[]{0x7e, 0x01, 0x7e},
                    Map.of("vin", "VIN001", "peer", "127.0.0.1:10001"),
                    Instant.parse("2026-06-22T08:00:00Z")));

            VehicleEvent.RawArchive raw = awaitEvent(sink, VehicleEvent.RawArchive.class);
            VehicleEvent.Location location = awaitEvent(sink, VehicleEvent.Location.class);

            String rawArchiveKey = raw.metadata().get("rawArchiveKey");
            assertThat(rawArchiveKey).isNotBlank();
            assertThat(raw.parsedJson()).contains("\"name\":\"payload-0200\"");
            assertThat(raw.metadata()).doesNotContainKey("rawArchiveParsedJson");
            assertThat(raw.eventId()).containsOnlyDigits();
            assertThat(rawArchiveKey).endsWith("/" + raw.eventId() + ".bin");
            assertThat(location.metadata())
                    .containsEntry("rawArchiveEventId", raw.eventId())
                    .containsEntry("rawArchiveKey", rawArchiveKey)
                    .containsEntry("rawArchiveUri", "archive://" + rawArchiveKey);
        } finally {
            batchExecutor.close();
            eventBus.close();
        }
    }

    @Test
    void enrichesAsyncBatchEventsWithTheirRawArchiveLookupMetadata() throws Exception {
        CollectingSink sink = new CollectingSink();
        DisruptorEventBus eventBus = new DisruptorEventBus(1024, "blocking", List.of(sink));
        AsyncBatchExecutor batchExecutor = new AsyncBatchExecutor(eventBus::publish);
        try {
            Dispatcher dispatcher = new Dispatcher(
                    registryWithAsyncLocationHandler(),
                    new InterceptorChain(List.of()),
                    new HandlerInvoker(),
                    eventBus,
                    batchExecutor);

            dispatcher.dispatch(new RawFrame(
                    ProtocolId.JT808,
                    0x0201,
                    0,
                    "payload-1",
                    new byte[]{0x7e, 0x02, 0x7e},
                    Map.of("vin", "VIN002"),
                    Instant.parse("2026-06-22T08:01:00Z")));

            VehicleEvent.RawArchive raw = awaitEvent(sink, VehicleEvent.RawArchive.class);
            VehicleEvent.Location location = awaitLocationEvent(sink, "async-location-1");

            String rawArchiveKey = raw.metadata().get("rawArchiveKey");
            assertThat(location.metadata())
                    .containsEntry("rawArchiveEventId", raw.eventId())
                    .containsEntry("rawArchiveKey", rawArchiveKey)
                    .containsEntry("rawArchiveUri", "archive://" + rawArchiveKey);
        } finally {
            batchExecutor.close();
            eventBus.close();
        }
    }

    private static HandlerRegistry registryWithLocationHandler() throws NoSuchMethodException {
        LocationHandler bean = new LocationHandler();
        Method method = LocationHandler.class.getDeclaredMethod("onLocation", SamplePayload.class);
        HandlerRegistry registry = new HandlerRegistry();
        registry.register(new HandlerDefinition(
                ProtocolId.JT808,
                0x0200,
                0,
                "test-location",
                bean,
                method,
                Object.class,
                null,
                null,
                null));
        return registry;
    }

    private static HandlerRegistry registryWithAsyncLocationHandler() throws NoSuchMethodException {
        AsyncLocationHandler bean = new AsyncLocationHandler();
        Method method = AsyncLocationHandler.class.getDeclaredMethod("onLocationBatch", List.class);
        HandlerRegistry registry = new HandlerRegistry();
        registry.register(new HandlerDefinition(
                ProtocolId.JT808,
                0x0201,
                0,
                "test-async-location",
                bean,
                method,
                Object.class,
                null,
                null,
                method.getAnnotation(AsyncBatch.class)));
        return registry;
    }

    private static <T extends VehicleEvent> T awaitEvent(CollectingSink sink, Class<T> type) throws InterruptedException {
        long deadline = System.currentTimeMillis() + 3000;
        while (System.currentTimeMillis() < deadline) {
            synchronized (sink.events) {
                for (VehicleEvent event : sink.events) {
                    if (type.isInstance(event)) {
                        return type.cast(event);
                    }
                }
            }
            Thread.sleep(25);
        }
        throw new AssertionError("event not published: " + type.getSimpleName() + ", actual=" + sink.events);
    }

    private static VehicleEvent.Location awaitLocationEvent(CollectingSink sink, String eventId) throws InterruptedException {
        long deadline = System.currentTimeMillis() + 3000;
        while (System.currentTimeMillis() < deadline) {
            synchronized (sink.events) {
                for (VehicleEvent event : sink.events) {
                    if (event instanceof VehicleEvent.Location location && location.eventId().equals(eventId)) {
                        return location;
                    }
                }
            }
            Thread.sleep(25);
        }
        throw new AssertionError("location event not published: " + eventId + ", actual=" + sink.events);
    }

    @ProtocolHandler(protocol = ProtocolId.JT808)
    static final class LocationHandler {
        @MessageMapping(command = 0x0200)
        VehicleEvent.Location onLocation(SamplePayload ignored) {
            return new VehicleEvent.Location(
                    "location-1",
                    "VIN001",
                    ProtocolId.JT808,
                    Instant.parse("2026-06-22T08:00:00Z"),
                    Instant.parse("2026-06-22T08:00:01Z"),
                    "trace-location",
                    Map.of(),
                    new LocationPayload(113.12, 23.45, 10.0, 60.0, 90.0, 0, 1));
        }
    }

    record SamplePayload(String name, int value) {
    }

    @ProtocolHandler(protocol = ProtocolId.JT808)
    public static final class AsyncLocationHandler {
        @MessageMapping(command = 0x0201)
        @AsyncBatch(size = 2, waitMs = 10, poolSize = 1)
        public List<VehicleEvent> onLocationBatch(List<Object> batch) {
            return List.of(new VehicleEvent.Location(
                    "async-location-1",
                    "VIN002",
                    ProtocolId.JT808,
                    Instant.parse("2026-06-22T08:01:00Z"),
                    Instant.parse("2026-06-22T08:01:01Z"),
                    "trace-async-location",
                    Map.of(),
                    new LocationPayload(113.12, 23.45, 10.0, 60.0, 90.0, 0, 1)));
        }
    }

    private static final class CollectingSink implements EventSink {
        private final List<VehicleEvent> events = new ArrayList<>();

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

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