package com.lingniu.ingest.protocol.gb32960.inbound;

import com.lingniu.ingest.api.event.VehicleEvent;
import com.lingniu.ingest.api.sink.EventSink;
import com.lingniu.ingest.codec.BccChecksum;
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.HandlerInvoker;
import com.lingniu.ingest.core.dispatcher.HandlerRegistry;
import com.lingniu.ingest.core.pipeline.InterceptorChain;
import com.lingniu.ingest.protocol.gb32960.auth.Gb32960PlatformAuthorizer;
import com.lingniu.ingest.protocol.gb32960.auth.Gb32960VinAuthorizer;
import com.lingniu.ingest.protocol.gb32960.codec.Gb32960BodyParser;
import com.lingniu.ingest.protocol.gb32960.codec.Gb32960DecoderTest;
import com.lingniu.ingest.protocol.gb32960.codec.Gb32960MessageDecoder;
import com.lingniu.ingest.protocol.gb32960.codec.InfoBlockParserRegistry;
import com.lingniu.ingest.protocol.gb32960.codec.parser.v2016.PositionV2016BlockParser;
import com.lingniu.ingest.protocol.gb32960.codec.parser.v2016.VehicleV2016BlockParser;
import com.lingniu.ingest.protocol.gb32960.config.Gb32960Properties;
import io.netty.buffer.ByteBuf;
import io.netty.channel.embedded.EmbeddedChannel;
import org.junit.jupiter.api.Test;

import java.io.ByteArrayOutputStream;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CompletableFuture;

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

class Gb32960ChannelHandlerAckBoundaryTest {

    @Test
    void realtimeReportAckWaitsForDispatchFutureCompletion() {
        try (DispatchHarness dispatch = new DispatchHarness()) {
            EmbeddedChannel channel = new EmbeddedChannel(handlerWith(dispatch.dispatcher()));

            assertThat(channel.writeInbound(Gb32960DecoderTest.buildRealtimeFrame("LTEST000000000001"))).isFalse();
            assertThat((Object) channel.readOutbound()).isNull();

            dispatch.awaitPublishCount(1);
            dispatch.completeDurability();

            ByteBuf ack = awaitOutbound(channel);
            assertThat(ack).isNotNull();
            assertThat(ack.getUnsignedByte(2)).isEqualTo((short) 0x02);
            assertThat(ack.getUnsignedByte(3)).isEqualTo((short) 0x01);
            ack.release();
            channel.finishAndReleaseAll();
        }
    }

    @Test
    void realtimeReportAckIsNotWrittenWhenDispatchFutureFails() {
        try (DispatchHarness dispatch = new DispatchHarness()) {
            EmbeddedChannel channel = new EmbeddedChannel(handlerWith(dispatch.dispatcher()));

            assertThat(channel.writeInbound(Gb32960DecoderTest.buildRealtimeFrame("LTEST000000000001"))).isFalse();

            dispatch.awaitPublishCount(1);
            dispatch.failDurability();

            assertThat((Object) channel.readOutbound()).isNull();
            assertThat(awaitInactive(channel)).isTrue();
            channel.finishAndReleaseAll();
        }
    }

    @Test
    void durableAcksAreWrittenInInboundOrderWhenLaterDispatchCompletesFirst() {
        try (DispatchHarness dispatch = new DispatchHarness()) {
            EmbeddedChannel channel = new EmbeddedChannel(handlerWith(dispatch.dispatcher()));

            assertThat(channel.writeInbound(Gb32960DecoderTest.buildRealtimeFrame("LTEST000000000001"))).isFalse();
            assertThat(channel.writeInbound(buildHeartbeatFrame("LTEST000000000001"))).isFalse();
            assertThat((Object) channel.readOutbound()).isNull();

            dispatch.awaitPublishCount(2);
            dispatch.completeDurability(1);
            channel.runPendingTasks();
            assertThat((Object) channel.readOutbound()).isNull();

            dispatch.completeDurability(0);

            ByteBuf firstAck = awaitOutbound(channel);
            ByteBuf secondAck = awaitOutbound(channel);
            assertThat(firstAck).isNotNull();
            assertThat(secondAck).isNotNull();
            assertThat(firstAck.getUnsignedByte(2)).isEqualTo((short) 0x02);
            assertThat(secondAck.getUnsignedByte(2)).isEqualTo((short) 0x07);
            firstAck.release();
            secondAck.release();
            channel.finishAndReleaseAll();
        }
    }

    private static Gb32960ChannelHandler handlerWith(Dispatcher dispatcher) {
        return new Gb32960ChannelHandler(
                decoder(),
                dispatcher,
                accessService(),
                new Gb32960AckService(),
                new Gb32960FrameDiagnostics(16),
                new Gb32960ConnectionDiagnostics());
    }

    private static Gb32960MessageDecoder decoder() {
        return new Gb32960MessageDecoder(new Gb32960BodyParser(new InfoBlockParserRegistry(List.of(
                new VehicleV2016BlockParser(),
                new PositionV2016BlockParser()))));
    }

    private static Gb32960AccessService accessService() {
        Gb32960Properties.Auth auth = new Gb32960Properties.Auth();
        auth.setEnabled(false);
        return new Gb32960AccessService(
                new Gb32960VinAuthorizer(auth),
                new Gb32960PlatformAuthorizer(auth.getPlatforms()));
    }

    private static byte[] buildHeartbeatFrame(String vin) {
        ByteArrayOutputStream out = new ByteArrayOutputStream();
        out.write(0x23);
        out.write(0x23);
        out.write(0x07);
        out.write(0xFE);
        byte[] vinBytes = vin.getBytes(StandardCharsets.US_ASCII);
        out.write(vinBytes, 0, 17);
        out.write(0x01);
        out.write(0x00);
        out.write(0x00);

        byte[] almost = out.toByteArray();
        out.write(BccChecksum.compute(almost, 2, almost.length - 2) & 0xFF);
        return out.toByteArray();
    }

    private static ByteBuf awaitOutbound(EmbeddedChannel channel) {
        long deadline = System.currentTimeMillis() + 3000;
        while (System.currentTimeMillis() < deadline) {
            channel.runPendingTasks();
            ByteBuf outbound = channel.readOutbound();
            if (outbound != null) {
                return outbound;
            }
            Thread.onSpinWait();
        }
        return null;
    }

    private static boolean awaitInactive(EmbeddedChannel channel) {
        long deadline = System.currentTimeMillis() + 3000;
        while (System.currentTimeMillis() < deadline) {
            channel.runPendingTasks();
            if (!channel.isActive()) {
                return true;
            }
            Thread.onSpinWait();
        }
        return false;
    }

    private static final class DispatchHarness implements AutoCloseable {
        private final ControlledSink sink = new ControlledSink();
        private final DisruptorEventBus eventBus = new DisruptorEventBus(1024, "blocking", List.of(sink));
        private final AsyncBatchExecutor batchExecutor = new AsyncBatchExecutor(eventBus::publish);
        private final Dispatcher dispatcher = new Dispatcher(
                new HandlerRegistry(),
                new InterceptorChain(List.of()),
                new HandlerInvoker(),
                eventBus,
                batchExecutor);

        Dispatcher dispatcher() {
            return dispatcher;
        }

        void completeDurability() {
            completeDurability(0);
        }

        void completeDurability(int index) {
            sink.future(index).complete(null);
        }

        void failDurability() {
            sink.future(0).completeExceptionally(new IllegalStateException("durability failed"));
        }

        void awaitPublishCount(int count) {
            long deadline = System.currentTimeMillis() + 3000;
            while (System.currentTimeMillis() < deadline) {
                synchronized (sink.futures) {
                    if (sink.futures.size() >= count) {
                        return;
                    }
                }
                Thread.onSpinWait();
            }
            throw new AssertionError("expected " + count + " sink publishes, actual=" + sink.futures.size());
        }

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

    private static final class ControlledSink implements EventSink {
        private final List<CompletableFuture<Void>> futures = new ArrayList<>();

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

        @Override
        public CompletableFuture<Void> publish(VehicleEvent event) {
            CompletableFuture<Void> future = new CompletableFuture<>();
            synchronized (futures) {
                futures.add(future);
            }
            return future;
        }

        private CompletableFuture<Void> future(int index) {
            synchronized (futures) {
                return futures.get(index);
            }
        }
    }
}
