using Google.Protobuf.WellKnownTypes;
using Microsoft.Extensions.Options;
using ZB.MOM.WW.MxGateway.Contracts.Proto;
using ZB.MOM.WW.MxGateway.Server.Configuration;
using ZB.MOM.WW.MxGateway.Server.Metrics;
using ZB.MOM.WW.MxGateway.Server.Sessions;
using ZB.MOM.WW.MxGateway.Server.Workers;

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

/// <summary>
///     Per-method gateway-side coverage for every
///     <c>GatewaySession.*BulkAsync</c> entry point. Each method gets a
///     round-trip test that pins the <see cref="MxCommandKind"/> sent to the
///     worker, the per-entry payload shape, a failure-mode (per-entry failure
///     surfaced or protocol-status failure) check, and a cancellation-propagation
///     check. The secured-write variants additionally pin that the credential
///     payload (<c>current_user_id</c>, <c>verifier_user_id</c>) is preserved
///     end-to-end and not flattened/redacted by the gateway's command shape.
/// </summary>
public sealed class SessionManagerBulkTests
{
    /// <summary>Verifies that AddItemBulkAsync forwards the command and returns results.</summary>
    /// <returns>A task that represents the asynchronous operation.</returns>
    [Fact]
    public async Task AddItemBulkAsync_ForwardsOneAddItemBulkCommandAndReturnsResults()
    {
        FakeBulkWorkerClient workerClient = WithReply(reply => reply.AddItemBulk = new BulkSubscribeReply
        {
            Results =
            {
                new SubscribeResult { ServerHandle = 12, TagAddress = "Galaxy.Tag.Ok", ItemHandle = 511, WasSuccessful = true },
                new SubscribeResult { ServerHandle = 12, TagAddress = "Galaxy.Tag.Bad", ItemHandle = 0, WasSuccessful = false, ErrorMessage = "invalid tag" },
            },
        }, MxCommandKind.AddItemBulk);
        GatewaySession session = await OpenSessionAsync(workerClient);

        IReadOnlyList<SubscribeResult> results = await session.AddItemBulkAsync(
            12,
            ["Galaxy.Tag.Ok", "Galaxy.Tag.Bad"],
            CancellationToken.None);

        Assert.Equal(1, workerClient.InvokeCount);
        Assert.Equal(MxCommandKind.AddItemBulk, workerClient.LastCommand?.Command.Kind);
        Assert.Equal(12, workerClient.LastCommand?.Command.AddItemBulk.ServerHandle);
        Assert.Equal(["Galaxy.Tag.Ok", "Galaxy.Tag.Bad"], workerClient.LastCommand?.Command.AddItemBulk.TagAddresses);
        Assert.Equal(2, results.Count);
        Assert.True(results[0].WasSuccessful);
        Assert.False(results[1].WasSuccessful);
        Assert.Equal("invalid tag", results[1].ErrorMessage);
    }

    /// <summary>Verifies that AddItemBulkAsync propagates cancellation.</summary>
    /// <returns>A task that represents the asynchronous operation.</returns>
    [Fact]
    public async Task AddItemBulkAsync_PropagatesCancellation()
    {
        FakeBulkWorkerClient workerClient = new();
        GatewaySession session = await OpenSessionAsync(workerClient);
        using CancellationTokenSource cts = new();
        await cts.CancelAsync();

        await Assert.ThrowsAnyAsync<OperationCanceledException>(
            async () => await session.AddItemBulkAsync(12, ["Tag.A"], cts.Token));
    }

    /// <summary>Verifies that AdviseItemBulkAsync forwards the command and returns results.</summary>
    /// <returns>A task that represents the asynchronous operation.</returns>
    [Fact]
    public async Task AdviseItemBulkAsync_ForwardsOneAdviseItemBulkCommandAndReturnsResults()
    {
        FakeBulkWorkerClient workerClient = WithReply(reply => reply.AdviseItemBulk = new BulkSubscribeReply
        {
            Results =
            {
                new SubscribeResult { ServerHandle = 12, ItemHandle = 901, WasSuccessful = true },
                new SubscribeResult { ServerHandle = 12, ItemHandle = 902, WasSuccessful = false, ErrorMessage = "invalid item handle" },
            },
        }, MxCommandKind.AdviseItemBulk);
        GatewaySession session = await OpenSessionAsync(workerClient);

        IReadOnlyList<SubscribeResult> results = await session.AdviseItemBulkAsync(
            12,
            [901, 902],
            CancellationToken.None);

        Assert.Equal(MxCommandKind.AdviseItemBulk, workerClient.LastCommand?.Command.Kind);
        Assert.Equal(12, workerClient.LastCommand?.Command.AdviseItemBulk.ServerHandle);
        Assert.Equal([901, 902], workerClient.LastCommand?.Command.AdviseItemBulk.ItemHandles);
        Assert.Equal(2, results.Count);
        Assert.True(results[0].WasSuccessful);
        Assert.False(results[1].WasSuccessful);
    }

    /// <summary>Verifies that AdviseItemBulkAsync propagates cancellation.</summary>
    /// <returns>A task that represents the asynchronous operation.</returns>
    [Fact]
    public async Task AdviseItemBulkAsync_PropagatesCancellation()
    {
        FakeBulkWorkerClient workerClient = new();
        GatewaySession session = await OpenSessionAsync(workerClient);
        using CancellationTokenSource cts = new();
        await cts.CancelAsync();

        await Assert.ThrowsAnyAsync<OperationCanceledException>(
            async () => await session.AdviseItemBulkAsync(12, [101], cts.Token));
    }

    /// <summary>Verifies that RemoveItemBulkAsync forwards the command and returns results.</summary>
    /// <returns>A task that represents the asynchronous operation.</returns>
    [Fact]
    public async Task RemoveItemBulkAsync_ForwardsOneRemoveItemBulkCommandAndReturnsResults()
    {
        FakeBulkWorkerClient workerClient = WithReply(reply => reply.RemoveItemBulk = new BulkSubscribeReply
        {
            Results =
            {
                new SubscribeResult { ServerHandle = 12, ItemHandle = 11, WasSuccessful = true },
                new SubscribeResult { ServerHandle = 12, ItemHandle = 12, WasSuccessful = false, ErrorMessage = "unknown handle" },
            },
        }, MxCommandKind.RemoveItemBulk);
        GatewaySession session = await OpenSessionAsync(workerClient);

        IReadOnlyList<SubscribeResult> results = await session.RemoveItemBulkAsync(
            12,
            [11, 12],
            CancellationToken.None);

        Assert.Equal(MxCommandKind.RemoveItemBulk, workerClient.LastCommand?.Command.Kind);
        Assert.Equal([11, 12], workerClient.LastCommand?.Command.RemoveItemBulk.ItemHandles);
        Assert.Equal(2, results.Count);
        Assert.False(results[1].WasSuccessful);
    }

    /// <summary>Verifies that RemoveItemBulkAsync propagates cancellation.</summary>
    /// <returns>A task that represents the asynchronous operation.</returns>
    [Fact]
    public async Task RemoveItemBulkAsync_PropagatesCancellation()
    {
        FakeBulkWorkerClient workerClient = new();
        GatewaySession session = await OpenSessionAsync(workerClient);
        using CancellationTokenSource cts = new();
        await cts.CancelAsync();

        await Assert.ThrowsAnyAsync<OperationCanceledException>(
            async () => await session.RemoveItemBulkAsync(12, [11], cts.Token));
    }

    /// <summary>Verifies that UnAdviseItemBulkAsync forwards the command and returns results.</summary>
    /// <returns>A task that represents the asynchronous operation.</returns>
    [Fact]
    public async Task UnAdviseItemBulkAsync_ForwardsOneUnAdviseItemBulkCommandAndReturnsResults()
    {
        FakeBulkWorkerClient workerClient = WithReply(reply => reply.UnAdviseItemBulk = new BulkSubscribeReply
        {
            Results =
            {
                new SubscribeResult { ServerHandle = 12, ItemHandle = 21, WasSuccessful = true },
                new SubscribeResult { ServerHandle = 12, ItemHandle = 22, WasSuccessful = false, ErrorMessage = "not advised" },
            },
        }, MxCommandKind.UnAdviseItemBulk);
        GatewaySession session = await OpenSessionAsync(workerClient);

        IReadOnlyList<SubscribeResult> results = await session.UnAdviseItemBulkAsync(
            12,
            [21, 22],
            CancellationToken.None);

        Assert.Equal(MxCommandKind.UnAdviseItemBulk, workerClient.LastCommand?.Command.Kind);
        Assert.Equal([21, 22], workerClient.LastCommand?.Command.UnAdviseItemBulk.ItemHandles);
        Assert.Equal(2, results.Count);
        Assert.False(results[1].WasSuccessful);
        Assert.Equal("not advised", results[1].ErrorMessage);
    }

    /// <summary>Verifies that UnAdviseItemBulkAsync propagates cancellation.</summary>
    /// <returns>A task that represents the asynchronous operation.</returns>
    [Fact]
    public async Task UnAdviseItemBulkAsync_PropagatesCancellation()
    {
        FakeBulkWorkerClient workerClient = new();
        GatewaySession session = await OpenSessionAsync(workerClient);
        using CancellationTokenSource cts = new();
        await cts.CancelAsync();

        await Assert.ThrowsAnyAsync<OperationCanceledException>(
            async () => await session.UnAdviseItemBulkAsync(12, [21], cts.Token));
    }

    /// <summary>Verifies that SubscribeBulkAsync surfaces per-entry failures.</summary>
    /// <returns>A task that represents the asynchronous operation.</returns>
    [Fact]
    public async Task SubscribeBulkAsync_SurfacesPerEntryFailures()
    {
        // SubscribeBulkAsync already has a happy-path test in SessionManagerTests
        // (GatewaySessionSubscribeBulkAsync_ForwardsOneBulkCommandAndReturnsResults);
        // this complementary test pins the per-entry failure-surface behaviour.
        FakeBulkWorkerClient workerClient = WithReply(reply => reply.SubscribeBulk = new BulkSubscribeReply
        {
            Results =
            {
                new SubscribeResult { ServerHandle = 12, TagAddress = "Galaxy.Good", ItemHandle = 501, WasSuccessful = true },
                new SubscribeResult { ServerHandle = 12, TagAddress = "Galaxy.Bad", ItemHandle = 0, WasSuccessful = false, ErrorMessage = "MXAccess subscribe failed" },
            },
        }, MxCommandKind.SubscribeBulk);
        GatewaySession session = await OpenSessionAsync(workerClient);

        IReadOnlyList<SubscribeResult> results = await session.SubscribeBulkAsync(
            12,
            ["Galaxy.Good", "Galaxy.Bad"],
            CancellationToken.None);

        Assert.Equal(2, results.Count);
        Assert.True(results[0].WasSuccessful);
        Assert.False(results[1].WasSuccessful);
        Assert.Equal("MXAccess subscribe failed", results[1].ErrorMessage);
    }

    /// <summary>Verifies that SubscribeBulkAsync propagates cancellation.</summary>
    /// <returns>A task that represents the asynchronous operation.</returns>
    [Fact]
    public async Task SubscribeBulkAsync_PropagatesCancellation()
    {
        FakeBulkWorkerClient workerClient = new();
        GatewaySession session = await OpenSessionAsync(workerClient);
        using CancellationTokenSource cts = new();
        await cts.CancelAsync();

        await Assert.ThrowsAnyAsync<OperationCanceledException>(
            async () => await session.SubscribeBulkAsync(12, ["Tag"], cts.Token));
    }

    /// <summary>Verifies that UnsubscribeBulkAsync forwards the command and returns results.</summary>
    /// <returns>A task that represents the asynchronous operation.</returns>
    [Fact]
    public async Task UnsubscribeBulkAsync_ForwardsOneUnsubscribeBulkCommandAndReturnsResults()
    {
        FakeBulkWorkerClient workerClient = WithReply(reply => reply.UnsubscribeBulk = new BulkSubscribeReply
        {
            Results =
            {
                new SubscribeResult { ServerHandle = 12, ItemHandle = 31, WasSuccessful = true },
                new SubscribeResult { ServerHandle = 12, ItemHandle = 32, WasSuccessful = false, ErrorMessage = "unknown handle" },
            },
        }, MxCommandKind.UnsubscribeBulk);
        GatewaySession session = await OpenSessionAsync(workerClient);

        IReadOnlyList<SubscribeResult> results = await session.UnsubscribeBulkAsync(
            12,
            [31, 32],
            CancellationToken.None);

        Assert.Equal(MxCommandKind.UnsubscribeBulk, workerClient.LastCommand?.Command.Kind);
        Assert.Equal([31, 32], workerClient.LastCommand?.Command.UnsubscribeBulk.ItemHandles);
        Assert.Equal(2, results.Count);
        Assert.False(results[1].WasSuccessful);
    }

    /// <summary>Verifies that UnsubscribeBulkAsync propagates cancellation.</summary>
    /// <returns>A task that represents the asynchronous operation.</returns>
    [Fact]
    public async Task UnsubscribeBulkAsync_PropagatesCancellation()
    {
        FakeBulkWorkerClient workerClient = new();
        GatewaySession session = await OpenSessionAsync(workerClient);
        using CancellationTokenSource cts = new();
        await cts.CancelAsync();

        await Assert.ThrowsAnyAsync<OperationCanceledException>(
            async () => await session.UnsubscribeBulkAsync(12, [31], cts.Token));
    }

    /// <summary>Verifies that WriteBulkAsync surfaces per-entry failures.</summary>
    /// <returns>A task that represents the asynchronous operation.</returns>
    [Fact]
    public async Task WriteBulkAsync_SurfacesPerEntryFailures()
    {
        // Complement the existing happy-path WriteBulk test in SessionManagerTests
        // with an explicit per-entry failure assertion plus payload-shape pinning.
        FakeBulkWorkerClient workerClient = WithReply(reply => reply.WriteBulk = new BulkWriteReply
        {
            Results =
            {
                new BulkWriteResult { ServerHandle = 12, ItemHandle = 901, WasSuccessful = true },
                new BulkWriteResult { ServerHandle = 12, ItemHandle = 902, WasSuccessful = false, ErrorMessage = "MXAccess invalid handle" },
            },
        }, MxCommandKind.WriteBulk);
        GatewaySession session = await OpenSessionAsync(workerClient);

        IReadOnlyList<BulkWriteResult> results = await session.WriteBulkAsync(
            12,
            new[]
            {
                new WriteBulkEntry { ItemHandle = 901, UserId = 5, Value = new MxValue { DataType = MxDataType.Integer, Int32Value = 11 } },
                new WriteBulkEntry { ItemHandle = 902, UserId = 5, Value = new MxValue { DataType = MxDataType.Integer, Int32Value = 22 } },
            },
            CancellationToken.None);

        Assert.Equal(MxCommandKind.WriteBulk, workerClient.LastCommand?.Command.Kind);
        Assert.Equal(2, workerClient.LastCommand?.Command.WriteBulk.Entries.Count);
        Assert.Equal(901, workerClient.LastCommand?.Command.WriteBulk.Entries[0].ItemHandle);
        Assert.Equal(11, workerClient.LastCommand?.Command.WriteBulk.Entries[0].Value.Int32Value);
        Assert.False(results[1].WasSuccessful);
        Assert.Equal("MXAccess invalid handle", results[1].ErrorMessage);
    }

    /// <summary>Verifies that WriteBulkAsync propagates cancellation.</summary>
    /// <returns>A task that represents the asynchronous operation.</returns>
    [Fact]
    public async Task WriteBulkAsync_PropagatesCancellation()
    {
        FakeBulkWorkerClient workerClient = new();
        GatewaySession session = await OpenSessionAsync(workerClient);
        using CancellationTokenSource cts = new();
        await cts.CancelAsync();

        await Assert.ThrowsAnyAsync<OperationCanceledException>(
            async () => await session.WriteBulkAsync(
                12,
                new[] { new WriteBulkEntry { ItemHandle = 1, UserId = 1, Value = new MxValue { DataType = MxDataType.Integer, Int32Value = 0 } } },
                cts.Token));
    }

    /// <summary>Verifies that Write2BulkAsync forwards the command and preserves timestamp payload.</summary>
    /// <returns>A task that represents the asynchronous operation.</returns>
    [Fact]
    public async Task Write2BulkAsync_ForwardsOneWrite2BulkCommandAndPreservesTimestampPayload()
    {
        FakeBulkWorkerClient workerClient = WithReply(reply => reply.Write2Bulk = new BulkWriteReply
        {
            Results =
            {
                new BulkWriteResult { ServerHandle = 12, ItemHandle = 701, WasSuccessful = true },
                new BulkWriteResult { ServerHandle = 12, ItemHandle = 702, WasSuccessful = false, ErrorMessage = "MXAccess Write2 failed" },
            },
        }, MxCommandKind.Write2Bulk);
        GatewaySession session = await OpenSessionAsync(workerClient);

        IReadOnlyList<BulkWriteResult> results = await session.Write2BulkAsync(
            12,
            new[]
            {
                new Write2BulkEntry
                {
                    ItemHandle = 701,
                    UserId = 5,
                    Value = new MxValue { DataType = MxDataType.Integer, Int32Value = 11 },
                    TimestampValue = new MxValue { DataType = MxDataType.Time, Int64Value = 1234567890L },
                },
                new Write2BulkEntry
                {
                    ItemHandle = 702,
                    UserId = 5,
                    Value = new MxValue { DataType = MxDataType.Integer, Int32Value = 22 },
                    TimestampValue = new MxValue { DataType = MxDataType.Time, Int64Value = 1234567891L },
                },
            },
            CancellationToken.None);

        Assert.Equal(MxCommandKind.Write2Bulk, workerClient.LastCommand?.Command.Kind);
        Assert.Equal(12, workerClient.LastCommand?.Command.Write2Bulk.ServerHandle);
        Assert.Equal(2, workerClient.LastCommand?.Command.Write2Bulk.Entries.Count);
        Assert.Equal(701, workerClient.LastCommand?.Command.Write2Bulk.Entries[0].ItemHandle);
        Assert.Equal(1234567890L, workerClient.LastCommand?.Command.Write2Bulk.Entries[0].TimestampValue.Int64Value);
        Assert.False(results[1].WasSuccessful);
    }

    /// <summary>Verifies that Write2BulkAsync propagates cancellation.</summary>
    /// <returns>A task that represents the asynchronous operation.</returns>
    [Fact]
    public async Task Write2BulkAsync_PropagatesCancellation()
    {
        FakeBulkWorkerClient workerClient = new();
        GatewaySession session = await OpenSessionAsync(workerClient);
        using CancellationTokenSource cts = new();
        await cts.CancelAsync();

        await Assert.ThrowsAnyAsync<OperationCanceledException>(
            async () => await session.Write2BulkAsync(
                12,
                new[]
                {
                    new Write2BulkEntry
                    {
                        ItemHandle = 1,
                        UserId = 1,
                        Value = new MxValue { DataType = MxDataType.Integer, Int32Value = 0 },
                        TimestampValue = new MxValue { DataType = MxDataType.Time, Int64Value = 0L },
                    },
                },
                cts.Token));
    }

    /// <summary>Verifies that WriteSecuredBulkAsync forwards the command and preserves credential payload.</summary>
    /// <returns>A task that represents the asynchronous operation.</returns>
    [Fact]
    public async Task WriteSecuredBulkAsync_ForwardsOneWriteSecuredBulkCommandAndPreservesCredentialPayload()
    {
        // The secured variants carry caller credential identifiers (CurrentUserId /
        // VerifierUserId). Pin that those survive the gateway round-trip end-to-end —
        // the over-the-wire command shape must NOT redact or flatten them, only the
        // *log surface* (see GatewaySession's redaction rules) is allowed to drop them.
        FakeBulkWorkerClient workerClient = WithReply(reply => reply.WriteSecuredBulk = new BulkWriteReply
        {
            Results =
            {
                new BulkWriteResult { ServerHandle = 12, ItemHandle = 601, WasSuccessful = true },
                new BulkWriteResult { ServerHandle = 12, ItemHandle = 602, WasSuccessful = false, ErrorMessage = "MXAccess secured-write rejected" },
            },
        }, MxCommandKind.WriteSecuredBulk);
        GatewaySession session = await OpenSessionAsync(workerClient);

        IReadOnlyList<BulkWriteResult> results = await session.WriteSecuredBulkAsync(
            12,
            new[]
            {
                new WriteSecuredBulkEntry
                {
                    ItemHandle = 601,
                    CurrentUserId = 7,
                    VerifierUserId = 8,
                    Value = new MxValue { DataType = MxDataType.Integer, Int32Value = 1 },
                },
                new WriteSecuredBulkEntry
                {
                    ItemHandle = 602,
                    CurrentUserId = 7,
                    VerifierUserId = 8,
                    Value = new MxValue { DataType = MxDataType.Integer, Int32Value = 2 },
                },
            },
            CancellationToken.None);

        Assert.Equal(MxCommandKind.WriteSecuredBulk, workerClient.LastCommand?.Command.Kind);
        Assert.Equal(12, workerClient.LastCommand?.Command.WriteSecuredBulk.ServerHandle);
        Assert.Equal(2, workerClient.LastCommand?.Command.WriteSecuredBulk.Entries.Count);
        WriteSecuredBulkEntry firstEntry = workerClient.LastCommand!.Command.WriteSecuredBulk.Entries[0];
        Assert.Equal(601, firstEntry.ItemHandle);
        Assert.Equal(7, firstEntry.CurrentUserId);
        Assert.Equal(8, firstEntry.VerifierUserId);
        Assert.Equal(1, firstEntry.Value.Int32Value);
        Assert.False(results[1].WasSuccessful);
        Assert.Equal("MXAccess secured-write rejected", results[1].ErrorMessage);
    }

    /// <summary>Verifies that WriteSecuredBulkAsync propagates cancellation.</summary>
    /// <returns>A task that represents the asynchronous operation.</returns>
    [Fact]
    public async Task WriteSecuredBulkAsync_PropagatesCancellation()
    {
        FakeBulkWorkerClient workerClient = new();
        GatewaySession session = await OpenSessionAsync(workerClient);
        using CancellationTokenSource cts = new();
        await cts.CancelAsync();

        await Assert.ThrowsAnyAsync<OperationCanceledException>(
            async () => await session.WriteSecuredBulkAsync(
                12,
                new[]
                {
                    new WriteSecuredBulkEntry
                    {
                        ItemHandle = 1,
                        CurrentUserId = 7,
                        VerifierUserId = 8,
                        Value = new MxValue { DataType = MxDataType.Integer, Int32Value = 0 },
                    },
                },
                cts.Token));
    }

    /// <summary>
    ///     Pin mid-flight cancellation behaviour for at least one bulk
    ///     path. Unlike the pre-cancel <c>WriteSecuredBulkAsync_PropagatesCancellation</c>
    ///     above, this fake's <see cref="MidFlightBulkWorkerClient.InvokeAsync"/>
    ///     returns a <see cref="TaskCompletionSource"/>-backed task that does NOT
    ///     complete until the registered token fires. The session call therefore
    ///     reaches <c>InvokeBulkInternalAsync</c> → <c>InvokeAsync</c> →
    ///     <c>workerClient.InvokeAsync</c> and parks on an in-flight await; only
    ///     after that does <c>cts.CancelAsync()</c> fire. This is the path a real
    ///     client closing its stream would hit, which the pre-cancel pattern can't
    ///     exercise.
    /// </summary>
    /// <returns>A task that represents the asynchronous operation.</returns>
    [Fact]
    public async Task WriteSecuredBulkAsync_WhenCancelledMidFlight_ThrowsOperationCanceledForRequestToken()
    {
        MidFlightBulkWorkerClient workerClient = new();
        GatewaySession session = await OpenSessionAsync(workerClient);
        using CancellationTokenSource cts = new();

        Task<IReadOnlyList<BulkWriteResult>> writeTask = session.WriteSecuredBulkAsync(
            12,
            new[]
            {
                new WriteSecuredBulkEntry
                {
                    ItemHandle = 1,
                    CurrentUserId = 7,
                    VerifierUserId = 8,
                    Value = new MxValue { DataType = MxDataType.Integer, Int32Value = 0 },
                },
            },
            cts.Token);

        // Wait until the gateway has descended into the worker's InvokeAsync and
        // registered its cancellation continuation — only then is this a true
        // mid-flight cancel.
        await workerClient.InvokeStarted.Task.WaitAsync(TimeSpan.FromSeconds(5));
        Assert.False(writeTask.IsCompleted);

        await cts.CancelAsync();

        OperationCanceledException exception = await Assert.ThrowsAnyAsync<OperationCanceledException>(
            async () => await writeTask);
        Assert.Equal(cts.Token, exception.CancellationToken);
        Assert.Equal(1, workerClient.InvokeCount);
    }

    /// <summary>Verifies that WriteSecured2BulkAsync forwards the command and preserves credential and timestamp payload.</summary>
    /// <returns>A task that represents the asynchronous operation.</returns>
    [Fact]
    public async Task WriteSecured2BulkAsync_ForwardsOneWriteSecured2BulkCommandAndPreservesCredentialAndTimestampPayload()
    {
        FakeBulkWorkerClient workerClient = WithReply(reply => reply.WriteSecured2Bulk = new BulkWriteReply
        {
            Results =
            {
                new BulkWriteResult { ServerHandle = 12, ItemHandle = 801, WasSuccessful = true },
                new BulkWriteResult { ServerHandle = 12, ItemHandle = 802, WasSuccessful = false, ErrorMessage = "MXAccess secured2-write rejected" },
            },
        }, MxCommandKind.WriteSecured2Bulk);
        GatewaySession session = await OpenSessionAsync(workerClient);

        IReadOnlyList<BulkWriteResult> results = await session.WriteSecured2BulkAsync(
            12,
            new[]
            {
                new WriteSecured2BulkEntry
                {
                    ItemHandle = 801,
                    CurrentUserId = 7,
                    VerifierUserId = 8,
                    Value = new MxValue { DataType = MxDataType.Integer, Int32Value = 1 },
                    TimestampValue = new MxValue { DataType = MxDataType.Time, Int64Value = 1700000000L },
                },
                new WriteSecured2BulkEntry
                {
                    ItemHandle = 802,
                    CurrentUserId = 7,
                    VerifierUserId = 8,
                    Value = new MxValue { DataType = MxDataType.Integer, Int32Value = 2 },
                    TimestampValue = new MxValue { DataType = MxDataType.Time, Int64Value = 1700000001L },
                },
            },
            CancellationToken.None);

        Assert.Equal(MxCommandKind.WriteSecured2Bulk, workerClient.LastCommand?.Command.Kind);
        Assert.Equal(2, workerClient.LastCommand?.Command.WriteSecured2Bulk.Entries.Count);
        WriteSecured2BulkEntry firstEntry = workerClient.LastCommand!.Command.WriteSecured2Bulk.Entries[0];
        Assert.Equal(801, firstEntry.ItemHandle);
        Assert.Equal(7, firstEntry.CurrentUserId);
        Assert.Equal(8, firstEntry.VerifierUserId);
        Assert.Equal(1, firstEntry.Value.Int32Value);
        Assert.Equal(1700000000L, firstEntry.TimestampValue.Int64Value);
        Assert.False(results[1].WasSuccessful);
    }

    /// <summary>Verifies that WriteSecured2BulkAsync propagates cancellation.</summary>
    /// <returns>A task that represents the asynchronous operation.</returns>
    [Fact]
    public async Task WriteSecured2BulkAsync_PropagatesCancellation()
    {
        FakeBulkWorkerClient workerClient = new();
        GatewaySession session = await OpenSessionAsync(workerClient);
        using CancellationTokenSource cts = new();
        await cts.CancelAsync();

        await Assert.ThrowsAnyAsync<OperationCanceledException>(
            async () => await session.WriteSecured2BulkAsync(
                12,
                new[]
                {
                    new WriteSecured2BulkEntry
                    {
                        ItemHandle = 1,
                        CurrentUserId = 7,
                        VerifierUserId = 8,
                        Value = new MxValue { DataType = MxDataType.Integer, Int32Value = 0 },
                        TimestampValue = new MxValue { DataType = MxDataType.Time, Int64Value = 0L },
                    },
                },
                cts.Token));
    }

    /// <summary>Verifies that ReadBulkAsync surfaces per-entry failures.</summary>
    /// <returns>A task that represents the asynchronous operation.</returns>
    [Fact]
    public async Task ReadBulkAsync_SurfacesPerEntryFailures()
    {
        // Complement the existing happy-path ReadBulk test in SessionManagerTests
        // with the failure-mode case where one tag failed to read.
        FakeBulkWorkerClient workerClient = WithReply(reply => reply.ReadBulk = new BulkReadReply
        {
            Results =
            {
                new BulkReadResult
                {
                    ServerHandle = 12,
                    TagAddress = "Galaxy.Good",
                    ItemHandle = 511,
                    WasSuccessful = true,
                    WasCached = false,
                    Value = new MxValue { DataType = MxDataType.Integer, Int32Value = 42 },
                },
                new BulkReadResult
                {
                    ServerHandle = 12,
                    TagAddress = "Galaxy.Bad",
                    ItemHandle = 0,
                    WasSuccessful = false,
                    ErrorMessage = "MXAccess read timed out",
                },
            },
        }, MxCommandKind.ReadBulk);
        GatewaySession session = await OpenSessionAsync(workerClient);

        IReadOnlyList<BulkReadResult> results = await session.ReadBulkAsync(
            12,
            ["Galaxy.Good", "Galaxy.Bad"],
            TimeSpan.FromMilliseconds(750),
            CancellationToken.None);

        Assert.Equal(MxCommandKind.ReadBulk, workerClient.LastCommand?.Command.Kind);
        Assert.Equal(750u, workerClient.LastCommand?.Command.ReadBulk.TimeoutMs);
        Assert.Equal(["Galaxy.Good", "Galaxy.Bad"], workerClient.LastCommand?.Command.ReadBulk.TagAddresses);
        Assert.Equal(2, results.Count);
        Assert.True(results[0].WasSuccessful);
        Assert.False(results[1].WasSuccessful);
        Assert.Equal("MXAccess read timed out", results[1].ErrorMessage);
    }

    /// <summary>Verifies that ReadBulkAsync propagates cancellation.</summary>
    /// <returns>A task that represents the asynchronous operation.</returns>
    [Fact]
    public async Task ReadBulkAsync_PropagatesCancellation()
    {
        FakeBulkWorkerClient workerClient = new();
        GatewaySession session = await OpenSessionAsync(workerClient);
        using CancellationTokenSource cts = new();
        await cts.CancelAsync();

        await Assert.ThrowsAnyAsync<OperationCanceledException>(
            async () => await session.ReadBulkAsync(
                12,
                ["Galaxy.Tag"],
                TimeSpan.FromMilliseconds(500),
                cts.Token));
    }

    // -----------------------------------------------------------------
    // Helpers
    // -----------------------------------------------------------------

    private static FakeBulkWorkerClient WithReply(Action<MxCommandReply> populate, MxCommandKind kind)
    {
        MxCommandReply reply = new()
        {
            SessionId = "session-1",
            CorrelationId = "correlation-1",
            Kind = kind,
            ProtocolStatus = new ProtocolStatus { Code = ProtocolStatusCode.Ok },
        };
        populate(reply);
        return new FakeBulkWorkerClient
        {
            InvokeReply = new WorkerCommandReply { Reply = reply },
        };
    }

    private static async Task<GatewaySession> OpenSessionAsync(FakeBulkWorkerClient workerClient)
    {
        return await OpenSessionAsync((IWorkerClient)workerClient);
    }

    private static async Task<GatewaySession> OpenSessionAsync(IWorkerClient workerClient)
    {
        SessionManager manager = CreateManager(workerClient);
        return await manager.OpenSessionAsync(CreateOpenRequest(), "client-1", ownerKeyId: null, CancellationToken.None);
    }

    private static SessionManager CreateManager(IWorkerClient workerClient)
    {
        return new SessionManager(
            new SessionRegistry(),
            new FakeBulkSessionWorkerClientFactory(workerClient),
            Options.Create(new GatewayOptions
            {
                Sessions = new SessionOptions
                {
                    DefaultCommandTimeoutSeconds = 30,
                    MaxSessions = 16,
                    DefaultLeaseSeconds = 1800,
                },
                Worker = new WorkerOptions
                {
                    StartupTimeoutSeconds = 30,
                    ShutdownTimeoutSeconds = 10,
                },
            }),
            new GatewayMetrics());
    }

    private static SessionOpenRequest CreateOpenRequest()
    {
        return new SessionOpenRequest(
            RequestedBackend: null,
            ClientSessionName: "test-session",
            ClientCorrelationId: "client-correlation-1",
            CommandTimeout: Duration.FromTimeSpan(TimeSpan.FromSeconds(5)));
    }

    private sealed class FakeBulkSessionWorkerClientFactory(IWorkerClient workerClient) : ISessionWorkerClientFactory
    {
        /// <inheritdoc />
        public Task<IWorkerClient> CreateAsync(
            GatewaySession session,
            CancellationToken cancellationToken)
        {
            return Task.FromResult(workerClient);
        }
    }

    private sealed class FakeBulkWorkerClient : IWorkerClient
    {
        /// <inheritdoc />
        public string SessionId { get; init; } = "session-1";

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

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

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

        /// <summary>Gets the number of times Invoke was called on the fake worker client.</summary>
        public int InvokeCount { get; private set; }

        /// <summary>Gets the last command invoked on the fake worker client.</summary>
        public WorkerCommand? LastCommand { get; private set; }

        /// <summary>Gets the reply to return for invoke calls on the fake worker client.</summary>
        public WorkerCommandReply? InvokeReply { get; init; }

        /// <inheritdoc />
        public Task StartAsync(CancellationToken cancellationToken) => Task.CompletedTask;

        /// <inheritdoc />
        public Task<WorkerCommandReply> InvokeAsync(
            WorkerCommand command,
            TimeSpan timeout,
            CancellationToken cancellationToken)
        {
            cancellationToken.ThrowIfCancellationRequested();
            InvokeCount++;
            LastCommand = command;
            if (InvokeReply is not null)
            {
                return Task.FromResult(InvokeReply);
            }

            MxCommandKind kind = command.Command?.Kind ?? MxCommandKind.Unspecified;
            return Task.FromResult(new WorkerCommandReply
            {
                Reply = new MxCommandReply
                {
                    SessionId = SessionId,
                    CorrelationId = "correlation-1",
                    Kind = kind,
                    ProtocolStatus = new ProtocolStatus { Code = ProtocolStatusCode.Ok },
                },
            });
        }

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

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

        /// <inheritdoc />
        public void Kill(string reason) => State = WorkerClientState.Faulted;

        /// <summary>Disposes the fake worker client; this fake has no resources to release.</summary>
        /// <returns>A task that represents the asynchronous operation.</returns>
        public ValueTask DisposeAsync() => ValueTask.CompletedTask;
    }

    /// <summary>
    ///     Mid-flight cancellation fake worker client.
    ///     <see cref="InvokeAsync"/> signals <see cref="InvokeStarted"/>, registers
    ///     a cancellation continuation on the caller's <see cref="CancellationToken"/>,
    ///     and parks on a <see cref="TaskCompletionSource{TResult}"/> that completes
    ///     only when the token fires or the fake is shut down. This is the only
    ///     way to land an <see cref="OperationCanceledException"/> on the async
    ///     continuation rather than the synchronous fast-path inside
    ///     <c>ThrowIfCancellationRequested</c>.
    /// </summary>
    private sealed class MidFlightBulkWorkerClient : IWorkerClient
    {
        private readonly TaskCompletionSource<WorkerCommandReply> _invokeCompletion =
            new(TaskCreationOptions.RunContinuationsAsynchronously);

        /// <inheritdoc />
        public string SessionId { get; init; } = "session-1";

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

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

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

        /// <summary>Gets the number of times <see cref="InvokeAsync"/> was entered.</summary>
        public int InvokeCount { get; private set; }

        /// <summary>Signals when <see cref="InvokeAsync"/> first enters — the test
        /// awaits this before triggering mid-flight cancellation.</summary>
        public TaskCompletionSource InvokeStarted { get; } =
            new(TaskCreationOptions.RunContinuationsAsynchronously);

        /// <inheritdoc />
        public Task StartAsync(CancellationToken cancellationToken) => Task.CompletedTask;

        /// <inheritdoc />
        public Task<WorkerCommandReply> InvokeAsync(
            WorkerCommand command,
            TimeSpan timeout,
            CancellationToken cancellationToken)
        {
            InvokeCount++;
            // Register cancellation BEFORE signalling start so the test can be
            // certain the continuation is wired the moment InvokeStarted resolves.
            cancellationToken.Register(() => _invokeCompletion.TrySetCanceled(cancellationToken));
            InvokeStarted.TrySetResult();
            return _invokeCompletion.Task;
        }

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

        /// <inheritdoc />
        public Task ShutdownAsync(TimeSpan timeout, CancellationToken cancellationToken)
        {
            State = WorkerClientState.Closed;
            _invokeCompletion.TrySetCanceled(cancellationToken);
            return Task.CompletedTask;
        }

        /// <inheritdoc />
        public void Kill(string reason)
        {
            State = WorkerClientState.Faulted;
            _invokeCompletion.TrySetCanceled();
        }

        /// <summary>Disposes the fake worker client, cancelling any in-flight invoke.</summary>
        /// <returns>A task that represents the asynchronous operation.</returns>
        public ValueTask DisposeAsync()
        {
            _invokeCompletion.TrySetCanceled();
            return ValueTask.CompletedTask;
        }
    }
}
