package com.zb.mom.ww.mxgateway.client;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;

import com.google.protobuf.Timestamp;
import galaxy_repository.v1.GalaxyRepositoryGrpc;
import galaxy_repository.v1.GalaxyRepositoryOuterClass.DeployEvent;
import galaxy_repository.v1.GalaxyRepositoryOuterClass.DiscoverHierarchyReply;
import galaxy_repository.v1.GalaxyRepositoryOuterClass.DiscoverHierarchyRequest;
import galaxy_repository.v1.GalaxyRepositoryOuterClass.GalaxyAttribute;
import galaxy_repository.v1.GalaxyRepositoryOuterClass.GalaxyObject;
import galaxy_repository.v1.GalaxyRepositoryOuterClass.GetLastDeployTimeReply;
import galaxy_repository.v1.GalaxyRepositoryOuterClass.GetLastDeployTimeRequest;
import galaxy_repository.v1.GalaxyRepositoryOuterClass.TestConnectionReply;
import galaxy_repository.v1.GalaxyRepositoryOuterClass.TestConnectionRequest;
import galaxy_repository.v1.GalaxyRepositoryOuterClass.WatchDeployEventsRequest;
import io.grpc.ManagedChannel;
import io.grpc.Metadata;
import io.grpc.Server;
import io.grpc.ServerCall;
import io.grpc.ServerCallHandler;
import io.grpc.ServerInterceptor;
import io.grpc.inprocess.InProcessChannelBuilder;
import io.grpc.inprocess.InProcessServerBuilder;
import io.grpc.stub.ClientCallStreamObserver;
import io.grpc.stub.ClientResponseObserver;
import io.grpc.stub.StreamObserver;
import java.time.Duration;
import java.time.Instant;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
import org.junit.jupiter.api.Test;

final class GalaxyRepositoryClientTests {
    @Test
    void testConnectionReturnsOkAndSendsAuthMetadata() throws Exception {
        AtomicReference<String> authorization = new AtomicReference<>();
        TestService service = new TestService() {
            @Override
            public void testConnection(
                    TestConnectionRequest request, StreamObserver<TestConnectionReply> responseObserver) {
                responseObserver.onNext(TestConnectionReply.newBuilder().setOk(true).build());
                responseObserver.onCompleted();
            }
        };

        try (InProcessGalaxy g = InProcessGalaxy.start(service, authorization);
                GalaxyRepositoryClient client = g.client("mxgw_galaxy_secret")) {
            assertTrue(client.testConnection());
            assertEquals("Bearer mxgw_galaxy_secret", authorization.get());
        }
    }

    @Test
    void getLastDeployTimeReturnsEmptyWhenPresentFalse() throws Exception {
        TestService service = new TestService() {
            @Override
            public void getLastDeployTime(
                    GetLastDeployTimeRequest request, StreamObserver<GetLastDeployTimeReply> responseObserver) {
                responseObserver.onNext(
                        GetLastDeployTimeReply.newBuilder().setPresent(false).build());
                responseObserver.onCompleted();
            }
        };

        try (InProcessGalaxy g = InProcessGalaxy.start(service, new AtomicReference<>());
                GalaxyRepositoryClient client = g.client("")) {
            Optional<Instant> result = client.getLastDeployTime();
            assertFalse(result.isPresent());
        }
    }

    @Test
    void getLastDeployTimeReturnsInstantWhenPresent() throws Exception {
        Timestamp expected = Timestamp.newBuilder().setSeconds(1_700_000_000L).setNanos(123_000_000).build();
        TestService service = new TestService() {
            @Override
            public void getLastDeployTime(
                    GetLastDeployTimeRequest request, StreamObserver<GetLastDeployTimeReply> responseObserver) {
                responseObserver.onNext(GetLastDeployTimeReply.newBuilder()
                        .setPresent(true)
                        .setTimeOfLastDeploy(expected)
                        .build());
                responseObserver.onCompleted();
            }
        };

        try (InProcessGalaxy g = InProcessGalaxy.start(service, new AtomicReference<>());
                GalaxyRepositoryClient client = g.client("")) {
            Optional<Instant> result = client.getLastDeployTime();
            assertTrue(result.isPresent());
            assertEquals(Instant.ofEpochSecond(1_700_000_000L, 123_000_000), result.get());
        }
    }

    @Test
    void discoverHierarchyReturnsObjectsAndAttributes() throws Exception {
        AtomicReference<DiscoverHierarchyRequest> firstRequest = new AtomicReference<>();
        AtomicReference<DiscoverHierarchyRequest> secondRequest = new AtomicReference<>();
        TestService service = new TestService() {
            @Override
            public void discoverHierarchy(
                    DiscoverHierarchyRequest request, StreamObserver<DiscoverHierarchyReply> responseObserver) {
                if (request.getPageToken().isEmpty()) {
                    firstRequest.set(request);
                    responseObserver.onNext(DiscoverHierarchyReply.newBuilder()
                            .setNextPageToken("page-2")
                            .setTotalObjectCount(2)
                            .addObjects(GalaxyObject.newBuilder()
                                    .setGobjectId(7)
                                    .setTagName("Pump_001")
                                    .setContainedName("Pump")
                                    .setBrowseName("Pump")
                                    .setParentGobjectId(1)
                                    .setIsArea(false)
                                    .setCategoryId(3)
                                    .setHostedByGobjectId(0)
                                    .addTemplateChain("$Pump")
                                    .addAttributes(GalaxyAttribute.newBuilder()
                                            .setAttributeName("Speed")
                                            .setFullTagReference("Pump_001.Speed")
                                            .setMxDataType(5)
                                            .setDataTypeName("MxFloat")
                                            .setIsArray(false)
                                            .setIsHistorized(true)))
                            .build());
                } else {
                    secondRequest.set(request);
                    responseObserver.onNext(DiscoverHierarchyReply.newBuilder()
                            .setTotalObjectCount(2)
                            .addObjects(GalaxyObject.newBuilder()
                                    .setGobjectId(8)
                                    .setTagName("Pump_002"))
                            .build());
                }
                responseObserver.onCompleted();
            }
        };

        try (InProcessGalaxy g = InProcessGalaxy.start(service, new AtomicReference<>());
                GalaxyRepositoryClient client = g.client("")) {
            List<GalaxyObject> objects = client.discoverHierarchy();
            assertEquals(2, objects.size());
            assertEquals(5000, firstRequest.get().getPageSize());
            assertEquals("", firstRequest.get().getPageToken());
            assertEquals("page-2", secondRequest.get().getPageToken());
            GalaxyObject only = objects.get(0);
            assertEquals(7, only.getGobjectId());
            assertEquals("Pump_001", only.getTagName());
            assertEquals(1, only.getAttributesCount());
            assertEquals("Pump_001.Speed", only.getAttributes(0).getFullTagReference());
            assertTrue(only.getAttributes(0).getIsHistorized());
        }
    }

    @Test
    void deployEventStreamCloseBeforeBeforeStartCancelsStream() {
        DeployEventStream stream = new DeployEventStream(4);
        ClientResponseObserver<WatchDeployEventsRequest, DeployEvent> observer = stream.observer();
        RecordingClientCallStreamObserver requestStream = new RecordingClientCallStreamObserver();

        stream.close();
        observer.beforeStart(requestStream);

        assertTrue(requestStream.cancelled);
        assertEquals("client cancelled deploy event stream", requestStream.cancelMessage);
        assertFalse(stream.hasNext());
    }

    @Test
    void discoverHierarchyRejectsRepeatedPageToken() throws Exception {
        TestService service = new TestService() {
            @Override
            public void discoverHierarchy(
                    DiscoverHierarchyRequest request, StreamObserver<DiscoverHierarchyReply> responseObserver) {
                responseObserver.onNext(DiscoverHierarchyReply.newBuilder()
                        .setNextPageToken("7:1")
                        .build());
                responseObserver.onCompleted();
            }
        };

        try (InProcessGalaxy g = InProcessGalaxy.start(service, new AtomicReference<>());
                GalaxyRepositoryClient client = g.client("")) {
            MxGatewayException error = assertThrows(MxGatewayException.class, client::discoverHierarchy);

            assertTrue(error.getMessage().contains("repeated page token"));
        }
    }

    @Test
    void watchDeployEventsReceivesEventsInOrder() throws Exception {
        DeployEvent first = DeployEvent.newBuilder()
                .setSequence(1)
                .setObservedAt(Timestamp.newBuilder().setSeconds(1_700_000_000L).build())
                .setTimeOfLastDeploy(Timestamp.newBuilder().setSeconds(1_699_999_000L).build())
                .setTimeOfLastDeployPresent(true)
                .setObjectCount(42)
                .setAttributeCount(123)
                .build();
        DeployEvent second = DeployEvent.newBuilder()
                .setSequence(2)
                .setObservedAt(Timestamp.newBuilder().setSeconds(1_700_000_100L).build())
                .setTimeOfLastDeployPresent(false)
                .setObjectCount(43)
                .setAttributeCount(125)
                .build();

        TestService service = new TestService() {
            @Override
            public void watchDeployEvents(
                    WatchDeployEventsRequest request, StreamObserver<DeployEvent> responseObserver) {
                responseObserver.onNext(first);
                responseObserver.onNext(second);
                responseObserver.onCompleted();
            }
        };

        try (InProcessGalaxy g = InProcessGalaxy.start(service, new AtomicReference<>());
                GalaxyRepositoryClient client = g.client("")) {
            try (DeployEventStream stream = client.watchDeployEvents(null)) {
                assertTrue(stream.hasNext());
                DeployEvent event1 = stream.next();
                assertEquals(1L, event1.getSequence());
                assertEquals(42, event1.getObjectCount());
                assertTrue(event1.getTimeOfLastDeployPresent());

                assertTrue(stream.hasNext());
                DeployEvent event2 = stream.next();
                assertEquals(2L, event2.getSequence());
                assertFalse(event2.getTimeOfLastDeployPresent());

                assertFalse(stream.hasNext());
            }
        }
    }

    @Test
    void watchDeployEventsPropagatesLastSeenDeployTime() throws Exception {
        AtomicReference<WatchDeployEventsRequest> seen = new AtomicReference<>();
        Instant lastSeen = Instant.ofEpochSecond(1_700_000_000L, 250_000_000);

        TestService service = new TestService() {
            @Override
            public void watchDeployEvents(
                    WatchDeployEventsRequest request, StreamObserver<DeployEvent> responseObserver) {
                seen.set(request);
                responseObserver.onCompleted();
            }
        };

        try (InProcessGalaxy g = InProcessGalaxy.start(service, new AtomicReference<>());
                GalaxyRepositoryClient client = g.client("")) {
            try (DeployEventStream stream = client.watchDeployEvents(lastSeen)) {
                assertFalse(stream.hasNext());
            }
        }

        WatchDeployEventsRequest request = seen.get();
        assertNotNull(request);
        Timestamp expected = request.getLastSeenDeployTime();
        assertEquals(lastSeen.getEpochSecond(), expected.getSeconds());
        assertEquals(lastSeen.getNano(), expected.getNanos());
    }

    @Test
    void watchDeployEventsClientCancellationTearsDownCleanly() throws Exception {
        CountDownLatch cancelObserved = new CountDownLatch(1);

        TestService service = new TestService() {
            @Override
            public void watchDeployEvents(
                    WatchDeployEventsRequest request, StreamObserver<DeployEvent> responseObserver) {
                io.grpc.stub.ServerCallStreamObserver<DeployEvent> serverObserver =
                        (io.grpc.stub.ServerCallStreamObserver<DeployEvent>) responseObserver;
                serverObserver.setOnCancelHandler(cancelObserved::countDown);

                DeployEvent bootstrap = DeployEvent.newBuilder()
                        .setSequence(1)
                        .setObservedAt(Timestamp.newBuilder().setSeconds(1L).build())
                        .build();
                responseObserver.onNext(bootstrap);
                // Server holds the stream open; cancellation must come from the client.
            }
        };

        try (InProcessGalaxy g = InProcessGalaxy.start(service, new AtomicReference<>());
                GalaxyRepositoryClient client = g.client("")) {
            DeployEventStream stream = client.watchDeployEvents(null);
            assertTrue(stream.hasNext());
            assertEquals(1L, stream.next().getSequence());

            stream.close();
            assertTrue(
                    cancelObserved.await(5, TimeUnit.SECONDS),
                    "server should observe client-side cancellation");
            assertFalse(stream.hasNext());
        }
    }

    private abstract static class TestService extends GalaxyRepositoryGrpc.GalaxyRepositoryImplBase {
        @Override
        public void testConnection(
                TestConnectionRequest request, StreamObserver<TestConnectionReply> responseObserver) {
            responseObserver.onNext(TestConnectionReply.newBuilder().setOk(true).build());
            responseObserver.onCompleted();
        }

        @Override
        public void getLastDeployTime(
                GetLastDeployTimeRequest request, StreamObserver<GetLastDeployTimeReply> responseObserver) {
            responseObserver.onNext(GetLastDeployTimeReply.newBuilder().setPresent(false).build());
            responseObserver.onCompleted();
        }

        @Override
        public void discoverHierarchy(
                DiscoverHierarchyRequest request, StreamObserver<DiscoverHierarchyReply> responseObserver) {
            responseObserver.onNext(DiscoverHierarchyReply.getDefaultInstance());
            responseObserver.onCompleted();
        }

        @Override
        public void watchDeployEvents(
                WatchDeployEventsRequest request, StreamObserver<DeployEvent> responseObserver) {
            responseObserver.onCompleted();
        }
    }

    private static final class RecordingClientCallStreamObserver
            extends ClientCallStreamObserver<WatchDeployEventsRequest> {
        private boolean cancelled;
        private String cancelMessage;

        @Override
        public boolean isReady() {
            return true;
        }

        @Override
        public void setOnReadyHandler(Runnable onReadyHandler) {
        }

        @Override
        public void disableAutoInboundFlowControl() {
        }

        @Override
        public void request(int count) {
        }

        @Override
        public void setMessageCompression(boolean enable) {
        }

        @Override
        public void cancel(String message, Throwable cause) {
            cancelled = true;
            cancelMessage = message;
        }

        @Override
        public void onNext(WatchDeployEventsRequest value) {
        }

        @Override
        public void onError(Throwable error) {
        }

        @Override
        public void onCompleted() {
        }
    }

    private record InProcessGalaxy(Server server, ManagedChannel channel) implements AutoCloseable {
        static InProcessGalaxy start(
                GalaxyRepositoryGrpc.GalaxyRepositoryImplBase service, AtomicReference<String> authorization)
                throws Exception {
            String serverName = "mxgw-galaxy-java-" + UUID.randomUUID();
            ServerInterceptor interceptor = new ServerInterceptor() {
                @Override
                public <ReqT, RespT> ServerCall.Listener<ReqT> interceptCall(
                        ServerCall<ReqT, RespT> call,
                        Metadata headers,
                        ServerCallHandler<ReqT, RespT> next) {
                    authorization.set(headers.get(MxGatewayAuthInterceptor.AUTHORIZATION_HEADER));
                    return next.startCall(call, headers);
                }
            };
            Server server = InProcessServerBuilder.forName(serverName)
                    .directExecutor()
                    .addService(io.grpc.ServerInterceptors.intercept(service, interceptor))
                    .build()
                    .start();
            ManagedChannel channel = InProcessChannelBuilder.forName(serverName)
                    .directExecutor()
                    .build();
            return new InProcessGalaxy(server, channel);
        }

        GalaxyRepositoryClient client(String apiKey) {
            return new GalaxyRepositoryClient(
                    channel,
                    MxGatewayClientOptions.builder()
                            .endpoint("in-process")
                            .apiKey(apiKey)
                            .plaintext(true)
                            .callTimeout(Duration.ofSeconds(5))
                            .build());
        }

        @Override
        public void close() {
            channel.shutdownNow();
            server.shutdownNow();
        }
    }
}
