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

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

[Trait("Category", "Unit")]
public sealed class FramingTests
{
    [Fact]
    public async Task FrameWriter_round_trips_single_frame_through_FrameReader()
    {
        var buffer = new MemoryStream();
        using (var writer = new FrameWriter(buffer, leaveOpen: true))
        {
            await writer.WriteAsync(FocasMessageKind.Hello,
                new Hello { PeerName = "proxy", SharedSecret = "s3cr3t" }, TestContext.Current.CancellationToken);
        }

        buffer.Position = 0;
        using var reader = new FrameReader(buffer, leaveOpen: true);
        var frame = await reader.ReadFrameAsync(TestContext.Current.CancellationToken);
        frame.ShouldNotBeNull();
        frame!.Value.Kind.ShouldBe(FocasMessageKind.Hello);
        var hello = FrameReader.Deserialize<Hello>(frame.Value.Body);
        hello.PeerName.ShouldBe("proxy");
        hello.SharedSecret.ShouldBe("s3cr3t");
    }

    [Fact]
    public async Task FrameReader_returns_null_on_clean_EOF_at_frame_boundary()
    {
        using var empty = new MemoryStream();
        using var reader = new FrameReader(empty, leaveOpen: true);
        var frame = await reader.ReadFrameAsync(TestContext.Current.CancellationToken);
        frame.ShouldBeNull();
    }

    [Fact]
    public async Task FrameReader_throws_on_oversized_length_prefix()
    {
        var hostile = new byte[] { 0x7F, 0xFF, 0xFF, 0xFF, 0x01 }; // length > 16 MiB
        using var stream = new MemoryStream(hostile);
        using var reader = new FrameReader(stream, leaveOpen: true);
        await Should.ThrowAsync<InvalidDataException>(async () =>
            await reader.ReadFrameAsync(TestContext.Current.CancellationToken));
    }

    [Fact]
    public async Task FrameReader_throws_on_mid_frame_eof()
    {
        var buffer = new MemoryStream();
        using (var writer = new FrameWriter(buffer, leaveOpen: true))
        {
            await writer.WriteAsync(FocasMessageKind.Hello, new Hello { PeerName = "x" },
                TestContext.Current.CancellationToken);
        }
        // Truncate so body is incomplete.
        var truncated = buffer.ToArray()[..(buffer.ToArray().Length - 2)];
        using var partial = new MemoryStream(truncated);
        using var reader = new FrameReader(partial, leaveOpen: true);
        await Should.ThrowAsync<EndOfStreamException>(async () =>
            await reader.ReadFrameAsync(TestContext.Current.CancellationToken));
    }

    [Fact]
    public async Task FrameWriter_serializes_concurrent_writes()
    {
        var buffer = new MemoryStream();
        using var writer = new FrameWriter(buffer, leaveOpen: true);

        var tasks = Enumerable.Range(0, 20).Select(i => writer.WriteAsync(
            FocasMessageKind.Heartbeat,
            new Heartbeat { MonotonicTicks = i },
            TestContext.Current.CancellationToken)).ToArray();
        await Task.WhenAll(tasks);

        buffer.Position = 0;
        using var reader = new FrameReader(buffer, leaveOpen: true);
        var seen = new List<long>();
        while (await reader.ReadFrameAsync(TestContext.Current.CancellationToken) is { } frame)
        {
            frame.Kind.ShouldBe(FocasMessageKind.Heartbeat);
            seen.Add(FrameReader.Deserialize<Heartbeat>(frame.Body).MonotonicTicks);
        }
        seen.Count.ShouldBe(20);
        seen.OrderBy(x => x).ShouldBe(Enumerable.Range(0, 20).Select(x => (long)x));
    }

    [Fact]
    public void MessageKind_values_are_stable()
    {
        // Guardrail — if someone reorders/renumbers, the wire format breaks for deployed peers.
        ((byte)FocasMessageKind.Hello).ShouldBe((byte)0x01);
        ((byte)FocasMessageKind.Heartbeat).ShouldBe((byte)0x03);
        ((byte)FocasMessageKind.OpenSessionRequest).ShouldBe((byte)0x10);
        ((byte)FocasMessageKind.ReadRequest).ShouldBe((byte)0x30);
        ((byte)FocasMessageKind.WriteRequest).ShouldBe((byte)0x32);
        ((byte)FocasMessageKind.PmcBitWriteRequest).ShouldBe((byte)0x34);
        ((byte)FocasMessageKind.SubscribeRequest).ShouldBe((byte)0x40);
        ((byte)FocasMessageKind.OnDataChangeNotification).ShouldBe((byte)0x43);
        ((byte)FocasMessageKind.ProbeRequest).ShouldBe((byte)0x70);
        ((byte)FocasMessageKind.ErrorResponse).ShouldBe((byte)0xFE);
    }
}
