using ZB.MOM.WW.MxGateway.Contracts;
using ZB.MOM.WW.MxGateway.Contracts.Proto;
using ZB.MOM.WW.MxGateway.Server.Workers;
using ZB.MOM.WW.MxGateway.Tests.Gateway.Workers.Fakes;
using ZB.MOM.WW.MxGateway.Tests.TestSupport;

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

public sealed class FakeWorkerHarnessTests
{
    private static readonly TimeSpan TestTimeout = TimeSpan.FromSeconds(5);

    /// <summary>Verifies that completing startup with hello and ready transitions the client to ready state.</summary>
    /// <returns>A task that represents the asynchronous operation.</returns>
    [Fact]
    public async Task CompleteStartupAsync_WithHelloAndReady_TransitionsClientToReady()
    {
        await using FakeWorkerHarness fakeWorker = await FakeWorkerHarness.CreateConnectedPairAsync();
        await using WorkerClient client = fakeWorker.CreateClient();

        Task startTask = client.StartAsync(CancellationToken.None);
        WorkerEnvelope gatewayHello = await fakeWorker.CompleteStartupAsync();
        await startTask.WaitAsync(TestTimeout);

        Assert.Equal(WorkerEnvelope.BodyOneofCase.GatewayHello, gatewayHello.BodyCase);
        Assert.Equal(FakeWorkerHarness.DefaultNonce, gatewayHello.GatewayHello.Nonce);
        Assert.Equal(WorkerClientState.Ready, client.State);
        Assert.Equal(FakeWorkerHarness.DefaultWorkerProcessId, client.ProcessId);
    }

    /// <summary>Verifies that a protocol version mismatch during startup fails the client.</summary>
    /// <returns>A task that represents the asynchronous operation.</returns>
    [Fact]
    public async Task StartAsync_WithProtocolMismatch_FailsStartup()
    {
        await using FakeWorkerHarness fakeWorker = await FakeWorkerHarness.CreateConnectedPairAsync();
        await using WorkerClient client = fakeWorker.CreateClient();

        Task startTask = client.StartAsync(CancellationToken.None);
        WorkerEnvelope gatewayHello = await fakeWorker.ReadGatewayEnvelopeAsync();
        Assert.Equal(WorkerEnvelope.BodyOneofCase.GatewayHello, gatewayHello.BodyCase);
        await fakeWorker.SendWorkerHelloAsync(
            workerProtocolVersion: GatewayContractInfo.WorkerProtocolVersion + 1);

        WorkerClientException exception = await Assert.ThrowsAsync<WorkerClientException>(
            async () => await startTask.WaitAsync(TestTimeout));

        Assert.Equal(WorkerClientErrorCode.ProtocolViolation, exception.ErrorCode);
    }

    /// <summary>Verifies that a scripted reply completes a pending command invocation.</summary>
    /// <returns>A task that represents the asynchronous operation.</returns>
    [Fact]
    public async Task InvokeAsync_WithScriptedReply_CompletesCommand()
    {
        await using FakeWorkerHarness fakeWorker = await FakeWorkerHarness.CreateConnectedPairAsync();
        await using WorkerClient client = fakeWorker.CreateClient();
        await StartClientAsync(fakeWorker, client);

        Task<WorkerCommandReply> invokeTask = client.InvokeAsync(
            CreateCommand(MxCommandKind.Ping),
            TestTimeout,
            CancellationToken.None);
        WorkerEnvelope commandEnvelope = await fakeWorker.ReadCommandAsync();
        await fakeWorker.ReplyToCommandAsync(commandEnvelope);

        WorkerCommandReply reply = await invokeTask.WaitAsync(TestTimeout);

        Assert.Equal(commandEnvelope.CorrelationId, reply.Reply.CorrelationId);
        Assert.Equal(MxCommandKind.Ping, reply.Reply.Kind);
        Assert.Equal(ProtocolStatusCode.Ok, reply.Reply.ProtocolStatus.Code);
    }

    /// <summary>Verifies that scripted events are yielded in order through the event stream.</summary>
    /// <returns>A task that represents the asynchronous operation.</returns>
    [Fact]
    public async Task ReadEventsAsync_WithScriptedEvents_YieldsOrderedEvents()
    {
        await using FakeWorkerHarness fakeWorker = await FakeWorkerHarness.CreateConnectedPairAsync();
        await using WorkerClient client = fakeWorker.CreateClient();
        await StartClientAsync(fakeWorker, client);
        using CancellationTokenSource cancellationTokenSource = new(TestTimeout);

        await using IAsyncEnumerator<WorkerEvent> events =
            client.ReadEventsAsync(cancellationTokenSource.Token).GetAsyncEnumerator(cancellationTokenSource.Token);

        await fakeWorker.EmitEventAsync(MxEventFamily.OnDataChange, cancellationTokenSource.Token);
        await fakeWorker.EmitEventAsync(MxEventFamily.OperationComplete, cancellationTokenSource.Token);

        Assert.True(await events.MoveNextAsync());
        Assert.Equal((ulong)3, events.Current.Event.WorkerSequence);
        Assert.Equal(MxEventFamily.OnDataChange, events.Current.Event.Family);

        Assert.True(await events.MoveNextAsync());
        Assert.Equal((ulong)4, events.Current.Event.WorkerSequence);
        Assert.Equal(MxEventFamily.OperationComplete, events.Current.Event.Family);
    }

    /// <summary>Verifies that a scripted fault from the worker faults the client.</summary>
    /// <returns>A task that represents the asynchronous operation.</returns>
    [Fact]
    public async Task ReadLoop_WithScriptedFault_FaultsClient()
    {
        await using FakeWorkerHarness fakeWorker = await FakeWorkerHarness.CreateConnectedPairAsync();
        await using WorkerClient client = fakeWorker.CreateClient();
        await StartClientAsync(fakeWorker, client);

        await fakeWorker.EmitFaultAsync(
            WorkerFaultCategory.MxaccessCommandFailed,
            "scripted MXAccess command fault");

        await WaitUntilAsync(
            () => client.State == WorkerClientState.Faulted,
            TestTimeout);

        Assert.Equal(WorkerClientState.Faulted, client.State);
    }

    /// <summary>
    /// Verifies that sending a heartbeat updates the client heartbeat state. Uses a
    /// <see cref="ManualTimeProvider"/> so the timestamp advance is deterministic rather
    /// than relying on a wall-clock <c>Task.Delay</c> exceeding clock resolution.
    /// </summary>
    /// <returns>A task that represents the asynchronous operation.</returns>
    [Fact]
    public async Task SendHeartbeatAsync_UpdatesClientHeartbeatState()
    {
        ManualTimeProvider clock = new(DateTimeOffset.Parse("2026-05-18T12:00:00Z", System.Globalization.CultureInfo.InvariantCulture));
        await using FakeWorkerHarness fakeWorker = await FakeWorkerHarness.CreateConnectedPairAsync();
        await using WorkerClient client = fakeWorker.CreateClient(timeProvider: clock);
        await StartClientAsync(fakeWorker, client);
        DateTimeOffset previousHeartbeat = client.LastHeartbeatAt;

        clock.Advance(TimeSpan.FromSeconds(1));
        await fakeWorker.SendHeartbeatAsync(
            configureHeartbeat: heartbeat => heartbeat.WorkerProcessId = 2468);

        await WaitUntilAsync(
            () => client.ProcessId == 2468 && client.LastHeartbeatAt > previousHeartbeat,
            TestTimeout);

        Assert.Equal(WorkerClientState.Ready, client.State);
        Assert.Equal(previousHeartbeat + TimeSpan.FromSeconds(1), client.LastHeartbeatAt);
    }

    /// <summary>Verifies that a hung worker times out pending command invocations.</summary>
    /// <returns>A task that represents the asynchronous operation.</returns>
    [Fact]
    public async Task InvokeAsync_WithHungWorker_TimesOutPendingCommand()
    {
        await using FakeWorkerHarness fakeWorker = await FakeWorkerHarness.CreateConnectedPairAsync();
        await using WorkerClient client = fakeWorker.CreateClient();
        await StartClientAsync(fakeWorker, client);

        Task<WorkerCommandReply> invokeTask = client.InvokeAsync(
            CreateCommand(MxCommandKind.Ping),
            TimeSpan.FromMilliseconds(50),
            CancellationToken.None);
        WorkerEnvelope commandEnvelope = await fakeWorker.ReadCommandAsync();

        WorkerClientException exception = await Assert.ThrowsAsync<WorkerClientException>(
            async () => await invokeTask.WaitAsync(TestTimeout));

        Assert.Equal(WorkerEnvelope.BodyOneofCase.WorkerCommand, commandEnvelope.BodyCase);
        Assert.Equal(WorkerClientErrorCode.CommandTimeout, exception.ErrorCode);
    }

    /// <summary>Verifies that a malformed frame in the read loop faults the client.</summary>
    /// <returns>A task that represents the asynchronous operation.</returns>
    [Fact]
    public async Task ReadLoop_WithMalformedFrame_FaultsClient()
    {
        await using FakeWorkerHarness fakeWorker = await FakeWorkerHarness.CreateConnectedPairAsync();
        await using WorkerClient client = fakeWorker.CreateClient();
        await StartClientAsync(fakeWorker, client);

        await fakeWorker.WriteMalformedPayloadAsync(new byte[] { 0x08, 0x96, 0x01 });

        await WaitUntilAsync(
            () => client.State == WorkerClientState.Faulted,
            TestTimeout);

        Assert.Equal(WorkerClientState.Faulted, client.State);
    }

    /// <summary>Verifies that a shutdown acknowledgment from the worker closes the client.</summary>
    /// <returns>A task that represents the asynchronous operation.</returns>
    [Fact]
    public async Task ShutdownAsync_WithShutdownAck_ClosesClient()
    {
        await using FakeWorkerHarness fakeWorker = await FakeWorkerHarness.CreateConnectedPairAsync();
        await using WorkerClient client = fakeWorker.CreateClient();
        await StartClientAsync(fakeWorker, client);

        Task shutdownTask = client.ShutdownAsync(TestTimeout, CancellationToken.None);
        WorkerEnvelope shutdownEnvelope = await fakeWorker.ReadShutdownAsync();
        await fakeWorker.SendShutdownAckAsync();
        await shutdownTask.WaitAsync(TestTimeout);

        Assert.Equal(WorkerEnvelope.BodyOneofCase.WorkerShutdown, shutdownEnvelope.BodyCase);
        Assert.Equal(WorkerClientState.Closed, client.State);
    }

    /// <summary>
    /// Verifies that RespondToControlCommandAsync echoes the Ping message back
    /// in the DiagnosticMessage field, matching the real worker's ping reply shape.
    /// </summary>
    /// <returns>A task that represents the asynchronous operation.</returns>
    [Fact]
    public async Task RespondToControlCommandAsync_Ping_EchoesMessageInDiagnostic()
    {
        await using FakeWorkerHarness fakeWorker = await FakeWorkerHarness.CreateConnectedPairAsync();
        await using WorkerClient client = fakeWorker.CreateClient();
        await StartClientAsync(fakeWorker, client);

        Task<WorkerCommandReply> invokeTask = client.InvokeAsync(
            CreateCommand(MxCommandKind.Ping, cmd => cmd.Ping = new PingCommand { Message = "hello-ping" }),
            TestTimeout,
            CancellationToken.None);
        await fakeWorker.RespondToControlCommandAsync().WaitAsync(TestTimeout);

        WorkerCommandReply reply = await invokeTask.WaitAsync(TestTimeout);

        Assert.Equal(MxCommandKind.Ping, reply.Reply.Kind);
        Assert.Equal(ProtocolStatusCode.Ok, reply.Reply.ProtocolStatus.Code);
        Assert.Equal("hello-ping", reply.Reply.DiagnosticMessage);
    }

    /// <summary>
    /// Verifies that RespondToControlCommandAsync returns a SessionStateReply
    /// with state Ready for a GetSessionState command.
    /// </summary>
    /// <returns>A task that represents the asynchronous operation.</returns>
    [Fact]
    public async Task RespondToControlCommandAsync_GetSessionState_ReturnsReadyState()
    {
        await using FakeWorkerHarness fakeWorker = await FakeWorkerHarness.CreateConnectedPairAsync();
        await using WorkerClient client = fakeWorker.CreateClient();
        await StartClientAsync(fakeWorker, client);

        Task<WorkerCommandReply> invokeTask = client.InvokeAsync(
            CreateCommand(MxCommandKind.GetSessionState),
            TestTimeout,
            CancellationToken.None);
        await fakeWorker.RespondToControlCommandAsync().WaitAsync(TestTimeout);

        WorkerCommandReply reply = await invokeTask.WaitAsync(TestTimeout);

        Assert.Equal(MxCommandKind.GetSessionState, reply.Reply.Kind);
        Assert.Equal(ProtocolStatusCode.Ok, reply.Reply.ProtocolStatus.Code);
        Assert.NotNull(reply.Reply.SessionState);
        Assert.Equal(SessionState.Ready, reply.Reply.SessionState.State);
    }

    /// <summary>
    /// Verifies that RespondToControlCommandAsync returns a WorkerInfoReply
    /// with the fake worker's process ID, version, and MXAccess identifiers.
    /// </summary>
    /// <returns>A task that represents the asynchronous operation.</returns>
    [Fact]
    public async Task RespondToControlCommandAsync_GetWorkerInfo_ReturnsFakeWorkerInfo()
    {
        await using FakeWorkerHarness fakeWorker = await FakeWorkerHarness.CreateConnectedPairAsync();
        await using WorkerClient client = fakeWorker.CreateClient();
        await StartClientAsync(fakeWorker, client);

        Task<WorkerCommandReply> invokeTask = client.InvokeAsync(
            CreateCommand(MxCommandKind.GetWorkerInfo),
            TestTimeout,
            CancellationToken.None);
        await fakeWorker.RespondToControlCommandAsync().WaitAsync(TestTimeout);

        WorkerCommandReply reply = await invokeTask.WaitAsync(TestTimeout);

        Assert.Equal(MxCommandKind.GetWorkerInfo, reply.Reply.Kind);
        Assert.Equal(ProtocolStatusCode.Ok, reply.Reply.ProtocolStatus.Code);
        Assert.NotNull(reply.Reply.WorkerInfo);
        Assert.Equal(FakeWorkerHarness.DefaultWorkerProcessId, reply.Reply.WorkerInfo.WorkerProcessId);
        Assert.Equal("LMXProxy.LMXProxyServer.1", reply.Reply.WorkerInfo.MxaccessProgid);
        Assert.Equal("{C30B52F5-2CB5-4760-AF0A-3A344A7EB5DC}", reply.Reply.WorkerInfo.MxaccessClsid);
        Assert.Equal("fake-worker", reply.Reply.WorkerInfo.WorkerVersion);
    }

    /// <summary>
    /// Verifies that RespondToControlCommandAsync returns an empty DrainEventsReply
    /// for a DrainEvents command (the fake harness has no queued events).
    /// </summary>
    /// <returns>A task that represents the asynchronous operation.</returns>
    [Fact]
    public async Task RespondToControlCommandAsync_DrainEvents_ReturnsEmptyReply()
    {
        await using FakeWorkerHarness fakeWorker = await FakeWorkerHarness.CreateConnectedPairAsync();
        await using WorkerClient client = fakeWorker.CreateClient();
        await StartClientAsync(fakeWorker, client);

        Task<WorkerCommandReply> invokeTask = client.InvokeAsync(
            CreateCommand(MxCommandKind.DrainEvents, cmd => cmd.DrainEvents = new DrainEventsCommand { MaxEvents = 32 }),
            TestTimeout,
            CancellationToken.None);
        await fakeWorker.RespondToControlCommandAsync().WaitAsync(TestTimeout);

        WorkerCommandReply reply = await invokeTask.WaitAsync(TestTimeout);

        Assert.Equal(MxCommandKind.DrainEvents, reply.Reply.Kind);
        Assert.Equal(ProtocolStatusCode.Ok, reply.Reply.ProtocolStatus.Code);
        Assert.NotNull(reply.Reply.DrainEvents);
        Assert.Empty(reply.Reply.DrainEvents.Events);
    }

    /// <summary>
    /// Verifies that RespondToControlCommandAsync for ShutdownWorker sends an OK
    /// reply followed by a WorkerShutdownAck, which closes the client.
    /// </summary>
    /// <returns>A task that represents the asynchronous operation.</returns>
    [Fact]
    public async Task RespondToControlCommandAsync_ShutdownWorker_SendsReplyThenAck()
    {
        await using FakeWorkerHarness fakeWorker = await FakeWorkerHarness.CreateConnectedPairAsync();
        await using WorkerClient client = fakeWorker.CreateClient();
        await StartClientAsync(fakeWorker, client);

        // ShutdownAsync triggers a WorkerShutdown envelope (not WorkerCommand),
        // so we directly invoke ShutdownWorker as a control command via InvokeAsync.
        Task<WorkerCommandReply> invokeTask = client.InvokeAsync(
            CreateCommand(MxCommandKind.ShutdownWorker, cmd => cmd.ShutdownWorker = new ShutdownWorkerCommand()),
            TestTimeout,
            CancellationToken.None);

        // The harness reads the ShutdownWorker WorkerCommand and replies with
        // OK + ShutdownAck — the WorkerClient's read loop processes the ack and
        // transitions to Closed.
        await fakeWorker.RespondToControlCommandAsync().WaitAsync(TestTimeout);

        WorkerCommandReply reply = await invokeTask.WaitAsync(TestTimeout);

        Assert.Equal(MxCommandKind.ShutdownWorker, reply.Reply.Kind);
        Assert.Equal(ProtocolStatusCode.Ok, reply.Reply.ProtocolStatus.Code);

        await WaitUntilAsync(() => client.State == WorkerClientState.Closed, TestTimeout);
        Assert.Equal(WorkerClientState.Closed, client.State);
    }

    private static async Task StartClientAsync(
        FakeWorkerHarness fakeWorker,
        WorkerClient client)
    {
        Task startTask = client.StartAsync(CancellationToken.None);
        await fakeWorker.CompleteStartupAsync().ConfigureAwait(false);
        await startTask.WaitAsync(TestTimeout).ConfigureAwait(false);
    }

    private static WorkerCommand CreateCommand(
        MxCommandKind kind,
        Action<MxCommand>? configure = null)
    {
        MxCommand command = new() { Kind = kind };
        configure?.Invoke(command);
        return new WorkerCommand { Command = command };
    }

    private static async Task WaitUntilAsync(
        Func<bool> predicate,
        TimeSpan timeout)
    {
        using CancellationTokenSource cancellationTokenSource = new(timeout);
        while (!predicate())
        {
            await Task.Delay(TimeSpan.FromMilliseconds(10), cancellationTokenSource.Token);
        }
    }

}
