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.VehicleEvent;
import com.lingniu.ingest.api.pipeline.IngestContext;
import com.lingniu.ingest.api.pipeline.IngestInterceptor;
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.List;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionException;
import java.util.concurrent.CompletionStage;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;

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

class DispatcherDurableAckBoundaryTest {

    @Test
    void dispatchAndAwaitFailsWhenNoKafkaSinkAcceptsEvent() throws Exception {
        EventSink rawArchiveSink = new NamedSink("raw-archive", true);

        try (Harness harness = new Harness(registryWithHeartbeatHandler(), List.of(rawArchiveSink), List.of())) {
            CompletionStage<Void> future = harness.dispatcher.dispatchAndAwait(frame(0x07, "payload"));

            assertThatThrownBy(() -> future.toCompletableFuture().join())
                    .isInstanceOf(CompletionException.class)
                    .hasRootCauseInstanceOf(IllegalStateException.class)
                    .hasMessageContaining("kafka");
        }
    }

    @Test
    void dispatchAndAwaitNotifiesInterceptorWhenPlanCapturesFailure() throws Exception {
        CapturingInterceptor interceptor = new CapturingInterceptor();

        try (Harness harness = new Harness(registryWithThrowingHandler(), List.of(new NamedSink("kafka", true)),
                List.of(interceptor))) {
            CompletionStage<Void> future = harness.dispatcher.dispatchAndAwait(frame(0x08, "payload"));

            assertThatThrownBy(() -> future.toCompletableFuture().join())
                    .isInstanceOf(CompletionException.class)
                    .hasRootCauseInstanceOf(IllegalStateException.class)
                    .rootCause()
                    .hasMessageContaining("handler failed");
            assertThat(interceptor.error.get())
                    .isInstanceOf(RuntimeException.class)
                    .hasMessageContaining("Handler threw exception");
        }
    }

    @Test
    void dispatchAndAwaitFailsFastForAsyncBatchHandler() throws Exception {
        NamedSink kafka = new NamedSink("kafka", true);
        try (Harness harness = new Harness(registryWithAsyncBatchHandler(), List.of(kafka), List.of())) {
            CompletionStage<Void> future = harness.dispatcher.dispatchAndAwait(
                    frame(0x09, "payload", new byte[]{0x23, 0x23, 0x09}));

            assertThatThrownBy(() -> future.toCompletableFuture().join())
                    .isInstanceOf(CompletionException.class)
                    .hasRootCauseInstanceOf(IllegalStateException.class)
                    .hasMessageContaining("@AsyncBatch");
            assertThat(kafka.published.get()).isZero();
        }
    }

    private static HandlerRegistry registryWithHeartbeatHandler() throws NoSuchMethodException {
        HeartbeatHandler bean = new HeartbeatHandler();
        Method method = HeartbeatHandler.class.getDeclaredMethod("onHeartbeat", Object.class);
        return registry(bean, method, 0x07, method.getAnnotation(AsyncBatch.class));
    }

    private static HandlerRegistry registryWithThrowingHandler() throws NoSuchMethodException {
        ThrowingHandler bean = new ThrowingHandler();
        Method method = ThrowingHandler.class.getDeclaredMethod("onFrame", Object.class);
        return registry(bean, method, 0x08, method.getAnnotation(AsyncBatch.class));
    }

    private static HandlerRegistry registryWithAsyncBatchHandler() throws NoSuchMethodException {
        AsyncHandler bean = new AsyncHandler();
        Method method = AsyncHandler.class.getDeclaredMethod("onBatch", List.class);
        return registry(bean, method, 0x09, method.getAnnotation(AsyncBatch.class));
    }

    private static HandlerRegistry registry(Object bean, Method method, int command, AsyncBatch asyncBatch) {
        HandlerRegistry registry = new HandlerRegistry();
        registry.register(new HandlerDefinition(
                ProtocolId.GB32960,
                command,
                0,
                method.getName(),
                bean,
                method,
                Object.class,
                null,
                null,
                asyncBatch));
        return registry;
    }

    private static RawFrame frame(int command, Object payload) {
        return frame(command, payload, null);
    }

    private static RawFrame frame(int command, Object payload, byte[] rawBytes) {
        return new RawFrame(
                ProtocolId.GB32960,
                command,
                0,
                payload,
                rawBytes,
                Map.of("vin", "VIN001"),
                Instant.parse("2026-06-23T10:00:00Z"));
    }

    private static VehicleEvent.Heartbeat event(String id) {
        Instant now = Instant.parse("2026-06-23T10:00:00Z");
        return new VehicleEvent.Heartbeat(id, "VIN001", ProtocolId.GB32960, now, now, "trace-" + id, Map.of());
    }

    private static final class Harness implements AutoCloseable {
        private final DisruptorEventBus eventBus;
        private final AsyncBatchExecutor batchExecutor;
        private final Dispatcher dispatcher;

        private Harness(HandlerRegistry registry, List<EventSink> sinks, List<IngestInterceptor> interceptors) {
            this.eventBus = new DisruptorEventBus(1024, "blocking", sinks);
            this.batchExecutor = new AsyncBatchExecutor(eventBus::publish);
            this.dispatcher = new Dispatcher(
                    registry,
                    new InterceptorChain(interceptors),
                    new HandlerInvoker(),
                    eventBus,
                    batchExecutor);
        }

        @Override
        public void close() {
            batchExecutor.close();
            eventBus.close();
        }
    }

    private static final class NamedSink implements EventSink {
        private final String name;
        private final boolean accepts;
        private final AtomicInteger published = new AtomicInteger();

        private NamedSink(String name, boolean accepts) {
            this.name = name;
            this.accepts = accepts;
        }

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

        @Override
        public CompletableFuture<Void> publish(VehicleEvent event) {
            published.incrementAndGet();
            return CompletableFuture.completedFuture(null);
        }

        @Override
        public boolean accepts(VehicleEvent event) {
            return accepts;
        }
    }

    private static final class CapturingInterceptor implements IngestInterceptor {
        private final AtomicReference<Throwable> error = new AtomicReference<>();

        @Override
        public void onError(Throwable error, IngestContext ctx) {
            this.error.set(error);
        }
    }

    @ProtocolHandler(protocol = ProtocolId.GB32960)
    static final class HeartbeatHandler {
        @MessageMapping(command = 0x07)
        VehicleEvent.Heartbeat onHeartbeat(Object ignored) {
            return event("heartbeat-1");
        }
    }

    @ProtocolHandler(protocol = ProtocolId.GB32960)
    static final class ThrowingHandler {
        @MessageMapping(command = 0x08)
        VehicleEvent.Heartbeat onFrame(Object ignored) {
            throw new IllegalStateException("handler failed");
        }
    }

    @ProtocolHandler(protocol = ProtocolId.GB32960)
    public static final class AsyncHandler {
        @MessageMapping(command = 0x09)
        @AsyncBatch(size = 2, waitMs = 10, poolSize = 1)
        public List<VehicleEvent> onBatch(List<Object> ignored) {
            return List.of(event("async-1"));
        }
    }
}
