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

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

/// <summary>
/// Drives the public client API against <see cref="WireFakeGatewayServer"/> — a real
/// gRPC server on loopback — so the transport, protobuf serialization, call metadata,
/// and gRPC status mapping are all exercised. Every other test in this project
/// substitutes <see cref="FakeGatewayTransport"/> and therefore proves nothing about
/// what actually crosses the wire.
/// </summary>
public sealed class MxGatewayClientWireTests
{
    private const string ApiKey = "mxgw_wiretest_secret";

    /// <summary>
    /// Verifies the full session happy path decodes real wire bytes end to end.
    /// </summary>
    /// <returns>A task that represents the asynchronous operation.</returns>
    [Fact]
    public async Task SessionRoundTrip_OverRealTransport_DecodesEveryReplyField()
    {
        await using WireFakeGatewayServer server = await WireFakeGatewayServer.StartAsync();
        await using MxGatewayClient client = server.CreateClient(ApiKey);

        MxGatewaySession session = await client.OpenSessionAsync(
            new OpenSessionRequest { ClientSessionName = "wire-test" });

        Assert.Equal(WireFakeGatewayServer.FakeGatewayService.SessionId, session.SessionId);
        Assert.Equal("fake-backend", session.OpenSessionReply.BackendName);
        Assert.Equal(1234, session.OpenSessionReply.WorkerProcessId);
        Assert.Equal(3u, session.OpenSessionReply.GatewayProtocolVersion);
        Assert.Equal(["events", "invoke"], session.OpenSessionReply.Capabilities);

        int serverHandle = await session.RegisterAsync("wire-test-client");
        Assert.Equal(WireFakeGatewayServer.FakeGatewayService.ServerHandle, serverHandle);

        MxCommandRequest? invoke = server.Service.InvokeRequest;
        Assert.NotNull(invoke);
        Assert.Equal(MxCommandKind.Register, invoke.Command.Kind);
        Assert.Equal("wire-test-client", invoke.Command.Register.ClientName);

        List<MxEvent> events = await CollectAsync(client.StreamEventsAsync(
            new StreamEventsRequest { SessionId = session.SessionId }));

        MxEvent single = Assert.Single(events);
        Assert.Equal(MxEventFamily.OnDataChange, single.Family);
        Assert.Equal(WireFakeGatewayServer.FakeGatewayService.ServerHandle, single.ServerHandle);
        Assert.Equal(WireFakeGatewayServer.FakeGatewayService.ItemHandle, single.ItemHandle);
        Assert.Equal(17, single.Value.Int32Value);
        Assert.Equal(192, single.Quality);
        Assert.Equal(9ul, single.WorkerSequence);
        Assert.Equal(MxEvent.BodyOneofCase.OnDataChange, single.BodyCase);

        CloseSessionReply closeReply = await session.CloseAsync();
        Assert.Equal(SessionState.Closed, closeReply.FinalState);
        Assert.Equal(
            WireFakeGatewayServer.FakeGatewayService.SessionId,
            server.Service.CloseSessionRequest?.SessionId);
    }

    /// <summary>
    /// Verifies the API key reaches the server as a bearer header on unary and
    /// streaming calls alike. A transport fake can only assert what the client passes;
    /// this asserts what the server receives.
    /// </summary>
    /// <returns>A task that represents the asynchronous operation.</returns>
    [Fact]
    public async Task ApiKey_ReachesTheServerAsBearerMetadata_OnEveryRpc()
    {
        await using WireFakeGatewayServer server = await WireFakeGatewayServer.StartAsync();
        await using MxGatewayClient client = server.CreateClient(ApiKey);

        MxGatewaySession session = await client.OpenSessionAsync(
            new OpenSessionRequest { ClientSessionName = "wire-test" });
        await session.RegisterAsync("wire-test-client");
        await CollectAsync(client.StreamEventsAsync(
            new StreamEventsRequest { SessionId = session.SessionId }));
        await session.CloseAsync();

        string expected = $"Bearer {ApiKey}";
        Assert.Equal(
            new Dictionary<string, string>
            {
                ["OpenSession"] = expected,
                ["Invoke"] = expected,
                ["StreamEvents"] = expected,
                ["CloseSession"] = expected,
            },
            server.Service.AuthorizationByMethod);
    }

    /// <summary>
    /// Verifies the gateway's replay-gap sentinel survives serialization and is
    /// surfaced as a typed, non-terminal stream item.
    /// </summary>
    /// <returns>A task that represents the asynchronous operation.</returns>
    [Fact]
    public async Task ReplayGapSentinel_SurvivesTheWire_AsTypedStreamItem()
    {
        await using WireFakeGatewayServer server = await WireFakeGatewayServer.StartAsync(service =>
            service.ReplayGap = new ReplayGap
            {
                RequestedAfterSequence = 3,
                OldestAvailableSequence = 8,
            });
        await using MxGatewayClient client = server.CreateClient(ApiKey);

        MxGatewaySession session = await client.OpenSessionAsync(
            new OpenSessionRequest { ClientSessionName = "wire-test" });

        List<MxEventStreamItem> items = [];
        IAsyncEnumerable<MxEvent> stream = client.StreamEventsAsync(new StreamEventsRequest
        {
            SessionId = session.SessionId,
            AfterWorkerSequence = 3,
        });

        await foreach (MxEventStreamItem item in stream.AsStreamItemsAsync())
        {
            items.Add(item);
        }

        Assert.Equal(2, items.Count);
        Assert.True(items[0].IsReplayGap);
        Assert.Equal(3ul, items[0].ReplayGap!.RequestedAfterSequence);
        Assert.Equal(8ul, items[0].ReplayGap!.OldestAvailableSequence);
        Assert.False(items[1].IsReplayGap);
        Assert.Equal(MxEventFamily.OnDataChange, items[1].Event.Family);
        Assert.Equal(3ul, server.Service.StreamEventsRequest?.AfterWorkerSequence);
    }

    /// <summary>
    /// Verifies a genuine <c>PERMISSION_DENIED</c> status maps to the typed client
    /// exception rather than a bare RpcException.
    /// </summary>
    /// <returns>A task that represents the asynchronous operation.</returns>
    [Fact]
    public async Task PermissionDeniedStatus_MapsToAuthorizationException()
    {
        await using WireFakeGatewayServer server = await WireFakeGatewayServer.StartAsync(
            service => service.DenyInvoke = true);
        await using MxGatewayClient client = server.CreateClient(ApiKey);

        MxGatewaySession session = await client.OpenSessionAsync(
            new OpenSessionRequest { ClientSessionName = "wire-test" });

        await Assert.ThrowsAsync<MxGatewayAuthorizationException>(
            () => session.RegisterAsync("wire-test-client"));
    }

    private static async Task<List<MxEvent>> CollectAsync(IAsyncEnumerable<MxEvent> stream)
    {
        List<MxEvent> events = [];
        await foreach (MxEvent gatewayEvent in stream)
        {
            events.Add(gatewayEvent);
        }

        return events;
    }
}
