using MessagePack;
using Shouldly;
using Xunit;
using ZB.MOM.WW.OtOpcUa.Driver.FOCAS.Shared.Contracts;

namespace ZB.MOM.WW.OtOpcUa.Driver.FOCAS.Shared.Tests;

/// <summary>
///     MessagePack round-trip coverage for every FOCAS IPC contract. Ensures
///     <c>[Key]</c>-tagged fields survive serialize -> deserialize without loss so the
///     wire format stays stable across Proxy (.NET 10) and Host (.NET 4.8) processes.
/// </summary>
[Trait("Category", "Unit")]
public sealed class ContractRoundTripTests
{
    private static T RoundTrip<T>(T value)
    {
        var bytes = MessagePackSerializer.Serialize(value);
        return MessagePackSerializer.Deserialize<T>(bytes);
    }

    [Fact]
    public void Hello_round_trips()
    {
        var original = new Hello
        {
            ProtocolMajor = 1,
            ProtocolMinor = 2,
            PeerName = "OtOpcUa.Server",
            SharedSecret = "abc-123",
            Features = ["bulk-read", "pmc-rmw"],
        };
        var decoded = RoundTrip(original);
        decoded.ProtocolMajor.ShouldBe(1);
        decoded.ProtocolMinor.ShouldBe(2);
        decoded.PeerName.ShouldBe("OtOpcUa.Server");
        decoded.SharedSecret.ShouldBe("abc-123");
        decoded.Features.ShouldBe(["bulk-read", "pmc-rmw"]);
    }

    [Fact]
    public void HelloAck_rejected_carries_reason()
    {
        var original = new HelloAck { Accepted = false, RejectReason = "bad secret" };
        var decoded = RoundTrip(original);
        decoded.Accepted.ShouldBeFalse();
        decoded.RejectReason.ShouldBe("bad secret");
    }

    [Fact]
    public void Heartbeat_and_ack_preserve_ticks()
    {
        var hb = RoundTrip(new Heartbeat { MonotonicTicks = 987654321 });
        hb.MonotonicTicks.ShouldBe(987654321);

        var ack = RoundTrip(new HeartbeatAck { MonotonicTicks = 987654321, HostUtcUnixMs = 1_700_000_000_000 });
        ack.MonotonicTicks.ShouldBe(987654321);
        ack.HostUtcUnixMs.ShouldBe(1_700_000_000_000);
    }

    [Fact]
    public void ErrorResponse_preserves_code_and_message()
    {
        var decoded = RoundTrip(new ErrorResponse { Code = "Fwlib32Crashed", Message = "EW_UNEXPECTED" });
        decoded.Code.ShouldBe("Fwlib32Crashed");
        decoded.Message.ShouldBe("EW_UNEXPECTED");
    }

    [Fact]
    public void OpenSessionRequest_preserves_series_and_timeout()
    {
        var decoded = RoundTrip(new OpenSessionRequest
        {
            HostAddress = "192.168.1.50:8193",
            TimeoutMs = 3500,
            CncSeries = 5,
        });
        decoded.HostAddress.ShouldBe("192.168.1.50:8193");
        decoded.TimeoutMs.ShouldBe(3500);
        decoded.CncSeries.ShouldBe(5);
    }

    [Fact]
    public void OpenSessionResponse_failure_carries_error_code()
    {
        var decoded = RoundTrip(new OpenSessionResponse
        {
            Success = false,
            SessionId = 0,
            Error = "unreachable",
            ErrorCode = "EW_SOCKET",
        });
        decoded.Success.ShouldBeFalse();
        decoded.Error.ShouldBe("unreachable");
        decoded.ErrorCode.ShouldBe("EW_SOCKET");
    }

    [Fact]
    public void FocasAddressDto_carries_pmc_with_bit_index()
    {
        var decoded = RoundTrip(new FocasAddressDto
        {
            Kind = 0,
            PmcLetter = "R",
            Number = 100,
            BitIndex = 3,
        });
        decoded.Kind.ShouldBe(0);
        decoded.PmcLetter.ShouldBe("R");
        decoded.Number.ShouldBe(100);
        decoded.BitIndex.ShouldBe(3);
    }

    [Fact]
    public void FocasAddressDto_macro_omits_letter_and_bit()
    {
        var decoded = RoundTrip(new FocasAddressDto { Kind = 2, Number = 500 });
        decoded.Kind.ShouldBe(2);
        decoded.PmcLetter.ShouldBeNull();
        decoded.Number.ShouldBe(500);
        decoded.BitIndex.ShouldBeNull();
    }

    [Fact]
    public void ReadRequest_and_response_round_trip()
    {
        var req = RoundTrip(new ReadRequest
        {
            SessionId = 42,
            Address = new FocasAddressDto { Kind = 1, Number = 1815 },
            DataType = FocasDataTypeCode.Int32,
            TimeoutMs = 1500,
        });
        req.SessionId.ShouldBe(42);
        req.Address.Number.ShouldBe(1815);
        req.DataType.ShouldBe(FocasDataTypeCode.Int32);

        var resp = RoundTrip(new ReadResponse
        {
            Success = true,
            StatusCode = 0,
            ValueBytes = MessagePackSerializer.Serialize((int)12345),
            ValueTypeCode = FocasDataTypeCode.Int32,
            SourceTimestampUtcUnixMs = 1_700_000_000_000,
        });
        resp.Success.ShouldBeTrue();
        resp.StatusCode.ShouldBe(0u);
        MessagePackSerializer.Deserialize<int>(resp.ValueBytes!).ShouldBe(12345);
        resp.ValueTypeCode.ShouldBe(FocasDataTypeCode.Int32);
    }

    [Fact]
    public void WriteRequest_and_response_round_trip()
    {
        var req = RoundTrip(new WriteRequest
        {
            SessionId = 1,
            Address = new FocasAddressDto { Kind = 2, Number = 500 },
            DataType = FocasDataTypeCode.Float64,
            ValueBytes = MessagePackSerializer.Serialize(3.14159),
            ValueTypeCode = FocasDataTypeCode.Float64,
        });
        MessagePackSerializer.Deserialize<double>(req.ValueBytes!).ShouldBe(3.14159);

        var resp = RoundTrip(new WriteResponse { Success = true, StatusCode = 0 });
        resp.Success.ShouldBeTrue();
        resp.StatusCode.ShouldBe(0u);
    }

    [Fact]
    public void PmcBitWriteRequest_preserves_bit_and_value()
    {
        var req = RoundTrip(new PmcBitWriteRequest
        {
            SessionId = 7,
            Address = new FocasAddressDto { Kind = 0, PmcLetter = "Y", Number = 12 },
            BitIndex = 5,
            Value = true,
        });
        req.BitIndex.ShouldBe(5);
        req.Value.ShouldBeTrue();
    }

    [Fact]
    public void SubscribeRequest_round_trips_multiple_items()
    {
        var original = new SubscribeRequest
        {
            SessionId = 1,
            SubscriptionId = 100,
            IntervalMs = 250,
            Items =
            [
                new() { MonitoredItemId = 1, Address = new() { Kind = 0, PmcLetter = "R", Number = 100 }, DataType = FocasDataTypeCode.Bit },
                new() { MonitoredItemId = 2, Address = new() { Kind = 2, Number = 500 }, DataType = FocasDataTypeCode.Float64 },
            ],
        };
        var decoded = RoundTrip(original);
        decoded.Items.Length.ShouldBe(2);
        decoded.Items[0].MonitoredItemId.ShouldBe(1);
        decoded.Items[0].Address.PmcLetter.ShouldBe("R");
        decoded.Items[1].DataType.ShouldBe(FocasDataTypeCode.Float64);
    }

    [Fact]
    public void SubscribeResponse_rejected_items_survive()
    {
        var decoded = RoundTrip(new SubscribeResponse
        {
            Success = true,
            RejectedMonitoredItemIds = [2, 7],
        });
        decoded.RejectedMonitoredItemIds.ShouldBe([2, 7]);
    }

    [Fact]
    public void UnsubscribeRequest_round_trips()
    {
        var decoded = RoundTrip(new UnsubscribeRequest { SubscriptionId = 42 });
        decoded.SubscriptionId.ShouldBe(42);
    }

    [Fact]
    public void OnDataChangeNotification_round_trips()
    {
        var original = new OnDataChangeNotification
        {
            SubscriptionId = 100,
            Changes =
            [
                new()
                {
                    MonitoredItemId = 1,
                    StatusCode = 0,
                    ValueBytes = MessagePackSerializer.Serialize(true),
                    ValueTypeCode = FocasDataTypeCode.Bit,
                    SourceTimestampUtcUnixMs = 1_700_000_000_000,
                },
            ],
        };
        var decoded = RoundTrip(original);
        decoded.Changes.Length.ShouldBe(1);
        MessagePackSerializer.Deserialize<bool>(decoded.Changes[0].ValueBytes!).ShouldBeTrue();
    }

    [Fact]
    public void ProbeRequest_and_response_round_trip()
    {
        var req = RoundTrip(new ProbeRequest { SessionId = 1, TimeoutMs = 500 });
        req.TimeoutMs.ShouldBe(500);

        var resp = RoundTrip(new ProbeResponse { Healthy = true, ObservedAtUtcUnixMs = 1_700_000_000_000 });
        resp.Healthy.ShouldBeTrue();
        resp.ObservedAtUtcUnixMs.ShouldBe(1_700_000_000_000);
    }

    [Fact]
    public void RuntimeStatusChangeNotification_round_trips()
    {
        var decoded = RoundTrip(new RuntimeStatusChangeNotification
        {
            SessionId = 5,
            RuntimeStatus = "Stopped",
            ObservedAtUtcUnixMs = 1_700_000_000_000,
        });
        decoded.RuntimeStatus.ShouldBe("Stopped");
    }

    [Fact]
    public void RecycleHostRequest_and_response_round_trip()
    {
        var req = RoundTrip(new RecycleHostRequest { Kind = "Hard", Reason = "wedge-detected" });
        req.Kind.ShouldBe("Hard");
        req.Reason.ShouldBe("wedge-detected");

        var resp = RoundTrip(new RecycleStatusResponse { Accepted = true, GraceSeconds = 20 });
        resp.Accepted.ShouldBeTrue();
        resp.GraceSeconds.ShouldBe(20);
    }
}
