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.BrowseChildrenReply;
import galaxy_repository.v1.GalaxyRepositoryOuterClass.BrowseChildrenRequest;
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.Status;
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.ArrayDeque;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import java.util.Queue;
import java.util.UUID;
import java.util.ArrayList;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
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 browseChildrenRejectsRepeatedPageToken() throws Exception {
        // Queue the same BrowseChildrenReply twice with a non-empty NextPageToken.
        // The client will request a second page and detect that the token repeats.
        BrowseChildrenService service = new BrowseChildrenService();
        BrowseChildrenReply repeatedReply = browseReply(
                List.of(obj(1, "Plant", true)),
                List.of(true),
                1L,
                "1:abc:1");
        service.replies.add(repeatedReply);
        service.replies.add(repeatedReply);

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

            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());
        }
    }

    @Test
    void browseNoParentReturnsRoots() throws Exception {
        BrowseChildrenService service = new BrowseChildrenService();
        service.replies.add(browseReply(
                List.of(obj(1, "Plant", true), obj(2, "Other", false)),
                List.of(true, false),
                1L,
                ""));

        try (InProcessGalaxy g = InProcessGalaxy.start(service, new AtomicReference<>());
                GalaxyRepositoryClient client = g.client("")) {
            List<LazyBrowseNode> roots = client.browse();

            assertEquals(2, roots.size());
            assertEquals("Plant", roots.get(0).getObject().getTagName());
            assertTrue(roots.get(0).hasChildrenHint());
            assertFalse(roots.get(0).isExpanded());
            assertEquals("Other", roots.get(1).getObject().getTagName());
            assertFalse(roots.get(1).hasChildrenHint());
            assertFalse(roots.get(1).isExpanded());
            assertEquals(1, service.calls.size());
            assertFalse(service.calls.get(0).hasParentGobjectId());
        }
    }

    @Test
    void browseExpandPopulatesChildrenAndMarksExpanded() throws Exception {
        BrowseChildrenService service = new BrowseChildrenService();
        service.replies.add(browseReply(
                List.of(obj(1, "Plant", true)),
                List.of(true),
                1L,
                ""));
        service.replies.add(browseReply(
                List.of(obj(10, "Line1", false)),
                List.of(false),
                1L,
                ""));

        try (InProcessGalaxy g = InProcessGalaxy.start(service, new AtomicReference<>());
                GalaxyRepositoryClient client = g.client("")) {
            List<LazyBrowseNode> roots = client.browse();
            roots.get(0).expand();

            assertTrue(roots.get(0).isExpanded());
            assertEquals(1, roots.get(0).getChildren().size());
            assertEquals("Line1", roots.get(0).getChildren().get(0).getObject().getTagName());
            assertEquals(2, service.calls.size());
            assertTrue(service.calls.get(1).hasParentGobjectId());
            assertEquals(1, service.calls.get(1).getParentGobjectId());
        }
    }

    @Test
    void browseExpandIdempotentNoSecondRpc() throws Exception {
        BrowseChildrenService service = new BrowseChildrenService();
        service.replies.add(browseReply(
                List.of(obj(1, "Plant", true)),
                List.of(true),
                1L,
                ""));
        service.replies.add(browseReply(
                List.of(obj(10, "Line1", false)),
                List.of(false),
                1L,
                ""));

        try (InProcessGalaxy g = InProcessGalaxy.start(service, new AtomicReference<>());
                GalaxyRepositoryClient client = g.client("")) {
            List<LazyBrowseNode> roots = client.browse();
            roots.get(0).expand();
            roots.get(0).expand();

            assertEquals(2, service.calls.size());
            assertEquals(1, roots.get(0).getChildren().size());
        }
    }

    @Test
    void browseExpandUnknownParentThrowsGalaxyNotFound() throws Exception {
        BrowseChildrenService service = new BrowseChildrenService();
        service.replies.add(browseReply(
                List.of(obj(1, "Plant", true)),
                List.of(true),
                1L,
                ""));
        service.errors.add(Status.NOT_FOUND.withDescription("Parent not found").asRuntimeException());

        try (InProcessGalaxy g = InProcessGalaxy.start(service, new AtomicReference<>());
                GalaxyRepositoryClient client = g.client("")) {
            List<LazyBrowseNode> roots = client.browse();

            MxGatewayException error = assertThrows(MxGatewayException.class, () -> roots.get(0).expand());
            assertTrue(
                    error.getMessage().toLowerCase().contains("not found"),
                    "expected message to mention 'not found', got: " + error.getMessage());
        }
    }

    @Test
    void browseExpandMultiPageGathersAllPages() throws Exception {
        BrowseChildrenService service = new BrowseChildrenService();
        // Roots
        service.replies.add(browseReply(
                List.of(obj(7, "Plant", true)),
                List.of(true),
                1L,
                ""));
        // First child page with a next token
        service.replies.add(browseReply(
                List.of(obj(70, "ChildA", false), obj(71, "ChildB", false)),
                List.of(false, false),
                1L,
                "7:abc:2"));
        // Second child page closes the loop
        service.replies.add(browseReply(
                List.of(obj(72, "ChildC", false)),
                List.of(false),
                1L,
                ""));

        try (InProcessGalaxy g = InProcessGalaxy.start(service, new AtomicReference<>());
                GalaxyRepositoryClient client = g.client("")) {
            List<LazyBrowseNode> roots = client.browse();
            roots.get(0).expand();

            assertEquals(3, roots.get(0).getChildren().size());
            assertEquals(3, service.calls.size());
            assertEquals("7:abc:2", service.calls.get(2).getPageToken());
        }
    }

    @Test
    void browseExpandConcurrentCallersOnlyFireOneRpc() throws Exception {
        // Verifies that concurrent expand() calls coalesce onto a single in-flight
        // BrowseChildren RPC and that readers (isExpanded/getChildren) are not
        // blocked for the full RPC duration.
        BrowseChildrenReply rootsReply = browseReply(
                List.of(obj(1, "Plant", true)),
                List.of(true),
                7L,
                "");
        BrowseChildrenReply childrenReply = browseReply(
                List.of(obj(2, "Mixer_001", false)),
                List.of(false),
                7L,
                "");

        // Gate the child fetch behind a latch so multiple expanders can pile up.
        CountDownLatch release = new CountDownLatch(1);
        AtomicInteger childCalls = new AtomicInteger();
        BrowseChildrenService service = new BrowseChildrenService() {
            @Override
            public void browseChildren(
                    BrowseChildrenRequest request, StreamObserver<BrowseChildrenReply> responseObserver) {
                calls.add(request);
                BrowseChildrenReply reply;
                if (!request.hasParentGobjectId()) {
                    reply = rootsReply;
                } else {
                    // Block the leader until the followers have arrived.
                    try {
                        assertTrue(release.await(5, TimeUnit.SECONDS), "release latch never tripped");
                    } catch (InterruptedException ie) {
                        Thread.currentThread().interrupt();
                        responseObserver.onError(Status.CANCELLED.asRuntimeException());
                        return;
                    }
                    childCalls.incrementAndGet();
                    reply = childrenReply;
                }
                responseObserver.onNext(reply);
                responseObserver.onCompleted();
            }
        };

        try (InProcessGalaxy g = InProcessGalaxy.start(service, new AtomicReference<>());
                GalaxyRepositoryClient client = g.client("")) {
            List<LazyBrowseNode> roots = client.browse();
            LazyBrowseNode root = roots.get(0);

            int parallelism = 10;
            ExecutorService pool = Executors.newFixedThreadPool(parallelism);
            try {
                CountDownLatch ready = new CountDownLatch(parallelism);
                List<Future<Void>> futures = new ArrayList<>();
                for (int i = 0; i < parallelism; i++) {
                    futures.add(pool.submit(() -> {
                        ready.countDown();
                        root.expand();
                        return null;
                    }));
                }
                // Wait for all callers to be in flight, then release the leader.
                assertTrue(ready.await(5, TimeUnit.SECONDS), "expander threads did not start");
                // Readers must not be blocked by an in-flight expand; this should not deadlock
                // and should return the pre-expand state.
                assertFalse(root.isExpanded());
                assertEquals(0, root.getChildren().size());
                release.countDown();

                for (Future<Void> f : futures) {
                    f.get(10, TimeUnit.SECONDS);
                }
            } finally {
                pool.shutdownNow();
            }

            assertTrue(root.isExpanded());
            assertEquals(1, root.getChildren().size());
            // Exactly one expand RPC was issued even though many callers raced.
            assertEquals(1, childCalls.get());
            // 1 roots fetch + exactly 1 expand fetch.
            assertEquals(2, service.calls.size());
        }
    }

    @Test
    void browseWithFilterForwardsToRequest() throws Exception {
        BrowseChildrenService service = new BrowseChildrenService();
        // Default reply is empty; only the request shape matters here.
        try (InProcessGalaxy g = InProcessGalaxy.start(service, new AtomicReference<>());
                GalaxyRepositoryClient client = g.client("")) {
            client.browse(BrowseChildrenOptions.builder()
                    .tagNameGlob("Mixer*")
                    .alarmBearingOnly(true)
                    .build());
        }

        assertEquals(1, service.calls.size());
        BrowseChildrenRequest request = service.calls.get(0);
        assertEquals("Mixer*", request.getTagNameGlob());
        assertTrue(request.getAlarmBearingOnly());
    }

    private static GalaxyObject obj(int id, String tag, boolean isArea) {
        return GalaxyObject.newBuilder()
                .setGobjectId(id)
                .setTagName(tag)
                .setBrowseName(tag)
                .setIsArea(isArea)
                .build();
    }

    private static BrowseChildrenReply browseReply(
            List<GalaxyObject> children,
            List<Boolean> childHasChildren,
            long cacheSequence,
            String nextPageToken) {
        BrowseChildrenReply.Builder b = BrowseChildrenReply.newBuilder()
                .setTotalChildCount(children.size())
                .setCacheSequence(cacheSequence)
                .setNextPageToken(nextPageToken);
        b.addAllChildren(children);
        b.addAllChildHasChildren(childHasChildren);
        return b.build();
    }

    private static class BrowseChildrenService extends TestService {
        final List<BrowseChildrenRequest> calls =
                Collections.synchronizedList(new CopyOnWriteArrayList<>());
        final Queue<BrowseChildrenReply> replies = new ArrayDeque<>();
        final Queue<Throwable> errors = new ArrayDeque<>();

        @Override
        public void browseChildren(
                BrowseChildrenRequest request, StreamObserver<BrowseChildrenReply> responseObserver) {
            calls.add(request);
            BrowseChildrenReply reply;
            Throwable err;
            synchronized (this) {
                // Prefer queued replies first; once they're exhausted, fall through to any
                // queued error. This matches the .NET fake's ordering used by parity tests.
                reply = replies.poll();
                err = reply == null ? errors.poll() : null;
            }
            if (err != null) {
                responseObserver.onError(err);
                return;
            }
            if (reply == null) {
                reply = BrowseChildrenReply.getDefaultInstance();
            }
            responseObserver.onNext(reply);
            responseObserver.onCompleted();
        }
    }

    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();
        }
    }
}
