using System.Diagnostics.Metrics;
using System.Runtime.CompilerServices;
using Google.Protobuf.WellKnownTypes;
using Grpc.Core;
using Microsoft.Extensions.Logging.Abstractions;
using ZB.MOM.WW.MxGateway.Contracts;
using ZB.MOM.WW.MxGateway.Contracts.Proto;
using ZB.MOM.WW.MxGateway.Server.Grpc;
using ZB.MOM.WW.MxGateway.Server.Metrics;
using ZB.MOM.WW.MxGateway.Server.Security.Authentication;
using ZB.MOM.WW.MxGateway.Server.Security.Authorization;
using ZB.MOM.WW.MxGateway.Server.Sessions;
using ZB.MOM.WW.MxGateway.Server.Workers;
using ZB.MOM.WW.MxGateway.Tests.TestSupport;

namespace ZB.MOM.WW.MxGateway.Tests.Gateway.Grpc;

public sealed class MxAccessGatewayServiceTests
{
    /// <summary>Verifies that OpenSession returns correct session details for a valid request.</summary>
    /// <returns>A task that represents the asynchronous operation.</returns>
    [Fact]
    public async Task OpenSession_WithValidRequest_ReturnsSessionDetails()
    {
        GatewayRequestIdentityAccessor identityAccessor = new();
        FakeSessionManager sessionManager = new()
        {
            OpenSessionResult = CreateSession("session-1", processId: 4321),
        };
        MxAccessGatewayService service = CreateService(sessionManager, identityAccessor);

        using IDisposable identityScope = identityAccessor.Push(CreateIdentity());
        OpenSessionReply reply = await service.OpenSession(
            new OpenSessionRequest
            {
                ClientSessionName = "operator-session",
                CommandTimeout = Duration.FromTimeSpan(TimeSpan.FromSeconds(7)),
            },
            new TestServerCallContext());

        Assert.Equal("session-1", reply.SessionId);
        Assert.Equal(GatewayContractInfo.DefaultBackendName, reply.BackendName);
        Assert.Equal(4321, reply.WorkerProcessId);
        Assert.Equal(GatewayContractInfo.WorkerProtocolVersion, reply.WorkerProtocolVersion);
        Assert.Equal(GatewayContractInfo.GatewayProtocolVersion, reply.GatewayProtocolVersion);
        Assert.Equal(ProtocolStatusCode.Ok, reply.ProtocolStatus.Code);
        Assert.Contains("unary-invoke", reply.Capabilities);
        Assert.Equal("Operator Key", sessionManager.LastClientIdentity);
        Assert.Equal("operator01", sessionManager.LastOwnerKeyId);
        Assert.Equal("operator-session", sessionManager.LastOpenRequest?.ClientSessionName);
    }

    /// <summary>
    ///     Verifies OpenSession forwards the calling key's dashboard-visibility tags, so the
    ///     session's tags are derived from the owning API key rather than the wire request (SEC-25).
    /// </summary>
    /// <returns>A task that represents the asynchronous operation.</returns>
    [Fact]
    public async Task OpenSession_WithTaggedKey_ForwardsOwnerDashboardTags()
    {
        GatewayRequestIdentityAccessor identityAccessor = new();
        FakeSessionManager sessionManager = new();
        MxAccessGatewayService service = CreateService(sessionManager, identityAccessor);
        ApiKeyIdentity identity = CreateIdentity() with
        {
            Constraints = ApiKeyConstraints.Empty with { DashboardTags = ["team-a"] },
        };

        using IDisposable identityScope = identityAccessor.Push(identity);
        await service.OpenSession(new OpenSessionRequest(), new TestServerCallContext());

        Assert.Equal(["team-a"], sessionManager.LastOwnerDashboardTags);
    }

    /// <summary>
    ///     Verifies an unauthenticated OpenSession (no resolved key identity) opens an untagged
    ///     session — the fail-closed state for dashboard event visibility.
    /// </summary>
    /// <returns>A task that represents the asynchronous operation.</returns>
    [Fact]
    public async Task OpenSession_WithoutIdentity_ForwardsNoDashboardTags()
    {
        FakeSessionManager sessionManager = new();
        MxAccessGatewayService service = CreateService(sessionManager, new GatewayRequestIdentityAccessor());

        await service.OpenSession(new OpenSessionRequest(), new TestServerCallContext());

        Assert.Null(sessionManager.LastOwnerDashboardTags);
        Assert.Null(sessionManager.LastOwnerKeyId);
    }

    /// <summary>
    ///     Verifies that Invoke maps a genuinely missing session to NotFound via the
    ///     service's own <c>ResolveSession</c> lookup. No <c>InvokeException</c> is
    ///     injected — <see cref="FakeSessionManager.ResolveOnlySeededSessions"/> makes
    ///     <c>TryGetSession</c> return false, so this test fails if the service drops
    ///     its missing-session check rather than passing for the wrong reason.
    /// </summary>
    /// <returns>A task that represents the asynchronous operation.</returns>
    [Fact]
    public async Task Invoke_WhenSessionMissing_ThrowsNotFound()
    {
        FakeSessionManager sessionManager = new() { ResolveOnlySeededSessions = true };
        MxAccessGatewayService service = CreateService(sessionManager);

        RpcException exception = await Assert.ThrowsAsync<RpcException>(
            async () => await service.Invoke(
                CreatePingRequest("session-missing"),
                new TestServerCallContext()));

        Assert.Equal(StatusCode.NotFound, exception.StatusCode);
        Assert.Contains("session-missing", exception.Status.Detail, StringComparison.Ordinal);
        // The service must reject before delegating to the session manager.
        Assert.Equal(0, sessionManager.InvokeCount);
    }

    /// <summary>
    ///     Verifies that Invoke resolves a session that was seeded into the session
    ///     manager when <see cref="FakeSessionManager.ResolveOnlySeededSessions"/> is on,
    ///     confirming the missing-session test above is gated on a real lookup.
    /// </summary>
    /// <returns>A task that represents the asynchronous operation.</returns>
    [Fact]
    public async Task Invoke_WhenSessionSeeded_ResolvesAndInvokes()
    {
        FakeSessionManager sessionManager = new() { ResolveOnlySeededSessions = true };
        sessionManager.SeedSession(CreateSession("session-1", processId: 1234));
        MxAccessGatewayService service = CreateService(sessionManager);

        MxCommandReply reply = await service.Invoke(
            CreatePingRequest("session-1"),
            new TestServerCallContext());

        Assert.Equal(ProtocolStatusCode.Ok, reply.ProtocolStatus.Code);
        Assert.Equal(1, sessionManager.InvokeCount);
    }

    /// <summary>Verifies that Invoke throws InvalidArgument and does not invoke the session manager when payload is mismatched.</summary>
    /// <returns>A task that represents the asynchronous operation.</returns>
    [Fact]
    public async Task Invoke_WithMismatchedPayload_ThrowsInvalidArgumentAndDoesNotCallSessionManager()
    {
        FakeSessionManager sessionManager = new();
        MxAccessGatewayService service = CreateService(sessionManager);
        MxCommandRequest request = new()
        {
            SessionId = "session-1",
            Command = new MxCommand
            {
                Kind = MxCommandKind.AddItem,
                Ping = new PingCommand { Message = "wrong-payload" },
            },
        };

        RpcException exception = await Assert.ThrowsAsync<RpcException>(
            async () => await service.Invoke(request, new TestServerCallContext()));

        Assert.Equal(StatusCode.InvalidArgument, exception.StatusCode);
        Assert.Equal(0, sessionManager.InvokeCount);
    }

    /// <summary>Verifies that Invoke returns HResult status and method payload from worker reply.</summary>
    /// <returns>A task that represents the asynchronous operation.</returns>
    [Fact]
    public async Task Invoke_WithWorkerReply_ReturnsHresultStatusAndMethodPayload()
    {
        const int hresult = unchecked((int)0x80004005);
        FakeSessionManager sessionManager = new()
        {
            InvokeReply = new WorkerCommandReply
            {
                Reply = new MxCommandReply
                {
                    SessionId = "session-1",
                    CorrelationId = "worker-correlation",
                    Kind = MxCommandKind.AddItem,
                    ProtocolStatus = MxAccessGrpcMapper.Ok(),
                    Hresult = hresult,
                    AddItem = new AddItemReply { ItemHandle = 42 },
                    DiagnosticMessage = "mxaccess diagnostic",
                },
            },
        };
        sessionManager.InvokeReply.Reply.Statuses.Add(new MxStatusProxy
        {
            Success = 0,
            Category = MxStatusCategory.SoftwareError,
            Detail = 1001,
            DiagnosticText = "status detail",
        });
        MxAccessGatewayService service = CreateService(sessionManager);
        MxCommandRequest request = new()
        {
            SessionId = "session-1",
            ClientCorrelationId = "client-correlation",
            Command = new MxCommand
            {
                Kind = MxCommandKind.AddItem,
                AddItem = new AddItemCommand
                {
                    ServerHandle = 12,
                    ItemDefinition = "Galaxy.Tag.Value",
                },
            },
        };

        MxCommandReply reply = await service.Invoke(request, new TestServerCallContext());

        Assert.Equal(MxCommandKind.AddItem, sessionManager.LastWorkerCommand?.Command.Kind);
        Assert.Equal("Galaxy.Tag.Value", sessionManager.LastWorkerCommand?.Command.AddItem.ItemDefinition);
        Assert.NotNull(sessionManager.LastWorkerCommand?.EnqueueTimestamp);
        Assert.Equal(hresult, reply.Hresult);
        Assert.Equal(42, reply.AddItem.ItemHandle);
        Assert.Equal("status detail", Assert.Single(reply.Statuses).DiagnosticText);
        Assert.Equal("mxaccess diagnostic", reply.DiagnosticMessage);
    }

    /// <summary>Verifies that StreamEvents writes only events after the specified worker sequence.</summary>
    /// <returns>A task that represents the asynchronous operation.</returns>
    [Fact]
    public async Task StreamEvents_WithAfterSequence_WritesOnlyLaterEvents()
    {
        FakeSessionManager sessionManager = new();
        sessionManager.Events.Add(CreateWorkerEvent("session-1", workerSequence: 1));
        sessionManager.Events.Add(CreateWorkerEvent("session-1", workerSequence: 2));
        MxAccessGatewayService service = CreateService(sessionManager);
        RecordingServerStreamWriter<MxEvent> writer = new();

        await service.StreamEvents(
            new StreamEventsRequest
            {
                SessionId = "session-1",
                AfterWorkerSequence = 1,
            },
            writer,
            new TestServerCallContext());

        MxEvent writtenEvent = Assert.Single(writer.Messages);
        Assert.Equal((ulong)2, writtenEvent.WorkerSequence);
        Assert.Equal("session-1", sessionManager.LastReadEventsSessionId);
    }

    /// <summary>
    /// Verifies that <c>StreamEvents</c> records the send-duration histogram per event.
    /// </summary>
    /// <returns>A task that represents the asynchronous operation.</returns>
    [Fact]
    public async Task StreamEvents_WhenEventIsWritten_RecordsSendDuration()
    {
        using GatewayMetrics metrics = new();
        using MeterListener listener = new();
        List<string> families = [];
        listener.InstrumentPublished = (instrument, meterListener) =>
        {
            if (ReferenceEquals(instrument.Meter, metrics.Meter)
                && instrument.Name == "mxgateway.events.stream_send.duration")
            {
                meterListener.EnableMeasurementEvents(instrument);
            }
        };
        listener.SetMeasurementEventCallback<double>(
            (instrument, measurement, tags, _) =>
            {
                if (!ReferenceEquals(instrument.Meter, metrics.Meter)
                    || instrument.Name != "mxgateway.events.stream_send.duration")
                {
                    return;
                }

                foreach (KeyValuePair<string, object?> tag in tags)
                {
                    if (tag.Key == "family" && tag.Value is string family)
                    {
                        families.Add(family);
                    }
                }
            });
        listener.Start();
        FakeSessionManager sessionManager = new();
        sessionManager.Events.Add(CreateWorkerEvent("session-1", workerSequence: 2));
        MxAccessGatewayService service = CreateService(sessionManager, metrics: metrics);
        RecordingServerStreamWriter<MxEvent> writer = new();

        await service.StreamEvents(
            new StreamEventsRequest { SessionId = "session-1" },
            writer,
            new TestServerCallContext());

        Assert.Equal([MxEventFamily.OnDataChange.ToString()], families);
    }

    /// <summary>
    /// A <see cref="MeterListener"/> that filters by the specific
    /// <see cref="System.Diagnostics.Metrics.Meter"/> instance (via <see cref="object.ReferenceEquals"/>)
    /// must NOT observe measurements recorded on a different <see cref="GatewayMetrics"/> that shares
    /// the same <see cref="GatewayMetrics.MeterName"/>. This is the cross-talk vector that previously
    /// caused <c>StreamEvents_WhenEventIsWritten_RecordsSendDuration</c> to fail intermittently when
    /// run in parallel with another test recording the same histogram.
    /// </summary>
    /// <returns>A task that represents the asynchronous operation.</returns>
    [Fact]
    public async Task StreamEvents_RecordSendDurationListener_IgnoresMeasurementsFromOtherMetersWithSameName()
    {
        using GatewayMetrics metricsUnderTest = new();
        using GatewayMetrics otherMetrics = new();
        using MeterListener listener = new();
        List<string> families = [];
        listener.InstrumentPublished = (instrument, meterListener) =>
        {
            // Subscribe to the stream_send histogram on BOTH meters so the listener
            // would observe a cross-talk measurement if the callback did not filter.
            if (instrument.Name == "mxgateway.events.stream_send.duration")
            {
                meterListener.EnableMeasurementEvents(instrument);
            }
        };
        listener.SetMeasurementEventCallback<double>(
            (instrument, measurement, tags, _) =>
            {
                if (!ReferenceEquals(instrument.Meter, metricsUnderTest.Meter)
                    || instrument.Name != "mxgateway.events.stream_send.duration")
                {
                    return;
                }

                foreach (KeyValuePair<string, object?> tag in tags)
                {
                    if (tag.Key == "family" && tag.Value is string family)
                    {
                        families.Add(family);
                    }
                }
            });
        listener.Start();

        // Simulate the cross-talk: another test's GatewayMetrics records a value
        // before the test-under-test does its single event publish. The listener
        // must filter this out by Meter reference.
        otherMetrics.RecordEventStreamSend(MxEventFamily.OnWriteComplete.ToString(), TimeSpan.FromMilliseconds(123));

        FakeSessionManager sessionManager = new();
        sessionManager.Events.Add(CreateWorkerEvent("session-1", workerSequence: 2));
        MxAccessGatewayService service = CreateService(sessionManager, metrics: metricsUnderTest);
        RecordingServerStreamWriter<MxEvent> writer = new();

        await service.StreamEvents(
            new StreamEventsRequest { SessionId = "session-1" },
            writer,
            new TestServerCallContext());

        // Only the test-under-test's OnDataChange recording should be observed —
        // the OnAlarm recording on the sibling meter must NOT leak through.
        Assert.Equal([MxEventFamily.OnDataChange.ToString()], families);
    }

    /// <summary>Verifies that CloseSession throws InvalidArgument when session ID is blank.</summary>
    /// <returns>A task that represents the asynchronous operation.</returns>
    [Fact]
    public async Task CloseSession_WithBlankSessionId_ThrowsInvalidArgument()
    {
        MxAccessGatewayService service = CreateService(new FakeSessionManager());

        RpcException exception = await Assert.ThrowsAsync<RpcException>(
            async () => await service.CloseSession(
                new CloseSessionRequest(),
                new TestServerCallContext()));

        Assert.Equal(StatusCode.InvalidArgument, exception.StatusCode);
    }

    // ===== AcknowledgeAlarm + StreamAlarms handler contract =====
    //
    // AcknowledgeAlarm validates alarm_full_reference then delegates to the
    // session-less IGatewayAlarmService; StreamAlarms forwards the central
    // alarm feed. CreateService injects FakeGatewayAlarmService.

    /// <summary>Verifies AcknowledgeAlarm rejects an empty alarm_full_reference.</summary>
    /// <returns>A task that represents the asynchronous operation.</returns>
    [Fact]
    public async Task AcknowledgeAlarm_WithMissingAlarmReference_ThrowsInvalidArgument()
    {
        MxAccessGatewayService service = CreateService(new FakeSessionManager());

        RpcException exception = await Assert.ThrowsAsync<RpcException>(
            async () => await service.AcknowledgeAlarm(
                new AcknowledgeAlarmRequest { OperatorUser = "alice" },
                new TestServerCallContext()));

        Assert.Equal(StatusCode.InvalidArgument, exception.StatusCode);
    }

    /// <summary>Verifies AcknowledgeAlarm delegates a valid request to the alarm service.</summary>
    /// <returns>A task that represents the asynchronous operation.</returns>
    [Fact]
    public async Task AcknowledgeAlarm_WithValidRequest_DelegatesToAlarmService()
    {
        MxAccessGatewayService service = CreateService(new FakeSessionManager());

        AcknowledgeAlarmReply reply = await service.AcknowledgeAlarm(
            new AcknowledgeAlarmRequest
            {
                ClientCorrelationId = "corr-1",
                AlarmFullReference = "Galaxy!Area.Tank01.Level.HiHi",
                Comment = "investigating",
                OperatorUser = "alice",
            },
            new TestServerCallContext());

        Assert.Equal(ProtocolStatusCode.Ok, reply.ProtocolStatus.Code);
        Assert.Equal("corr-1", reply.CorrelationId);
    }

    /// <summary>Verifies StreamAlarms forwards the central alarm feed, ending with snapshot_complete.</summary>
    /// <returns>A task that represents the asynchronous operation.</returns>
    [Fact]
    public async Task StreamAlarms_ForwardsTheCentralAlarmFeed()
    {
        MxAccessGatewayService service = CreateService(new FakeSessionManager());
        RecordingServerStreamWriter<AlarmFeedMessage> sink = new();

        await service.StreamAlarms(
            new StreamAlarmsRequest(),
            sink,
            new TestServerCallContext());

        Assert.Contains(
            sink.Messages,
            message => message.PayloadCase == AlarmFeedMessage.PayloadOneofCase.SnapshotComplete);
    }

    /// <summary>Verifies OpenSession advertises the alarm RPC capability strings.</summary>
    /// <returns>A task that represents the asynchronous operation.</returns>
    [Fact]
    public async Task OpenSession_AdvertisesAlarmRpcCapabilities()
    {
        FakeSessionManager sessionManager = new();
        GatewayRequestIdentityAccessor identityAccessor = new();
        MxAccessGatewayService service = CreateService(sessionManager, identityAccessor);

        using IDisposable identityScope = identityAccessor.Push(CreateIdentity());

        OpenSessionReply reply = await service.OpenSession(
            new OpenSessionRequest(),
            new TestServerCallContext());

        Assert.Contains("unary-acknowledge-alarm", reply.Capabilities);
        Assert.Contains("server-stream-active-alarms", reply.Capabilities);
    }

    private static MxAccessGatewayService CreateService(
        FakeSessionManager sessionManager,
        IGatewayRequestIdentityAccessor? identityAccessor = null,
        GatewayMetrics? metrics = null,
        IConstraintEnforcer? constraintEnforcer = null)
    {
        return new MxAccessGatewayService(
            sessionManager,
            identityAccessor ?? new GatewayRequestIdentityAccessor(),
            constraintEnforcer ?? new AllowAllConstraintEnforcer(),
            new MxAccessGrpcRequestValidator(),
            new MxAccessGrpcMapper(),
            new FakeEventStreamService(sessionManager),
            metrics ?? new GatewayMetrics(),
            NullLogger<MxAccessGatewayService>.Instance,
            new FakeGatewayAlarmService());
    }

    private static ApiKeyIdentity CreateIdentity()
    {
        return new ApiKeyIdentity(
            KeyId: "operator01",
            KeyPrefix: "mxgw_operator01",
            DisplayName: "Operator Key",
            Scopes: new HashSet<string>(StringComparer.Ordinal));
    }

    private static GatewaySession CreateSession(
        string sessionId,
        int processId)
    {
        GatewaySession session = new(
            sessionId,
            GatewayContractInfo.DefaultBackendName,
            "pipe",
            "nonce",
            "Operator Key",
            "operator-session",
            "client-correlation",
            TimeSpan.FromSeconds(7),
            TimeSpan.FromSeconds(30),
            TimeSpan.FromSeconds(10),
            DateTimeOffset.UtcNow);
        session.AttachWorkerClient(new FakeWorkerClient(processId));
        session.MarkReady();

        return session;
    }

    private static MxCommandRequest CreatePingRequest(string sessionId)
    {
        return new MxCommandRequest
        {
            SessionId = sessionId,
            Command = new MxCommand
            {
                Kind = MxCommandKind.Ping,
                Ping = new PingCommand { Message = "ping" },
            },
        };
    }

    private static WorkerEvent CreateWorkerEvent(
        string sessionId,
        ulong workerSequence)
    {
        return new WorkerEvent
        {
            Event = new MxEvent
            {
                Family = MxEventFamily.OnDataChange,
                SessionId = sessionId,
                WorkerSequence = workerSequence,
                OnDataChange = new OnDataChangeEvent(),
            },
        };
    }

    private sealed class FakeSessionManager : ISessionManager
    {
        private readonly Dictionary<string, GatewaySession> seededSessions = new(StringComparer.Ordinal);

        /// <summary>The session to return from OpenSessionAsync.</summary>
        public GatewaySession? OpenSessionResult { get; init; }

        /// <summary>
        ///     When true, <see cref="TryGetSession"/> only resolves sessions that have been
        ///     explicitly seeded via <see cref="SeedSession"/> (or <see cref="OpenSessionResult"/>),
        ///     and returns false for any other id. This exercises the gateway service's own
        ///     missing-session handling instead of masking it with a synthesized session.
        /// </summary>
        public bool ResolveOnlySeededSessions { get; init; }

        /// <summary>Registers a session so <see cref="TryGetSession"/> resolves its id.</summary>
        /// <param name="session">Session to register by its <see cref="GatewaySession.SessionId"/>.</param>
        public void SeedSession(GatewaySession session)
        {
            seededSessions[session.SessionId] = session;
        }

        /// <summary>The last OpenSessionAsync request captured.</summary>
        public SessionOpenRequest? LastOpenRequest { get; private set; }

        /// <summary>The last client identity passed to OpenSessionAsync.</summary>
        public string? LastClientIdentity { get; private set; }

        /// <summary>The last owner key id passed to OpenSessionAsync.</summary>
        public string? LastOwnerKeyId { get; private set; }

        /// <summary>The last owner dashboard tags passed to OpenSessionAsync.</summary>
        public IReadOnlyList<string>? LastOwnerDashboardTags { get; private set; }

        /// <summary>The last session ID the event stream service was asked to stream.</summary>
        public string? LastReadEventsSessionId { get; private set; }

        /// <summary>The last worker command passed to InvokeAsync.</summary>
        public WorkerCommand? LastWorkerCommand { get; private set; }

        /// <summary>The reply to return from InvokeAsync.</summary>
        public WorkerCommandReply InvokeReply { get; init; } = new()
        {
            Reply = new MxCommandReply
            {
                SessionId = "session-1",
                Kind = MxCommandKind.Ping,
                ProtocolStatus = MxAccessGrpcMapper.Ok(),
            },
        };

        /// <summary>The exception to throw from InvokeAsync.</summary>
        public Exception? InvokeException { get; init; }

        /// <summary>The number of times InvokeAsync was called.</summary>
        public int InvokeCount { get; private set; }

        /// <summary>The events the fake event stream service replays for this manager.</summary>
        public List<WorkerEvent> Events { get; } = [];

        /// <summary>Records the session ID the event stream service was asked to stream.</summary>
        /// <param name="sessionId">Identifier of the session.</param>
        public void RecordReadEventsSessionId(string sessionId)
        {
            LastReadEventsSessionId = sessionId;
        }

        /// <inheritdoc />
        public Task<GatewaySession> OpenSessionAsync(
            SessionOpenRequest request,
            string? clientIdentity,
            string? ownerKeyId,
            CancellationToken cancellationToken)
        {
            LastOpenRequest = request;
            LastClientIdentity = clientIdentity;
            LastOwnerKeyId = ownerKeyId;

            return Task.FromResult(OpenSessionResult ?? CreateSession("session-1", processId: 1234));
        }

        /// <inheritdoc />
        public Task<GatewaySession> OpenSessionAsync(
            SessionOpenRequest request,
            string? clientIdentity,
            string? ownerKeyId,
            IReadOnlyList<string>? ownerDashboardTags,
            CancellationToken cancellationToken)
        {
            LastOwnerDashboardTags = ownerDashboardTags;

            return OpenSessionAsync(request, clientIdentity, ownerKeyId, cancellationToken);
        }

        /// <inheritdoc />
        public bool TryGetSession(
            string sessionId,
            out GatewaySession session)
        {
            if (seededSessions.TryGetValue(sessionId, out GatewaySession? seeded))
            {
                session = seeded;
                return true;
            }

            if (ResolveOnlySeededSessions)
            {
                session = null!;
                return false;
            }

            session = OpenSessionResult ?? CreateSession(sessionId, processId: 1234);
            return true;
        }

        /// <inheritdoc />
        public Task<WorkerCommandReply> InvokeAsync(
            string sessionId,
            WorkerCommand command,
            CancellationToken cancellationToken)
        {
            InvokeCount++;
            LastWorkerCommand = command;

            if (InvokeException is not null)
            {
                throw InvokeException;
            }

            return Task.FromResult(InvokeReply);
        }

        /// <inheritdoc />
        public Task<SessionCloseResult> CloseSessionAsync(
            string sessionId,
            CancellationToken cancellationToken)
        {
            return Task.FromResult(new SessionCloseResult(sessionId, SessionState.Closed, AlreadyClosed: false));
        }

        /// <inheritdoc />
        public Task<SessionCloseResult> KillWorkerAsync(
            string sessionId,
            string reason,
            CancellationToken cancellationToken)
        {
            return Task.FromResult(new SessionCloseResult(sessionId, SessionState.Closed, AlreadyClosed: false));
        }

        /// <inheritdoc />
        public Task<int> CloseExpiredLeasesAsync(
            DateTimeOffset now,
            CancellationToken cancellationToken)
        {
            return Task.FromResult(0);
        }

        /// <inheritdoc />
        public Task ShutdownAsync(CancellationToken cancellationToken)
        {
            return Task.CompletedTask;
        }
    }

    private sealed class FakeEventStreamService(FakeSessionManager sessionManager) : IEventStreamService
    {
        /// <inheritdoc />
        public async IAsyncEnumerable<MxEvent> StreamEventsAsync(
            StreamEventsRequest request,
            string? callerKeyId,
            [EnumeratorCancellation] CancellationToken cancellationToken)
        {
            sessionManager.RecordReadEventsSessionId(request.SessionId);
            foreach (WorkerEvent workerEvent in sessionManager.Events)
            {
                cancellationToken.ThrowIfCancellationRequested();
                await Task.Yield();
                if (workerEvent.Event.WorkerSequence <= request.AfterWorkerSequence)
                {
                    continue;
                }

                yield return workerEvent.Event;
            }
        }
    }

    private sealed class FakeWorkerClient(int processId) : IWorkerClient
    {
        /// <inheritdoc />
        public string SessionId { get; } = "session-1";

        /// <inheritdoc />
        public int? ProcessId { get; } = processId;

        /// <inheritdoc />
        public WorkerClientState State { get; } = WorkerClientState.Ready;

        /// <inheritdoc />
        public DateTimeOffset LastHeartbeatAt { get; } = DateTimeOffset.UtcNow;

        /// <inheritdoc />
        public Task StartAsync(CancellationToken cancellationToken)
        {
            return Task.CompletedTask;
        }

        /// <inheritdoc />
        public Task<WorkerCommandReply> InvokeAsync(
            WorkerCommand command,
            TimeSpan timeout,
            CancellationToken cancellationToken)
        {
            return Task.FromResult(new WorkerCommandReply());
        }

        /// <inheritdoc />
        public async IAsyncEnumerable<WorkerEvent> ReadEventsAsync(
            [EnumeratorCancellation] CancellationToken cancellationToken)
        {
            await Task.CompletedTask;
            yield break;
        }

        /// <inheritdoc />
        public Task ShutdownAsync(
            TimeSpan timeout,
            CancellationToken cancellationToken)
        {
            return Task.CompletedTask;
        }

        /// <inheritdoc />
        public void Kill(string reason)
        {
        }

        /// <summary>Completes immediately without disposing any resources.</summary>
        /// <returns>A task that represents the asynchronous operation.</returns>
        public ValueTask DisposeAsync()
        {
            return ValueTask.CompletedTask;
        }
    }

}
