using System.IO;
using System.Linq;
using System.Threading.Tasks;
using ZB.MOM.WW.MxGateway.Contracts;
using ZB.MOM.WW.MxGateway.Contracts.Proto;
using ZB.MOM.WW.MxGateway.Worker.Ipc;
using ZB.MOM.WW.MxGateway.Worker.Tests.TestSupport;

namespace ZB.MOM.WW.MxGateway.Worker.Tests.Ipc;

public sealed class WorkerFrameProtocolTests
{
    private const string SessionId = "session-1";
    private const string Nonce = "nonce-secret";

    /// <summary>Verifies that valid envelopes round-trip through write and read.</summary>
    [Fact]
    public async Task WriteAndReadAsync_WithValidEnvelope_RoundTripsFrame()
    {
        WorkerFrameProtocolOptions options = CreateOptions();
        using MemoryStream stream = new();
        WorkerEnvelope original = CreateGatewayHelloEnvelope();

        WorkerFrameWriter writer = new(stream, options);
        await writer.WriteAsync(original);
        stream.Position = 0;

        WorkerFrameReader reader = new(stream, options);
        WorkerEnvelope parsed = await reader.ReadAsync();

        Assert.Equal(original, parsed);
    }

    /// <summary>Verifies that wrong protocol version throws mismatch error.</summary>
    [Fact]
    public async Task ReadAsync_WithWrongProtocolVersion_ThrowsProtocolVersionMismatch()
    {
        WorkerFrameProtocolOptions options = CreateOptions();
        WorkerEnvelope envelope = CreateGatewayHelloEnvelope();
        envelope.ProtocolVersion++;
        using MemoryStream stream = new(WorkerFrameTestHelpers.CreateFrame(envelope));

        WorkerFrameReader reader = new(stream, options);
        WorkerFrameProtocolException exception =
            await Assert.ThrowsAsync<WorkerFrameProtocolException>(
                async () => await reader.ReadAsync());

        Assert.Equal(WorkerFrameProtocolErrorCode.ProtocolVersionMismatch, exception.ErrorCode);
    }

    /// <summary>Verifies that wrong session ID throws mismatch error.</summary>
    [Fact]
    public async Task ReadAsync_WithWrongSessionId_ThrowsSessionMismatch()
    {
        WorkerFrameProtocolOptions options = CreateOptions();
        WorkerEnvelope envelope = CreateGatewayHelloEnvelope();
        envelope.SessionId = "different-session";
        using MemoryStream stream = new(WorkerFrameTestHelpers.CreateFrame(envelope));

        WorkerFrameReader reader = new(stream, options);
        WorkerFrameProtocolException exception =
            await Assert.ThrowsAsync<WorkerFrameProtocolException>(
                async () => await reader.ReadAsync());

        Assert.Equal(WorkerFrameProtocolErrorCode.SessionMismatch, exception.ErrorCode);
    }

    /// <summary>
    /// Verifies that a frame whose length prefix is zero is rejected before the
    /// payload buffer is allocated. <c>docs/WorkerFrameProtocol.md</c> states the
    /// reader rejects zero-length payloads as a malformed-length error. The
    /// length prefix is the leading four bytes of the stream, so a four-zero-byte
    /// stream is exactly a frame declaring a zero-length payload.
    /// </summary>
    [Fact]
    public async Task ReadAsync_WithZeroLengthPayload_ThrowsMalformedLength()
    {
        WorkerFrameProtocolOptions options = CreateOptions();
        using MemoryStream stream = new(new byte[sizeof(uint)]);

        WorkerFrameReader reader = new(stream, options);
        WorkerFrameProtocolException exception =
            await Assert.ThrowsAsync<WorkerFrameProtocolException>(
                async () => await reader.ReadAsync());

        Assert.Equal(WorkerFrameProtocolErrorCode.MalformedLength, exception.ErrorCode);
    }

    /// <summary>
    /// Verifies that a frame whose length prefix exceeds the configured maximum
    /// is rejected before the payload buffer is allocated. <c>docs/WorkerFrameProtocol.md</c>
    /// states the reader rejects oversized payloads as a message-too-large error.
    /// A small maximum is configured so the rejection is asserted without
    /// allocating a multi-megabyte buffer.
    /// </summary>
    [Fact]
    public async Task ReadAsync_WithPayloadAboveConfiguredMaximum_ThrowsMessageTooLarge()
    {
        const int maxMessageBytes = 64;
        WorkerFrameProtocolOptions options = new(
            SessionId,
            GatewayContractInfo.WorkerProtocolVersion,
            Nonce,
            maxMessageBytes);
        byte[] frame = new byte[sizeof(uint)];
        WorkerFrameTestHelpers.WriteUInt32LittleEndian(frame, maxMessageBytes + 1);
        using MemoryStream stream = new(frame);

        WorkerFrameReader reader = new(stream, options);
        WorkerFrameProtocolException exception =
            await Assert.ThrowsAsync<WorkerFrameProtocolException>(
                async () => await reader.ReadAsync());

        Assert.Equal(WorkerFrameProtocolErrorCode.MessageTooLarge, exception.ErrorCode);
    }

    /// <summary>Verifies that malformed payload throws invalid envelope error.</summary>
    [Fact]
    public async Task ReadAsync_WithMalformedPayload_ThrowsInvalidEnvelope()
    {
        WorkerFrameProtocolOptions options = CreateOptions();
        using MemoryStream stream = new(WorkerFrameTestHelpers.CreateFrame(new byte[] { 0x80 }));

        WorkerFrameReader reader = new(stream, options);
        WorkerFrameProtocolException exception =
            await Assert.ThrowsAsync<WorkerFrameProtocolException>(
                async () => await reader.ReadAsync());

        Assert.Equal(WorkerFrameProtocolErrorCode.InvalidEnvelope, exception.ErrorCode);
    }

    /// <summary>
    ///     Worker.Tests-021 (a): pins the <c>EndOfStream</c> branch of
    ///     <c>WorkerFrameReader.ReadExactlyOrThrowAsync</c>. The gateway
    ///     closing its end of the pipe during a partial-frame read is the
    ///     most common production transport failure; the reader must
    ///     surface this as <c>WorkerFrameProtocolErrorCode.EndOfStream</c>
    ///     so the worker session can fault deterministically rather than
    ///     spinning on a partial buffer. The stream here declares a 100-byte
    ///     payload but only supplies 50 bytes, so the inner read loop sees
    ///     <c>bytesRead == 0</c> mid-frame.
    /// </summary>
    [Fact]
    public async Task ReadAsync_WhenStreamEndsMidFrame_ThrowsEndOfStream()
    {
        WorkerFrameProtocolOptions options = CreateOptions();
        byte[] frame = new byte[sizeof(uint) + 50];
        WorkerFrameTestHelpers.WriteUInt32LittleEndian(frame, 100);
        using MemoryStream stream = new(frame);

        WorkerFrameReader reader = new(stream, options);
        WorkerFrameProtocolException exception =
            await Assert.ThrowsAsync<WorkerFrameProtocolException>(
                async () => await reader.ReadAsync());

        Assert.Equal(WorkerFrameProtocolErrorCode.EndOfStream, exception.ErrorCode);
    }

    /// <summary>
    ///     Worker.Tests-021 (b): pins the writer-side
    ///     <c>MessageTooLarge</c> branch. A session that constructs an
    ///     envelope whose serialised size exceeds <c>MaxMessageBytes</c>
    ///     must be rejected by the writer before any bytes are sent down
    ///     the pipe, so a misbehaving producer cannot push the receiver
    ///     past its bounds. A small <c>MaxMessageBytes</c> is configured
    ///     so a modest <c>GatewayHello</c> payload — with its nonce
    ///     padded out to several hundred bytes — exceeds the limit
    ///     without allocating anything large.
    /// </summary>
    [Fact]
    public async Task WriteAsync_WithEnvelopeAboveConfiguredMaximum_ThrowsMessageTooLarge()
    {
        const int maxMessageBytes = 64;
        WorkerFrameProtocolOptions options = new(
            SessionId,
            GatewayContractInfo.WorkerProtocolVersion,
            Nonce,
            maxMessageBytes);
        using MemoryStream stream = new();
        WorkerFrameWriter writer = new(stream, options);

        WorkerEnvelope envelope = CreateGatewayHelloEnvelope();
        envelope.GatewayHello.GatewayVersion = new string('x', 1024);

        WorkerFrameProtocolException exception =
            await Assert.ThrowsAsync<WorkerFrameProtocolException>(
                async () => await writer.WriteAsync(envelope));

        Assert.Equal(WorkerFrameProtocolErrorCode.MessageTooLarge, exception.ErrorCode);
        Assert.Equal(0, stream.Length);
    }

    /// <summary>
    ///     Worker.Tests-021 (c): documents that the writer-side
    ///     <c>InvalidEnvelope</c> branch (raised when
    ///     <c>WorkerEnvelope.CalculateSize()</c> returns 0) is unreachable
    ///     through public API. <c>WorkerEnvelopeValidator.Validate</c> (run
    ///     before the size check in <c>WorkerFrameWriter.WriteAsync</c>)
    ///     rejects any envelope whose <c>BodyCase</c> is <c>None</c> with
    ///     <c>InvalidEnvelope</c>; a body-less envelope is therefore
    ///     intercepted before the empty-payload branch can fire. Any
    ///     envelope carrying a typed body serialises at least the field
    ///     tag bytes, so <c>CalculateSize()</c> is strictly positive. This
    ///     test exercises the body-less path and asserts the same
    ///     <c>InvalidEnvelope</c> error code reaches the caller, pinning
    ///     the contract that "no body" is rejected before any size check.
    ///     The defensive zero-length branch in <c>WriteAsync</c> is left
    ///     in place because the cost is one comparison and removing it
    ///     would weaken the writer against future serialisation
    ///     regressions; this test makes its rationale visible.
    /// </summary>
    [Fact]
    public async Task WriteAsync_WithEmptyEnvelope_ThrowsInvalidEnvelopeFromValidator()
    {
        WorkerFrameProtocolOptions options = CreateOptions();
        using MemoryStream stream = new();
        WorkerFrameWriter writer = new(stream, options);

        WorkerEnvelope envelope = new()
        {
            ProtocolVersion = GatewayContractInfo.WorkerProtocolVersion,
            SessionId = SessionId,
            Sequence = 1,
            // No body — BodyCase == None, validator rejects.
        };

        WorkerFrameProtocolException exception =
            await Assert.ThrowsAsync<WorkerFrameProtocolException>(
                async () => await writer.WriteAsync(envelope));

        Assert.Equal(WorkerFrameProtocolErrorCode.InvalidEnvelope, exception.ErrorCode);
        Assert.Equal(0, stream.Length);
    }

    /// <summary>Verifies that concurrent writes produce complete serialized frames.</summary>
    [Fact]
    public async Task WriteAsync_WithConcurrentCalls_SerializesCompleteFrames()
    {
        WorkerFrameProtocolOptions options = CreateOptions();
        using MemoryStream stream = new();
        WorkerFrameWriter writer = new(stream, options);

        await Task.WhenAll(
            writer.WriteAsync(CreateGatewayHelloEnvelope(sequence: 1)),
            writer.WriteAsync(CreateGatewayHelloEnvelope(sequence: 2)),
            writer.WriteAsync(CreateGatewayHelloEnvelope(sequence: 3)));

        stream.Position = 0;
        WorkerFrameReader reader = new(stream, options);

        WorkerEnvelope first = await reader.ReadAsync();
        WorkerEnvelope second = await reader.ReadAsync();
        WorkerEnvelope third = await reader.ReadAsync();

        Assert.Equal(new ulong[] { 1, 2, 3 }, new[] { first.Sequence, second.Sequence, third.Sequence }.OrderBy(sequence => sequence));
    }

    /// <summary>
    /// Worker-009 regression: the reader rents its payload buffer from a
    /// shared pool, so a rented buffer can be larger than the current frame
    /// and may carry bytes from a previous, larger frame. Reading frames of
    /// differing sizes back-to-back through one reader must parse each frame
    /// using only its own payload length, never trailing pooled bytes.
    /// </summary>
    [Fact]
    public async Task ReadAsync_WithVaryingFrameSizes_ParsesEachFrameExactly()
    {
        WorkerFrameProtocolOptions options = CreateOptions();
        using MemoryStream stream = new();
        WorkerFrameWriter writer = new(stream, options);

        // A large-payload frame followed by a small-payload frame: if the
        // reader reused a pooled buffer without honouring the second frame's
        // length, the small frame would parse with stale trailing bytes.
        WorkerEnvelope large = CreateGatewayHelloEnvelope(sequence: 1);
        large.GatewayHello.GatewayVersion = new string('x', 4096);
        WorkerEnvelope small = CreateGatewayHelloEnvelope(sequence: 2);

        await writer.WriteAsync(large);
        await writer.WriteAsync(small);
        stream.Position = 0;

        WorkerFrameReader reader = new(stream, options);
        WorkerEnvelope firstParsed = await reader.ReadAsync();
        WorkerEnvelope secondParsed = await reader.ReadAsync();

        Assert.Equal(large, firstParsed);
        Assert.Equal(small, secondParsed);
    }

    private static WorkerFrameProtocolOptions CreateOptions()
    {
        return new WorkerFrameProtocolOptions(
            SessionId,
            GatewayContractInfo.WorkerProtocolVersion,
            Nonce);
    }

    private static WorkerEnvelope CreateGatewayHelloEnvelope(ulong sequence = 1)
    {
        return new WorkerEnvelope
        {
            ProtocolVersion = GatewayContractInfo.WorkerProtocolVersion,
            SessionId = SessionId,
            Sequence = sequence,
            GatewayHello = new GatewayHello
            {
                SupportedProtocolVersion = GatewayContractInfo.WorkerProtocolVersion,
                Nonce = Nonce,
                GatewayVersion = "test-gateway",
            },
        };
    }

}
