package com.lingniu.ingest.core.concurrency;

import com.lingniu.ingest.api.ProtocolId;
import com.lingniu.ingest.api.event.VehicleEvent;
import com.lingniu.ingest.api.sink.EventSink;
import org.junit.jupiter.api.Test;

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.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicLong;

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

class DisruptorEventBusAwaitTest {

    @Test
    void publishAndAwaitDoesNotInvokeSinkPublishOnCallerThread() throws Exception {
        long callerThreadId = Thread.currentThread().threadId();
        CapturingSink sink = new CapturingSink("kafka");

        try (DisruptorEventBus bus = new DisruptorEventBus(1024, "blocking", java.util.List.of(sink))) {
            bus.publishAndAwait(event(), "kafka").get(3, TimeUnit.SECONDS);
        }

        assertThat(sink.publishThreadId.get()).isNotEqualTo(callerThreadId);
    }

    @Test
    void publishAndAwaitDoesNotInvokeOptionalSinksWhenRequiredSinkIsAbsent() throws Exception {
        CapturingSink optionalSink = new CapturingSink("raw-archive");

        try (DisruptorEventBus bus = new DisruptorEventBus(1024, "blocking", List.of(optionalSink))) {
            assertThatThrownBy(() -> bus.publishAndAwait(event(), "kafka").join())
                    .isInstanceOf(CompletionException.class)
                    .hasRootCauseInstanceOf(IllegalStateException.class)
                    .hasMessageContaining("kafka");

            assertThat(optionalSink.published.await(200, TimeUnit.MILLISECONDS)).isFalse();
        }
    }

    @Test
    void publishFansOutEveryEventToEveryAcceptingSink() throws Exception {
        int count = 1000;
        CountingSink rawArchive = new CountingSink("raw-archive", count);
        CountingSink kafka = new CountingSink("kafka", count);

        try (DisruptorEventBus bus = new DisruptorEventBus(1024, "blocking", List.of(rawArchive, kafka))) {
            for (int i = 0; i < count; i++) {
                bus.publish(event("heartbeat-" + i));
            }

            assertThat(rawArchive.received.await(3, TimeUnit.SECONDS)).isTrue();
            assertThat(kafka.received.await(3, TimeUnit.SECONDS)).isTrue();
        }

        assertThat(rawArchive.count.get()).isEqualTo(count);
        assertThat(kafka.count.get()).isEqualTo(count);
    }

    private static VehicleEvent.Heartbeat event() {
        return event("heartbeat-1");
    }

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

    private static final class CapturingSink implements EventSink {
        private final String name;
        private final AtomicLong publishThreadId = new AtomicLong(-1);
        private final CountDownLatch published = new CountDownLatch(1);

        private CapturingSink(String name) {
            this.name = name;
        }

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

        @Override
        public CompletableFuture<Void> publish(VehicleEvent event) {
            publishThreadId.set(Thread.currentThread().threadId());
            published.countDown();
            return CompletableFuture.completedFuture(null);
        }
    }

    private static final class CountingSink implements EventSink {
        private final String name;
        private final AtomicLong count = new AtomicLong();
        private final CountDownLatch received;

        private CountingSink(String name, int expected) {
            this.name = name;
            this.received = new CountDownLatch(expected);
        }

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

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