using Microsoft.Extensions.Time.Testing;
using ZB.MOM.WW.MxGateway.Contracts.Proto;
using ZB.MOM.WW.MxGateway.Server.Grpc;

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

public sealed class MxAccessGrpcMapperTests
{
    /// <summary>Verifies that command mapping clones payloads to isolate them across process boundaries.</summary>
    [Fact]
    public void MapCommand_ClonesMethodSpecificPayloadForWorkerBoundary()
    {
        MxAccessGrpcMapper mapper = new();
        MxCommandRequest request = new()
        {
            SessionId = "session-1",
            Command = new MxCommand
            {
                Kind = MxCommandKind.Write,
                Write = new WriteCommand
                {
                    ServerHandle = 10,
                    ItemHandle = 20,
                    UserId = 30,
                    Value = new MxValue
                    {
                        DataType = MxDataType.String,
                        StringValue = "value",
                    },
                },
            },
        };

        WorkerCommand workerCommand = mapper.MapCommand(request);
        request.Command.Write.Value.StringValue = "changed";

        Assert.Equal(MxCommandKind.Write, workerCommand.Command.Kind);
        Assert.Equal("value", workerCommand.Command.Write.Value.StringValue);
        Assert.NotNull(workerCommand.EnqueueTimestamp);
    }

    /// <summary>
    ///     The command-only overload exists so <c>Invoke</c> does not deep-clone the whole request just to
    ///     overwrite and discard its command (GWC-29). It must still perform the one clone that keeps the
    ///     worker-bound graph unaliased from the caller-owned gRPC command, and must produce the same
    ///     <see cref="WorkerCommand"/> as the request overload.
    /// </summary>
    [Fact]
    public void MapCommandFromCommandClonesPayload()
    {
        FakeTimeProvider timeProvider = new(new DateTimeOffset(2026, 8, 7, 12, 0, 0, TimeSpan.Zero));
        MxAccessGrpcMapper mapper = new(timeProvider);
        MxCommand command = new()
        {
            Kind = MxCommandKind.Write,
            Write = new WriteCommand
            {
                ServerHandle = 10,
                ItemHandle = 20,
                UserId = 30,
                Value = new MxValue
                {
                    DataType = MxDataType.String,
                    StringValue = "value",
                },
            },
        };
        MxCommandRequest request = new()
        {
            SessionId = "session-1",
            Command = command.Clone(),
        };

        WorkerCommand fromCommand = mapper.MapCommand(command);
        WorkerCommand fromRequest = mapper.MapCommand(request);
        command.Write.Value.StringValue = "changed";

        Assert.Equal(MxCommandKind.Write, fromCommand.Command.Kind);
        Assert.Equal("value", fromCommand.Command.Write.Value.StringValue);
        Assert.NotNull(fromCommand.EnqueueTimestamp);
        Assert.Equal(fromRequest, fromCommand);
    }

    /// <summary>Verifies that command reply mapping preserves HRESULT and status information.</summary>
    [Fact]
    public void MapCommandReply_PreservesHresultStatusesAndPayload()
    {
        const int hresult = unchecked((int)0x80070005);
        WorkerCommandReply workerReply = new()
        {
            Reply = new MxCommandReply
            {
                SessionId = "session-1",
                Kind = MxCommandKind.Register,
                ProtocolStatus = MxAccessGrpcMapper.Ok(),
                Hresult = hresult,
                Register = new RegisterReply { ServerHandle = 50 },
            },
        };
        workerReply.Reply.Statuses.Add(new MxStatusProxy
        {
            Success = 0,
            Category = MxStatusCategory.SecurityError,
            DiagnosticText = "denied",
        });

        MxCommandReply publicReply = new MxAccessGrpcMapper().MapCommandReply(workerReply);

        Assert.Equal(hresult, publicReply.Hresult);
        Assert.Equal(50, publicReply.Register.ServerHandle);
        Assert.Equal("denied", Assert.Single(publicReply.Statuses).DiagnosticText);
    }

    /// <summary>Verifies that a missing worker reply returns a protocol violation status.</summary>
    [Fact]
    public void MapCommandReply_WhenWorkerReplyMissing_ReturnsProtocolViolationReply()
    {
        MxCommandReply publicReply = new MxAccessGrpcMapper().MapCommandReply(new WorkerCommandReply());

        Assert.Equal(ProtocolStatusCode.ProtocolViolation, publicReply.ProtocolStatus.Code);
    }

    /// <summary>
    ///     Verifies MapCommandReply transfers ownership of the inner MxCommandReply the same way
    ///     MapEvent does: the returned reference is the instance carried by the WorkerCommandReply,
    ///     not a clone. The WorkerCommandReply is discarded after mapping and the awaiting Invoke
    ///     call is its single consumer, so moving the inner reply out is safe and avoids a deep copy
    ///     of a potentially large bulk-read payload.
    /// </summary>
    [Fact]
    public void MapCommandReply_TransfersOwnershipOfInnerReplyWithoutCloning()
    {
        MxCommandReply innerReply = new()
        {
            SessionId = "session-1",
            Kind = MxCommandKind.Register,
            ProtocolStatus = MxAccessGrpcMapper.Ok(),
            Register = new RegisterReply { ServerHandle = 50 },
        };
        WorkerCommandReply workerReply = new() { Reply = innerReply };

        MxCommandReply mapped = new MxAccessGrpcMapper().MapCommandReply(workerReply);

        Assert.Same(innerReply, mapped);
    }

    /// <summary>
    ///     Verifies MapEvent transfers ownership of the inner MxEvent (GWC-07 / IPC-05): the
    ///     returned reference is the same instance carried by the WorkerEvent, not a clone. The
    ///     WorkerEvent is discarded after mapping and the distributor pump is its single consumer,
    ///     so moving the inner event out is safe and avoids a per-event deep copy.
    /// </summary>
    [Fact]
    public void MapEvent_TransfersOwnershipOfInnerEventWithoutCloning()
    {
        MxEvent innerEvent = new()
        {
            Family = MxEventFamily.OnDataChange,
            RawStatus = "OK",
        };
        WorkerEvent workerEvent = new() { Event = innerEvent };

        MxEvent mapped = new MxAccessGrpcMapper().MapEvent(workerEvent);

        Assert.Same(innerEvent, mapped);
    }

    /// <summary>Verifies that a worker event with no public payload maps to the unspecified-family sentinel.</summary>
    [Fact]
    public void MapEvent_WhenEventMissing_ReturnsUnspecifiedFamilySentinel()
    {
        MxEvent mapped = new MxAccessGrpcMapper().MapEvent(new WorkerEvent());

        Assert.Equal(MxEventFamily.Unspecified, mapped.Family);
        Assert.Equal("Worker event did not contain a public event payload.", mapped.RawStatus);
    }
}
