using Google.Protobuf.WellKnownTypes;
using Grpc.Core;
using ZB.MOM.WW.MxGateway.Contracts.Proto;

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

/// <summary>
///     PR E.2 — pins the .NET SDK surface for the new alarm RPCs:
///     <see cref="MxGatewayClient.AcknowledgeAlarmAsync"/> and
///     <see cref="MxGatewayClient.QueryActiveAlarmsAsync"/>.
/// </summary>
public sealed class MxGatewayClientAlarmsTests
{
    /// <summary>AcknowledgeAlarmAsync records request and returns reply.</summary>
    /// <returns>A task that represents the asynchronous operation.</returns>
    [Fact]
    public async Task AcknowledgeAlarmAsync_RecordsRequestShapeAndReturnsReply()
    {
        FakeGatewayTransport transport = CreateTransport();
        transport.AddAcknowledgeReply(new AcknowledgeAlarmReply
        {
            CorrelationId = "corr-1",
            ProtocolStatus = new ProtocolStatus { Code = ProtocolStatusCode.Ok },
            Status = new MxStatusProxy
            {
                Success = 1,
                Category = MxStatusCategory.Ok,
                DetectedBy = MxStatusSource.RespondingLmx,
            },
        });
        await using MxGatewayClient client = CreateClient(transport);

        AcknowledgeAlarmReply reply = await client.AcknowledgeAlarmAsync(new AcknowledgeAlarmRequest
        {
            ClientCorrelationId = "corr-1",
            AlarmFullReference = "Tank01.Level.HiHi",
            Comment = "investigating",
            OperatorUser = "alice",
        });

        Assert.Equal(ProtocolStatusCode.Ok, reply.ProtocolStatus.Code);
        Assert.Equal(MxStatusCategory.Ok, reply.Status.Category);

        var call = Assert.Single(transport.AcknowledgeAlarmCalls);
        Assert.Equal("Tank01.Level.HiHi", call.Request.AlarmFullReference);
        Assert.Equal("investigating", call.Request.Comment);
        Assert.Equal("alice", call.Request.OperatorUser);
        Assert.Equal("Bearer test-api-key", call.CallOptions.Headers?.GetValue("authorization"));
    }

    /// <summary>AcknowledgeAlarmAsync honors cancellation.</summary>
    /// <returns>A task that represents the asynchronous operation.</returns>
    [Fact]
    public async Task AcknowledgeAlarmAsync_HonorsCancellation()
    {
        // Acks are routed through the safe-unary retry pipeline (idempotent at the
        // MxAccess level), so the transport-side cancellation token is a linked one
        // rather than the caller's original. Verify cancellation by tripping the source
        // and asserting the call observes it.
        using CancellationTokenSource cancellation = new();
        cancellation.Cancel();
        FakeGatewayTransport transport = CreateTransport();
        await using MxGatewayClient client = CreateClient(transport);

        await Assert.ThrowsAnyAsync<OperationCanceledException>(() =>
            client.AcknowledgeAlarmAsync(
                new AcknowledgeAlarmRequest
                {
                    AlarmFullReference = "Tank01.Level.HiHi",
                    Comment = string.Empty,
                    OperatorUser = "alice",
                },
                cancellation.Token));
    }

    /// <summary>AcknowledgeAlarmAsync maps unauthenticated RPC exception to typed exception.</summary>
    /// <returns>A task that represents the asynchronous operation.</returns>
    [Fact]
    public async Task AcknowledgeAlarmAsync_MapsUnauthenticated_RpcException_ToTypedException()
    {
        FakeGatewayTransport transport = CreateTransport();
        transport.AcknowledgeAlarmExceptions.Enqueue(
            new RpcException(new Status(StatusCode.Unauthenticated, "expired key")));
        await using MxGatewayClient client = CreateClient(transport);

        // Note: the FakeGatewayTransport surfaces RpcException directly (it does not run
        // through GrpcMxGatewayClientTransport's mapping); the fake's contract here is to
        // pass the exception verbatim. RpcException → typed exception mapping is covered
        // in the GrpcMxGatewayClientTransport-level tests; the SDK-level test pins the
        // pass-through shape so a future migration to direct mapping won't silently
        // change observable behaviour.
        var ex = await Assert.ThrowsAsync<RpcException>(
            () => client.AcknowledgeAlarmAsync(new AcknowledgeAlarmRequest
            {
                AlarmFullReference = "Tank01.Level.HiHi",
                Comment = string.Empty,
                OperatorUser = "alice",
            }));
        Assert.Equal(StatusCode.Unauthenticated, ex.StatusCode);
    }

    /// <summary>QueryActiveAlarmsAsync streams enqueued snapshots.</summary>
    /// <returns>A task that represents the asynchronous operation.</returns>
    [Fact]
    public async Task QueryActiveAlarmsAsync_StreamsEnqueuedSnapshots()
    {
        FakeGatewayTransport transport = CreateTransport();
        transport.AddActiveAlarmSnapshot(MakeSnapshot("Tank01.Level.HiHi", AlarmConditionState.Active));
        transport.AddActiveAlarmSnapshot(MakeSnapshot("Tank02.Level.HiHi", AlarmConditionState.ActiveAcked));
        await using MxGatewayClient client = CreateClient(transport);

        List<ActiveAlarmSnapshot> snapshots = [];
        await foreach (ActiveAlarmSnapshot snapshot in client.QueryActiveAlarmsAsync(new QueryActiveAlarmsRequest
        {
            SessionId = "session-fixture",
        }))
        {
            snapshots.Add(snapshot);
        }

        Assert.Equal(2, snapshots.Count);
        Assert.Equal("Tank01.Level.HiHi", snapshots[0].AlarmFullReference);
        Assert.Equal(AlarmConditionState.Active, snapshots[0].CurrentState);
        Assert.Equal(AlarmConditionState.ActiveAcked, snapshots[1].CurrentState);
        Assert.Single(transport.QueryActiveAlarmsCalls);
    }

    /// <summary>QueryActiveAlarmsAsync passes filter prefix.</summary>
    /// <returns>A task that represents the asynchronous operation.</returns>
    [Fact]
    public async Task QueryActiveAlarmsAsync_PassesFilterPrefix()
    {
        FakeGatewayTransport transport = CreateTransport();
        await using MxGatewayClient client = CreateClient(transport);

        await foreach (ActiveAlarmSnapshot _ in client.QueryActiveAlarmsAsync(new QueryActiveAlarmsRequest
        {
            SessionId = "session-fixture",
            AlarmFilterPrefix = "Tank01.",
        }))
        {
            // no snapshots enqueued; just verifying the request passes through
        }

        var call = Assert.Single(transport.QueryActiveAlarmsCalls);
        Assert.Equal("Tank01.", call.Request.AlarmFilterPrefix);
    }

    /// <summary>QueryActiveAlarmsAsync honors cancellation during enumeration.</summary>
    /// <returns>A task that represents the asynchronous operation.</returns>
    [Fact]
    public async Task QueryActiveAlarmsAsync_HonorsCancellationDuringEnumeration()
    {
        FakeGatewayTransport transport = CreateTransport();
        transport.AddActiveAlarmSnapshot(MakeSnapshot("Tank01.Level.HiHi", AlarmConditionState.Active));
        transport.AddActiveAlarmSnapshot(MakeSnapshot("Tank02.Level.HiHi", AlarmConditionState.Active));
        await using MxGatewayClient client = CreateClient(transport);

        using CancellationTokenSource cancellation = new();
        await Assert.ThrowsAsync<OperationCanceledException>(async () =>
        {
            await foreach (ActiveAlarmSnapshot _ in client.QueryActiveAlarmsAsync(
                new QueryActiveAlarmsRequest { SessionId = "session-fixture" },
                cancellation.Token))
            {
                cancellation.Cancel();
            }
        });
    }

    private static ActiveAlarmSnapshot MakeSnapshot(string fullReference, AlarmConditionState state)
    {
        return new ActiveAlarmSnapshot
        {
            AlarmFullReference = fullReference,
            SourceObjectReference = fullReference.Split('.')[0],
            AlarmTypeName = "AnalogLimitAlarm.HiHi",
            Severity = 750,
            CurrentState = state,
            Category = "Process",
            Description = "Tank high-high level",
            OriginalRaiseTimestamp = Timestamp.FromDateTime(new DateTime(2026, 5, 1, 12, 0, 0, DateTimeKind.Utc)),
            LastTransitionTimestamp = Timestamp.FromDateTime(new DateTime(2026, 5, 1, 12, 0, 30, DateTimeKind.Utc)),
        };
    }

    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",
        });
    }
}
