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

import com.zb.mom.ww.mxgateway.client.GalaxyRepositoryClient;
import com.zb.mom.ww.mxgateway.client.MxGatewayClient;
import com.zb.mom.ww.mxgateway.client.MxGatewayClientOptions;
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.GalaxyObject;
import galaxy_repository.v1.GalaxyRepositoryOuterClass.WatchDeployEventsRequest;
import io.grpc.ManagedChannel;
import io.grpc.Server;
import io.grpc.inprocess.InProcessChannelBuilder;
import io.grpc.inprocess.InProcessServerBuilder;
import io.grpc.stub.StreamObserver;
import java.io.IOException;
import java.time.Duration;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.CopyOnWriteArrayList;
import mxaccess_gateway.v1.MxAccessGatewayGrpc;
import mxaccess_gateway.v1.MxaccessGateway.CloseSessionReply;
import mxaccess_gateway.v1.MxaccessGateway.CloseSessionRequest;
import mxaccess_gateway.v1.MxaccessGateway.MxEvent;
import mxaccess_gateway.v1.MxaccessGateway.ProtocolStatus;
import mxaccess_gateway.v1.MxaccessGateway.ProtocolStatusCode;
import mxaccess_gateway.v1.MxaccessGateway.SessionState;

/**
 * Test fixture that stands up an in-process gRPC server hosting scripted fake
 * {@code MxAccessGateway} and {@code GalaxyRepository} service implementations,
 * so the real Java client types ({@link MxGatewayClient} /
 * {@link GalaxyRepositoryClient}) can be driven over a real channel.
 *
 * <p>The real streaming wrappers ({@code MxEventStream} /
 * {@code DeployEventStream}) have package-private constructors and
 * {@link GalaxyRepositoryClient} is {@code final}, so the streaming and galaxy
 * CLI commands cannot be exercised through the lightweight {@code FakeSession}
 * seam. Driving the real client over an in-process channel against scripted
 * services is the clean alternative; Tasks 5 and 6 add the CLI assertions on
 * top of this fixture.
 *
 * <p>Scripted payloads are settable via constructor args or setters. Each
 * instance uses a unique server name so harnesses do not collide. The
 * {@code directExecutor()} wiring keeps all dispatch on the calling thread, so
 * no background threads are leaked.
 *
 * <p><strong>Implemented RPCs.</strong> The scripted services override only the
 * RPCs the CLI tests currently exercise:
 *
 * <ul>
 *   <li>{@code MxAccessGateway}: {@code streamEvents}, {@code closeSession}.</li>
 *   <li>{@code GalaxyRepository}: {@code discoverHierarchy},
 *       {@code watchDeployEvents}.</li>
 * </ul>
 *
 * Every other RPC (e.g. {@code openSession}, {@code invoke}, {@code register},
 * {@code streamAlarms}, {@code queryActiveAlarms}, {@code browseChildren}) is
 * left at the generated {@code *ImplBase} default and therefore returns gRPC
 * {@code UNIMPLEMENTED} by design. A future test that needs one of those paths
 * must add the corresponding scripted override here first — otherwise the call
 * fails with {@code UNIMPLEMENTED} rather than the behaviour under test.
 */
final class InProcessGatewayHarness implements AutoCloseable {
    private final String serverName;
    private final Server server;
    private final ManagedChannel channel;
    private final FakeGatewayService fakeGateway;
    private final FakeGalaxyService fakeGalaxy;

    /** Starts a harness with empty scripted payloads; populate via setters. */
    InProcessGatewayHarness() {
        this(List.of(), List.of(), List.of());
    }

    /**
     * Starts a harness with the supplied scripted payloads.
     *
     * @param scriptedEvents events {@code streamEvents} pushes before completing
     * @param scriptedObjects objects {@code discoverHierarchy} returns (single page)
     * @param scriptedDeployEvents events {@code watchDeployEvents} streams before completing
     */
    InProcessGatewayHarness(
            List<MxEvent> scriptedEvents,
            List<GalaxyObject> scriptedObjects,
            List<DeployEvent> scriptedDeployEvents) {
        this.serverName = "mxgw-cli-harness-" + UUID.randomUUID();
        this.fakeGateway = new FakeGatewayService(scriptedEvents);
        this.fakeGalaxy = new FakeGalaxyService(scriptedObjects, scriptedDeployEvents);
        try {
            this.server = InProcessServerBuilder.forName(serverName)
                    .directExecutor()
                    .addService(fakeGateway)
                    .addService(fakeGalaxy)
                    .build()
                    .start();
        } catch (IOException error) {
            throw new IllegalStateException("failed to start in-process gateway harness", error);
        }
        this.channel = InProcessChannelBuilder.forName(serverName).directExecutor().build();
    }

    /** Replaces the scripted {@code streamEvents} payload. */
    void setScriptedEvents(List<MxEvent> events) {
        fakeGateway.scriptedEvents.clear();
        fakeGateway.scriptedEvents.addAll(events);
    }

    /** Replaces the scripted {@code discoverHierarchy} payload. */
    void setScriptedObjects(List<GalaxyObject> objects) {
        fakeGalaxy.scriptedObjects.clear();
        fakeGalaxy.scriptedObjects.addAll(objects);
    }

    /** Replaces the scripted {@code watchDeployEvents} payload. */
    void setScriptedDeployEvents(List<DeployEvent> deployEvents) {
        fakeGalaxy.scriptedDeployEvents.clear();
        fakeGalaxy.scriptedDeployEvents.addAll(deployEvents);
    }

    /**
     * Returns the in-process channel into the scripted services.
     *
     * @return the managed channel; lifecycle owned by the harness
     */
    ManagedChannel channel() {
        return channel;
    }

    /**
     * Builds a real {@link MxGatewayClient} over the in-process channel.
     *
     * @return a client borrowing the harness channel
     */
    MxGatewayClient gatewayClient() {
        return new MxGatewayClient(channel, testOptions());
    }

    /**
     * Builds a real {@link GalaxyRepositoryClient} over the in-process channel.
     *
     * @return a client borrowing the harness channel
     */
    GalaxyRepositoryClient galaxyClient() {
        return new GalaxyRepositoryClient(channel, testOptions());
    }

    private static MxGatewayClientOptions testOptions() {
        return MxGatewayClientOptions.builder()
                .endpoint("in-process")
                .apiKey("mxgw_test_secret")
                .plaintext(true)
                .callTimeout(Duration.ofSeconds(5))
                .build();
    }

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

    private static ProtocolStatus ok() {
        return ProtocolStatus.newBuilder()
                .setCode(ProtocolStatusCode.PROTOCOL_STATUS_CODE_OK)
                .build();
    }

    /** Scripted fake of the {@code MxAccessGateway} service. */
    private static final class FakeGatewayService extends MxAccessGatewayGrpc.MxAccessGatewayImplBase {
        private final List<MxEvent> scriptedEvents = new CopyOnWriteArrayList<>();

        FakeGatewayService(List<MxEvent> scriptedEvents) {
            this.scriptedEvents.addAll(scriptedEvents);
        }

        @Override
        public void streamEvents(
                mxaccess_gateway.v1.MxaccessGateway.StreamEventsRequest request,
                StreamObserver<MxEvent> responseObserver) {
            for (MxEvent event : scriptedEvents) {
                responseObserver.onNext(event);
            }
            responseObserver.onCompleted();
        }

        @Override
        public void closeSession(
                CloseSessionRequest request, StreamObserver<CloseSessionReply> responseObserver) {
            responseObserver.onNext(CloseSessionReply.newBuilder()
                    .setSessionId(request.getSessionId())
                    .setFinalState(SessionState.SESSION_STATE_CLOSED)
                    .setProtocolStatus(ok())
                    .build());
            responseObserver.onCompleted();
        }
    }

    /** Scripted fake of the {@code GalaxyRepository} service. */
    private static final class FakeGalaxyService extends GalaxyRepositoryGrpc.GalaxyRepositoryImplBase {
        private final List<GalaxyObject> scriptedObjects = new CopyOnWriteArrayList<>();
        private final List<DeployEvent> scriptedDeployEvents = new CopyOnWriteArrayList<>();

        FakeGalaxyService(List<GalaxyObject> scriptedObjects, List<DeployEvent> scriptedDeployEvents) {
            this.scriptedObjects.addAll(scriptedObjects);
            this.scriptedDeployEvents.addAll(scriptedDeployEvents);
        }

        @Override
        public void discoverHierarchy(
                DiscoverHierarchyRequest request, StreamObserver<DiscoverHierarchyReply> responseObserver) {
            List<GalaxyObject> snapshot = new ArrayList<>(scriptedObjects);
            responseObserver.onNext(DiscoverHierarchyReply.newBuilder()
                    .setTotalObjectCount(snapshot.size())
                    .addAllObjects(snapshot)
                    .setNextPageToken("")
                    .build());
            responseObserver.onCompleted();
        }

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