using ZB.MOM.WW.MxGateway.Contracts.Proto;
using Grpc.Core;

namespace ZB.MOM.WW.MxGateway.Client.Tests;

/// <summary>Tests for MxGatewaySession and client command behavior.</summary>
public sealed class MxGatewayClientSessionTests
{
    /// <summary>Verifies that open session attaches API key metadata and cancellation token.</summary>
    /// <returns>A task that represents the asynchronous operation.</returns>
    [Fact]
    public async Task OpenSessionRawAsync_AttachesApiKeyMetadataAndCancellation()
    {
        using CancellationTokenSource cancellation = new();
        FakeGatewayTransport transport = CreateTransport();
        await using MxGatewayClient client = CreateClient(transport);

        await client.OpenSessionRawAsync(new OpenSessionRequest(), cancellation.Token);

        var call = Assert.Single(transport.OpenSessionCalls);
        Assert.Equal("Bearer test-api-key", call.CallOptions.Headers?.GetValue("authorization"));
        Assert.Equal(cancellation.Token, call.CallOptions.CancellationToken);
    }

    /// <summary>Verifies that open session returns a session with the raw open reply.</summary>
    /// <returns>A task that represents the asynchronous operation.</returns>
    [Fact]
    public async Task OpenSessionAsync_ReturnsSessionWithRawOpenReply()
    {
        FakeGatewayTransport transport = CreateTransport();
        transport.OpenSessionReply.WorkerProcessId = 1234;
        await using MxGatewayClient client = CreateClient(transport);

        MxGatewaySession session = await client.OpenSessionAsync();

        Assert.Equal("session-fixture", session.SessionId);
        Assert.Same(transport.OpenSessionReply, session.OpenSessionReply);
        Assert.Equal(1234, session.OpenSessionReply.WorkerProcessId);
    }

    /// <summary>Verifies that register builds a register command and returns server handle.</summary>
    /// <returns>A task that represents the asynchronous operation.</returns>
    [Fact]
    public async Task RegisterAsync_BuildsRegisterCommandAndReturnsServerHandle()
    {
        FakeGatewayTransport transport = CreateTransport();
        transport.AddInvokeReply(new MxCommandReply
        {
            SessionId = "session-fixture",
            Kind = MxCommandKind.Register,
            ProtocolStatus = new ProtocolStatus { Code = ProtocolStatusCode.Ok },
            Register = new RegisterReply { ServerHandle = 12 },
        });
        await using MxGatewayClient client = CreateClient(transport);
        MxGatewaySession session = await client.OpenSessionAsync();

        int serverHandle = await session.RegisterAsync("fixture-client");

        Assert.Equal(12, serverHandle);
        var call = Assert.Single(transport.InvokeCalls);
        Assert.Equal("session-fixture", call.Request.SessionId);
        Assert.False(string.IsNullOrWhiteSpace(call.Request.ClientCorrelationId));
        Assert.Equal(MxCommandKind.Register, call.Request.Command.Kind);
        Assert.Equal("fixture-client", call.Request.Command.Register.ClientName);
    }

    /// <summary>Verifies that add item 2 builds a command with the specified context.</summary>
    /// <returns>A task that represents the asynchronous operation.</returns>
    [Fact]
    public async Task AddItem2Async_BuildsAddItem2CommandWithContext()
    {
        FakeGatewayTransport transport = CreateTransport();
        transport.AddInvokeReply(new MxCommandReply
        {
            SessionId = "session-fixture",
            Kind = MxCommandKind.AddItem2,
            ProtocolStatus = new ProtocolStatus { Code = ProtocolStatusCode.Ok },
            AddItem2 = new AddItem2Reply { ItemHandle = 34 },
        });
        await using MxGatewayClient client = CreateClient(transport);
        MxGatewaySession session = await client.OpenSessionAsync();

        int itemHandle = await session.AddItem2Async(12, "Area001.Pump001.Speed", "runtime");

        Assert.Equal(34, itemHandle);
        MxCommandRequest request = Assert.Single(transport.InvokeCalls).Request;
        Assert.Equal(MxCommandKind.AddItem2, request.Command.Kind);
        Assert.Equal(12, request.Command.AddItem2.ServerHandle);
        Assert.Equal("Area001.Pump001.Speed", request.Command.AddItem2.ItemDefinition);
        Assert.Equal("runtime", request.Command.AddItem2.ItemContext);
    }

    /// <summary>Verifies that write raw builds a write command with the raw value.</summary>
    /// <returns>A task that represents the asynchronous operation.</returns>
    [Fact]
    public async Task WriteRawAsync_BuildsWriteCommandWithRawValue()
    {
        FakeGatewayTransport transport = CreateTransport();
        transport.AddInvokeReply(new MxCommandReply
        {
            SessionId = "session-fixture",
            Kind = MxCommandKind.Write,
            ProtocolStatus = new ProtocolStatus { Code = ProtocolStatusCode.Ok },
        });
        await using MxGatewayClient client = CreateClient(transport);
        MxGatewaySession session = await client.OpenSessionAsync();
        MxValue value = new()
        {
            DataType = MxDataType.Integer,
            VariantType = "VT_I4",
            Int32Value = 123,
        };

        MxCommandReply reply = await session.WriteRawAsync(12, 34, value, 56);

        Assert.Equal(MxCommandKind.Write, reply.Kind);
        MxCommandRequest request = Assert.Single(transport.InvokeCalls).Request;
        Assert.Equal(MxCommandKind.Write, request.Command.Kind);
        Assert.Equal(12, request.Command.Write.ServerHandle);
        Assert.Equal(34, request.Command.Write.ItemHandle);
        Assert.Same(value, request.Command.Write.Value);
        Assert.Equal(56, request.Command.Write.UserId);
    }

    /// <summary>Verifies that write 2 raw builds a write 2 command with value and timestamp.</summary>
    /// <returns>A task that represents the asynchronous operation.</returns>
    [Fact]
    public async Task Write2RawAsync_BuildsWrite2CommandWithValueAndTimestamp()
    {
        FakeGatewayTransport transport = CreateTransport();
        transport.AddInvokeReply(new MxCommandReply
        {
            SessionId = "session-fixture",
            Kind = MxCommandKind.Write2,
            ProtocolStatus = new ProtocolStatus { Code = ProtocolStatusCode.Ok },
        });
        await using MxGatewayClient client = CreateClient(transport);
        MxGatewaySession session = await client.OpenSessionAsync();
        MxValue value = 123.ToMxValue();
        MxValue timestampValue = DateTimeOffset.Parse("2026-01-01T00:00:00Z").ToMxValue();

        MxCommandReply reply = await session.Write2RawAsync(12, 34, value, timestampValue, 56);

        Assert.Equal(MxCommandKind.Write2, reply.Kind);
        MxCommandRequest request = Assert.Single(transport.InvokeCalls).Request;
        Assert.Equal(MxCommandKind.Write2, request.Command.Kind);
        Assert.Equal(12, request.Command.Write2.ServerHandle);
        Assert.Equal(34, request.Command.Write2.ItemHandle);
        Assert.Same(value, request.Command.Write2.Value);
        Assert.Same(timestampValue, request.Command.Write2.TimestampValue);
        Assert.Equal(56, request.Command.Write2.UserId);
    }

    /// <summary>Verifies that subscribe bulk builds one command and returns per-item results.</summary>
    /// <returns>A task that represents the asynchronous operation.</returns>
    [Fact]
    public async Task SubscribeBulkAsync_BuildsOneBulkCommandAndReturnsPerItemResults()
    {
        FakeGatewayTransport transport = CreateTransport();
        transport.AddInvokeReply(new MxCommandReply
        {
            SessionId = "session-fixture",
            Kind = MxCommandKind.SubscribeBulk,
            ProtocolStatus = new ProtocolStatus { Code = ProtocolStatusCode.Ok },
            SubscribeBulk = new BulkSubscribeReply
            {
                Results =
                {
                    new SubscribeResult
                    {
                        ServerHandle = 12,
                        TagAddress = "Area001.Pump001.Speed",
                        ItemHandle = 34,
                        WasSuccessful = true,
                    },
                },
            },
        });
        await using MxGatewayClient client = CreateClient(transport);
        MxGatewaySession session = await client.OpenSessionAsync();

        IReadOnlyList<SubscribeResult> results = await session.SubscribeBulkAsync(
            12,
            ["Area001.Pump001.Speed"]);

        SubscribeResult result = Assert.Single(results);
        Assert.Equal(34, result.ItemHandle);
        MxCommandRequest request = Assert.Single(transport.InvokeCalls).Request;
        Assert.Equal(MxCommandKind.SubscribeBulk, request.Command.Kind);
        Assert.Equal(12, request.Command.SubscribeBulk.ServerHandle);
        Assert.Equal(["Area001.Pump001.Speed"], request.Command.SubscribeBulk.TagAddresses);
    }

    /// <summary>Verifies that stream events yields events in the order received from the gateway.</summary>
    /// <returns>A task that represents the asynchronous operation.</returns>
    [Fact]
    public async Task StreamEventsAsync_YieldsEventsInGatewayOrder()
    {
        FakeGatewayTransport transport = CreateTransport();
        transport.AddEvent(new MxEvent
        {
            SessionId = "session-fixture",
            Family = MxEventFamily.OnDataChange,
            WorkerSequence = 1,
        });
        transport.AddEvent(new MxEvent
        {
            SessionId = "session-fixture",
            Family = MxEventFamily.OnWriteComplete,
            WorkerSequence = 2,
        });
        await using MxGatewayClient client = CreateClient(transport);
        MxGatewaySession session = await client.OpenSessionAsync();
        List<ulong> sequences = [];

        await foreach (MxEvent gatewayEvent in session.StreamEventsAsync(afterWorkerSequence: 0))
        {
            sequences.Add(gatewayEvent.WorkerSequence);
        }

        Assert.Equal([1UL, 2UL], sequences);
        StreamEventsRequest request = Assert.Single(transport.StreamEventsCalls).Request;
        Assert.Equal("session-fixture", request.SessionId);
    }

    /// <summary>
    /// Verifies that a reconnect-replay gap sentinel is surfaced as a typed
    /// <see cref="MxEventStreamItem"/> with the gap populated, while normal
    /// events pass through unchanged with IsReplayGap false.
    /// </summary>
    [Fact]
    public async Task StreamEventItemsAsync_SurfacesReplayGapSentinel()
    {
        FakeGatewayTransport transport = CreateTransport();
        transport.AddEvent(new MxEvent
        {
            SessionId = "session-fixture",
            ReplayGap = new ReplayGap
            {
                RequestedAfterSequence = 5,
                OldestAvailableSequence = 42,
            },
        });
        transport.AddEvent(new MxEvent
        {
            SessionId = "session-fixture",
            Family = MxEventFamily.OnDataChange,
            WorkerSequence = 42,
        });
        await using MxGatewayClient client = CreateClient(transport);
        MxGatewaySession session = await client.OpenSessionAsync();

        List<MxEventStreamItem> items = [];
        await foreach (MxEventStreamItem item in session.StreamEventItemsAsync(afterWorkerSequence: 5))
        {
            items.Add(item);
        }

        Assert.Equal(2, items.Count);

        MxEventStreamItem gap = items[0];
        Assert.True(gap.IsReplayGap);
        Assert.NotNull(gap.ReplayGap);
        Assert.Equal(5UL, gap.ReplayGap!.RequestedAfterSequence);
        Assert.Equal(42UL, gap.ReplayGap.OldestAvailableSequence);
        Assert.Same(gap.ReplayGap, gap.Event.ReplayGap);

        MxEventStreamItem normal = items[1];
        Assert.False(normal.IsReplayGap);
        Assert.Null(normal.ReplayGap);
        Assert.Equal(42UL, normal.Event.WorkerSequence);
        Assert.Equal(MxEventFamily.OnDataChange, normal.Event.Family);
    }

    /// <summary>Verifies that close is explicit and idempotent.</summary>
    /// <returns>A task that represents the asynchronous operation.</returns>
    [Fact]
    public async Task CloseAsync_IsExplicitAndIdempotent()
    {
        FakeGatewayTransport transport = CreateTransport();
        await using MxGatewayClient client = CreateClient(transport);
        MxGatewaySession session = await client.OpenSessionAsync();

        CloseSessionReply first = await session.CloseAsync();
        CloseSessionReply second = await session.CloseAsync();

        Assert.Same(first, second);
        var call = Assert.Single(transport.CloseSessionCalls);
        Assert.Equal("session-fixture", call.Request.SessionId);
    }

    /// <summary>Verifies that invoke retries safe diagnostic commands on transient RPC failure.</summary>
    /// <returns>A task that represents the asynchronous operation.</returns>
    [Fact]
    public async Task InvokeAsync_RetriesSafeDiagnosticCommandOnTransientGrpcFailure()
    {
        FakeGatewayTransport transport = CreateTransport();
        transport.InvokeExceptions.Enqueue(CreateTransientRpcException());
        transport.AddInvokeReply(new MxCommandReply
        {
            SessionId = "session-fixture",
            Kind = MxCommandKind.Ping,
            ProtocolStatus = new ProtocolStatus { Code = ProtocolStatusCode.Ok },
        });
        await using MxGatewayClient client = CreateClient(transport);
        MxGatewaySession session = await client.OpenSessionAsync();

        await session.InvokeAsync(new MxCommandRequest
        {
            SessionId = session.SessionId,
            Command = new MxCommand { Kind = MxCommandKind.Ping, Ping = new PingCommand() },
        });

        Assert.Equal(2, transport.InvokeCalls.Count);
    }

    /// <summary>Verifies that open session does not retry on transient RPC failure.</summary>
    /// <returns>A task that represents the asynchronous operation.</returns>
    [Fact]
    public async Task OpenSessionAsync_DoesNotRetryTransientGrpcFailure()
    {
        FakeGatewayTransport transport = CreateTransport();
        transport.OpenSessionExceptions.Enqueue(CreateTransientRpcException());
        await using MxGatewayClient client = CreateClient(transport);

        await Assert.ThrowsAsync<RpcException>(async () => await client.OpenSessionAsync());

        Assert.Single(transport.OpenSessionCalls);
    }

    /// <summary>Verifies that invoke does not retry write commands on transient RPC failure.</summary>
    /// <returns>A task that represents the asynchronous operation.</returns>
    [Fact]
    public async Task InvokeAsync_DoesNotRetryWriteCommand()
    {
        FakeGatewayTransport transport = CreateTransport();
        transport.InvokeExceptions.Enqueue(CreateTransientRpcException());
        await using MxGatewayClient client = CreateClient(transport);
        MxGatewaySession session = await client.OpenSessionAsync();

        await Assert.ThrowsAsync<RpcException>(async () =>
            await session.WriteRawAsync(1, 2, 3.ToMxValue(), userId: 0));

        Assert.Single(transport.InvokeCalls);
    }

    /// <summary>Verifies that invoke helpers pass cancellation token to the transport.</summary>
    /// <returns>A task that represents the asynchronous operation.</returns>
    [Fact]
    public async Task InvokeHelpers_PassCancellationTokenToTransport()
    {
        using CancellationTokenSource cancellation = new();
        FakeGatewayTransport transport = CreateTransport();
        transport.AddInvokeReply(new MxCommandReply
        {
            SessionId = "session-fixture",
            Kind = MxCommandKind.Advise,
            ProtocolStatus = new ProtocolStatus { Code = ProtocolStatusCode.Ok },
        });
        await using MxGatewayClient client = CreateClient(transport);
        MxGatewaySession session = await client.OpenSessionAsync();

        await session.AdviseAsync(12, 34, cancellation.Token);

        Assert.Equal(cancellation.Token, Assert.Single(transport.InvokeCalls).CallOptions.CancellationToken);
    }

    /// <summary>Verifies that BuildSparseArray produces a SparseArrayValue MxValue with the correct total length and elements.</summary>
    [Fact]
    public void BuildSparseArray_ProducesSparseArrayValueWithCorrectTotalLengthAndElements()
    {
        MxValue element0 = 42.ToMxValue();
        MxValue element3 = 99.ToMxValue();
        Dictionary<uint, MxValue> elements = new()
        {
            [0u] = element0,
            [3u] = element3,
        };

        MxValue result = MxGatewaySession.BuildSparseArray(MxDataType.Integer, totalLength: 10, elements);

        Assert.Equal(MxValue.KindOneofCase.SparseArrayValue, result.KindCase);
        Assert.Equal(10u, result.SparseArrayValue.TotalLength);
        Assert.Equal(MxDataType.Integer, result.SparseArrayValue.ElementDataType);
        Assert.Equal(2, result.SparseArrayValue.Elements.Count);

        MxSparseElement el0 = Assert.Single(result.SparseArrayValue.Elements, e => e.Index == 0u);
        Assert.Same(element0, el0.Value);

        MxSparseElement el3 = Assert.Single(result.SparseArrayValue.Elements, e => e.Index == 3u);
        Assert.Same(element3, el3.Value);
    }

    /// <summary>Verifies that WriteArrayElementsAsync builds a write command whose value is a SparseArrayValue.</summary>
    /// <returns>A task that represents the asynchronous operation.</returns>
    [Fact]
    public async Task WriteArrayElementsAsync_BuildsWriteCommandWithSparseArrayValue()
    {
        FakeGatewayTransport transport = CreateTransport();
        transport.AddInvokeReply(new MxCommandReply
        {
            SessionId = "session-fixture",
            Kind = MxCommandKind.Write,
            ProtocolStatus = new ProtocolStatus { Code = ProtocolStatusCode.Ok },
        });
        await using MxGatewayClient client = CreateClient(transport);
        MxGatewaySession session = await client.OpenSessionAsync();
        Dictionary<uint, MxValue> elements = new() { [1u] = 7.ToMxValue() };

        await session.WriteArrayElementsAsync(
            serverHandle: 12,
            itemHandle: 34,
            elementDataType: MxDataType.Integer,
            totalLength: 5,
            elements: elements,
            userId: 56);

        MxCommandRequest request = Assert.Single(transport.InvokeCalls).Request;
        Assert.Equal(MxCommandKind.Write, request.Command.Kind);
        Assert.Equal(12, request.Command.Write.ServerHandle);
        Assert.Equal(34, request.Command.Write.ItemHandle);
        Assert.Equal(56, request.Command.Write.UserId);
        MxValue written = request.Command.Write.Value;
        Assert.Equal(MxValue.KindOneofCase.SparseArrayValue, written.KindCase);
        Assert.Equal(5u, written.SparseArrayValue.TotalLength);
        Assert.Equal(MxDataType.Integer, written.SparseArrayValue.ElementDataType);
        MxSparseElement el = Assert.Single(written.SparseArrayValue.Elements);
        Assert.Equal(1u, el.Index);
        Assert.Equal(7, el.Value.Int32Value);
    }

    /// <summary>Verifies that unregister builds an unregister command with the server handle.</summary>
    [Fact]
    public async Task UnregisterAsync_BuildsUnregisterCommand()
    {
        FakeGatewayTransport transport = CreateTransport();
        transport.AddInvokeReply(new MxCommandReply
        {
            SessionId = "session-fixture",
            Kind = MxCommandKind.Unregister,
            ProtocolStatus = new ProtocolStatus { Code = ProtocolStatusCode.Ok },
        });
        await using MxGatewayClient client = CreateClient(transport);
        MxGatewaySession session = await client.OpenSessionAsync();

        await session.UnregisterAsync(12);

        MxCommandRequest request = Assert.Single(transport.InvokeCalls).Request;
        Assert.Equal(MxCommandKind.Unregister, request.Command.Kind);
        Assert.Equal(12, request.Command.Unregister.ServerHandle);
    }

    /// <summary>Verifies that advise-supervisory builds the supervisory advise command.</summary>
    [Fact]
    public async Task AdviseSupervisoryAsync_BuildsAdviseSupervisoryCommand()
    {
        FakeGatewayTransport transport = CreateTransport();
        transport.AddInvokeReply(new MxCommandReply
        {
            SessionId = "session-fixture",
            Kind = MxCommandKind.AdviseSupervisory,
            ProtocolStatus = new ProtocolStatus { Code = ProtocolStatusCode.Ok },
        });
        await using MxGatewayClient client = CreateClient(transport);
        MxGatewaySession session = await client.OpenSessionAsync();

        await session.AdviseSupervisoryAsync(12, 34);

        MxCommandRequest request = Assert.Single(transport.InvokeCalls).Request;
        Assert.Equal(MxCommandKind.AdviseSupervisory, request.Command.Kind);
        Assert.Equal(12, request.Command.AdviseSupervisory.ServerHandle);
        Assert.Equal(34, request.Command.AdviseSupervisory.ItemHandle);
    }

    /// <summary>Verifies that add-buffered-item returns the item handle from the typed reply.</summary>
    [Fact]
    public async Task AddBufferedItemAsync_BuildsCommandAndReturnsItemHandle()
    {
        FakeGatewayTransport transport = CreateTransport();
        transport.AddInvokeReply(new MxCommandReply
        {
            SessionId = "session-fixture",
            Kind = MxCommandKind.AddBufferedItem,
            ProtocolStatus = new ProtocolStatus { Code = ProtocolStatusCode.Ok },
            AddBufferedItem = new AddBufferedItemReply { ItemHandle = 77 },
        });
        await using MxGatewayClient client = CreateClient(transport);
        MxGatewaySession session = await client.OpenSessionAsync();

        int itemHandle = await session.AddBufferedItemAsync(12, "Area001.Pump001.Speed", "runtime");

        Assert.Equal(77, itemHandle);
        MxCommandRequest request = Assert.Single(transport.InvokeCalls).Request;
        Assert.Equal(MxCommandKind.AddBufferedItem, request.Command.Kind);
        Assert.Equal(12, request.Command.AddBufferedItem.ServerHandle);
        Assert.Equal("Area001.Pump001.Speed", request.Command.AddBufferedItem.ItemDefinition);
        Assert.Equal("runtime", request.Command.AddBufferedItem.ItemContext);
    }

    /// <summary>Verifies that set-buffered-update-interval builds the command with the interval.</summary>
    [Fact]
    public async Task SetBufferedUpdateIntervalAsync_BuildsCommandWithInterval()
    {
        FakeGatewayTransport transport = CreateTransport();
        transport.AddInvokeReply(new MxCommandReply
        {
            SessionId = "session-fixture",
            Kind = MxCommandKind.SetBufferedUpdateInterval,
            ProtocolStatus = new ProtocolStatus { Code = ProtocolStatusCode.Ok },
        });
        await using MxGatewayClient client = CreateClient(transport);
        MxGatewaySession session = await client.OpenSessionAsync();

        await session.SetBufferedUpdateIntervalAsync(12, 500);

        MxCommandRequest request = Assert.Single(transport.InvokeCalls).Request;
        Assert.Equal(MxCommandKind.SetBufferedUpdateInterval, request.Command.Kind);
        Assert.Equal(12, request.Command.SetBufferedUpdateInterval.ServerHandle);
        Assert.Equal(500, request.Command.SetBufferedUpdateInterval.UpdateIntervalMilliseconds);
    }

    /// <summary>Verifies that suspend builds the command and returns the reply status.</summary>
    [Fact]
    public async Task SuspendAsync_BuildsCommandAndReturnsStatus()
    {
        FakeGatewayTransport transport = CreateTransport();
        transport.AddInvokeReply(new MxCommandReply
        {
            SessionId = "session-fixture",
            Kind = MxCommandKind.Suspend,
            ProtocolStatus = new ProtocolStatus { Code = ProtocolStatusCode.Ok },
            Suspend = new SuspendReply { Status = new MxStatusProxy { Success = 1, Category = MxStatusCategory.Ok } },
        });
        await using MxGatewayClient client = CreateClient(transport);
        MxGatewaySession session = await client.OpenSessionAsync();

        MxStatusProxy? status = await session.SuspendAsync(12, 34);

        Assert.NotNull(status);
        Assert.Equal(1, status!.Success);
        MxCommandRequest request = Assert.Single(transport.InvokeCalls).Request;
        Assert.Equal(MxCommandKind.Suspend, request.Command.Kind);
        Assert.Equal(12, request.Command.Suspend.ServerHandle);
        Assert.Equal(34, request.Command.Suspend.ItemHandle);
    }

    /// <summary>Verifies that activate builds the command and returns the reply status.</summary>
    [Fact]
    public async Task ActivateAsync_BuildsCommandAndReturnsStatus()
    {
        FakeGatewayTransport transport = CreateTransport();
        transport.AddInvokeReply(new MxCommandReply
        {
            SessionId = "session-fixture",
            Kind = MxCommandKind.Activate,
            ProtocolStatus = new ProtocolStatus { Code = ProtocolStatusCode.Ok },
            Activate = new ActivateReply { Status = new MxStatusProxy { Success = 1, Category = MxStatusCategory.Ok } },
        });
        await using MxGatewayClient client = CreateClient(transport);
        MxGatewaySession session = await client.OpenSessionAsync();

        MxStatusProxy? status = await session.ActivateAsync(12, 34);

        Assert.NotNull(status);
        MxCommandRequest request = Assert.Single(transport.InvokeCalls).Request;
        Assert.Equal(MxCommandKind.Activate, request.Command.Kind);
        Assert.Equal(34, request.Command.Activate.ItemHandle);
    }

    /// <summary>Verifies that write-secured builds a WriteSecured command with value and user ids.</summary>
    [Fact]
    public async Task WriteSecuredAsync_BuildsWriteSecuredCommand()
    {
        FakeGatewayTransport transport = CreateTransport();
        transport.AddInvokeReply(new MxCommandReply
        {
            SessionId = "session-fixture",
            Kind = MxCommandKind.WriteSecured,
            ProtocolStatus = new ProtocolStatus { Code = ProtocolStatusCode.Ok },
        });
        await using MxGatewayClient client = CreateClient(transport);
        MxGatewaySession session = await client.OpenSessionAsync();
        MxValue value = 123.ToMxValue();

        await session.WriteSecuredAsync(12, 34, value, currentUserId: 5, verifierUserId: 6);

        MxCommandRequest request = Assert.Single(transport.InvokeCalls).Request;
        Assert.Equal(MxCommandKind.WriteSecured, request.Command.Kind);
        Assert.Equal(12, request.Command.WriteSecured.ServerHandle);
        Assert.Equal(34, request.Command.WriteSecured.ItemHandle);
        Assert.Same(value, request.Command.WriteSecured.Value);
        Assert.Equal(5, request.Command.WriteSecured.CurrentUserId);
        Assert.Equal(6, request.Command.WriteSecured.VerifierUserId);
    }

    /// <summary>
    /// MXAccess parity: WriteSecured issued before a prior AuthenticateUser fails
    /// natively. The client must surface that scripted failure (as an
    /// <see cref="MxAccessException"/>) rather than pre-validating it away.
    /// </summary>
    [Fact]
    public async Task WriteSecuredAsync_SurfacesNativeFailureWhenNotAuthenticated()
    {
        FakeGatewayTransport transport = CreateTransport();
        transport.AddInvokeReply(new MxCommandReply
        {
            SessionId = "session-fixture",
            Kind = MxCommandKind.WriteSecured,
            ProtocolStatus = new ProtocolStatus { Code = ProtocolStatusCode.MxaccessFailure },
            Hresult = unchecked((int)0x80040200),
        });
        await using MxGatewayClient client = CreateClient(transport);
        MxGatewaySession session = await client.OpenSessionAsync();

        MxAccessException exception = await Assert.ThrowsAsync<MxAccessException>(
            async () => await session.WriteSecuredAsync(12, 34, 123.ToMxValue(), currentUserId: 0, verifierUserId: 0));

        // The native HRESULT is surfaced; the request payload is never in the message.
        Assert.Contains("WriteSecured", exception.Message);
        Assert.Single(transport.InvokeCalls);
    }

    /// <summary>Verifies that write-secured2 builds a WriteSecured2 command with value and timestamp.</summary>
    [Fact]
    public async Task WriteSecured2Async_BuildsWriteSecured2Command()
    {
        FakeGatewayTransport transport = CreateTransport();
        transport.AddInvokeReply(new MxCommandReply
        {
            SessionId = "session-fixture",
            Kind = MxCommandKind.WriteSecured2,
            ProtocolStatus = new ProtocolStatus { Code = ProtocolStatusCode.Ok },
        });
        await using MxGatewayClient client = CreateClient(transport);
        MxGatewaySession session = await client.OpenSessionAsync();
        MxValue value = 123.ToMxValue();
        MxValue timestampValue = DateTimeOffset.Parse("2026-01-01T00:00:00Z").ToMxValue();

        await session.WriteSecured2Async(12, 34, value, timestampValue, currentUserId: 5, verifierUserId: 6);

        MxCommandRequest request = Assert.Single(transport.InvokeCalls).Request;
        Assert.Equal(MxCommandKind.WriteSecured2, request.Command.Kind);
        Assert.Same(value, request.Command.WriteSecured2.Value);
        Assert.Same(timestampValue, request.Command.WriteSecured2.TimestampValue);
        Assert.Equal(5, request.Command.WriteSecured2.CurrentUserId);
        Assert.Equal(6, request.Command.WriteSecured2.VerifierUserId);
    }

    /// <summary>Verifies that authenticate-user builds the command and returns the resolved user id.</summary>
    [Fact]
    public async Task AuthenticateUserAsync_BuildsCommandAndReturnsUserId()
    {
        FakeGatewayTransport transport = CreateTransport();
        transport.AddInvokeReply(new MxCommandReply
        {
            SessionId = "session-fixture",
            Kind = MxCommandKind.AuthenticateUser,
            ProtocolStatus = new ProtocolStatus { Code = ProtocolStatusCode.Ok },
            AuthenticateUser = new AuthenticateUserReply { UserId = 4242 },
        });
        await using MxGatewayClient client = CreateClient(transport);
        MxGatewaySession session = await client.OpenSessionAsync();

        int userId = await session.AuthenticateUserAsync(12, "operator", "s3cr3t-p@ss");

        Assert.Equal(4242, userId);
        MxCommandRequest request = Assert.Single(transport.InvokeCalls).Request;
        Assert.Equal(MxCommandKind.AuthenticateUser, request.Command.Kind);
        Assert.Equal("operator", request.Command.AuthenticateUser.VerifyUser);
        Assert.Equal("s3cr3t-p@ss", request.Command.AuthenticateUser.VerifyUserPassword);
    }

    /// <summary>
    /// SECRET REDACTION: when AuthenticateUser fails, the surfaced exception message
    /// must never contain the credential — the error path is built only from
    /// reply-derived diagnostics, not the request payload.
    /// </summary>
    [Fact]
    public async Task AuthenticateUserAsync_FailureDoesNotLeakCredentialInErrorMessage()
    {
        const string password = "super-secret-credential-123";
        FakeGatewayTransport transport = CreateTransport();
        transport.AddInvokeReply(new MxCommandReply
        {
            SessionId = "session-fixture",
            Kind = MxCommandKind.AuthenticateUser,
            ProtocolStatus = new ProtocolStatus { Code = ProtocolStatusCode.MxaccessFailure },
            Hresult = unchecked((int)0x80040210),
        });
        await using MxGatewayClient client = CreateClient(transport);
        MxGatewaySession session = await client.OpenSessionAsync();

        MxAccessException exception = await Assert.ThrowsAsync<MxAccessException>(
            async () => await session.AuthenticateUserAsync(12, "operator", password));

        Assert.DoesNotContain(password, exception.Message);
        Assert.DoesNotContain(password, exception.ToString());
    }

    /// <summary>Verifies that archestra-user-to-id builds the command and returns the resolved user id.</summary>
    [Fact]
    public async Task ArchestraUserToIdAsync_BuildsCommandAndReturnsUserId()
    {
        FakeGatewayTransport transport = CreateTransport();
        transport.AddInvokeReply(new MxCommandReply
        {
            SessionId = "session-fixture",
            Kind = MxCommandKind.ArchestraUserToId,
            ProtocolStatus = new ProtocolStatus { Code = ProtocolStatusCode.Ok },
            ArchestraUserToId = new ArchestrAUserToIdReply { UserId = 909 },
        });
        await using MxGatewayClient client = CreateClient(transport);
        MxGatewaySession session = await client.OpenSessionAsync();

        int userId = await session.ArchestraUserToIdAsync(12, "BCC47053-9542-4D65-BDAA-BCDEA6A32A73");

        Assert.Equal(909, userId);
        MxCommandRequest request = Assert.Single(transport.InvokeCalls).Request;
        Assert.Equal(MxCommandKind.ArchestraUserToId, request.Command.Kind);
        Assert.Equal("BCC47053-9542-4D65-BDAA-BCDEA6A32A73", request.Command.ArchestraUserToId.UserIdGuid);
    }

    private static MxGatewayClient CreateClient(FakeGatewayTransport transport)
    {
        return new MxGatewayClient(transport.Options, transport);
    }

    private static FakeGatewayTransport CreateTransport()
    {
        return new FakeGatewayTransport(new MxGatewayClientOptions
        {
            Endpoint = new Uri("http://localhost:5000"),
            ApiKey = "test-api-key",
        });
    }

    private static RpcException CreateTransientRpcException()
    {
        return new RpcException(new Status(StatusCode.Unavailable, "gateway unavailable"));
    }
}
