using Shouldly;
using Xunit;

namespace ZB.MOM.WW.OtOpcUa.Driver.Modbus.Tests;

/// <summary>
///     Exercises <see cref="ModbusRtuOverTcpTransport"/>'s send/receive orchestration, single-flight,
///     and per-op deadline against an in-memory duplex fake injected through the <c>ForTest</c> seam —
///     no real socket. The fake records the request ADU the transport writes and replays a canned RTU
///     response on read, or blocks forever (honouring cancellation) when <c>stall</c> is set.
/// </summary>
[Trait("Category", "Unit")]
public sealed class ModbusRtuOverTcpTransportTests
{
    [Fact]
    public async Task SendAsync_writes_rtu_adu_and_returns_deframed_pdu()
    {
        // Response the fake gateway will emit: FC03, 1 reg = 0x000A.
        var response = ModbusCrc.Append(new byte[] { 0x01, 0x03, 0x02, 0x00, 0x0A });
        var fake = new CapturingDuplexStream(response);

        await using var transport = ModbusRtuOverTcpTransport.ForTest(fake, TimeSpan.FromSeconds(2));
        var pdu = await transport.SendAsync(0x01, new byte[] { 0x03, 0x00, 0x00, 0x00, 0x01 },
            TestContext.Current.CancellationToken);

        pdu.ShouldBe(new byte[] { 0x03, 0x02, 0x00, 0x0A });
        // The request went out as an RTU ADU: unit + pdu + CRC, NO MBAP header.
        fake.Written.ShouldBe(new byte[] { 0x01, 0x03, 0x00, 0x00, 0x00, 0x01, 0x84, 0x0A });
    }

    [Fact]
    public async Task SendAsync_exception_pdu_surfaces_ModbusException()
    {
        var response = ModbusCrc.Append(new byte[] { 0x01, 0x83, 0x02 });
        var fake = new CapturingDuplexStream(response);
        await using var transport = ModbusRtuOverTcpTransport.ForTest(fake, TimeSpan.FromSeconds(2));
        await Should.ThrowAsync<ModbusException>(
            transport.SendAsync(0x01, new byte[] { 0x03, 0x00, 0x00, 0x00, 0x01 },
                TestContext.Current.CancellationToken));
    }

    [Fact]
    public async Task SendAsync_stalled_gateway_hits_per_op_deadline()
    {
        var fake = new CapturingDuplexStream(respondBytes: Array.Empty<byte>(), stall: true);
        await using var transport = ModbusRtuOverTcpTransport.ForTest(fake, TimeSpan.FromMilliseconds(200));
        await Should.ThrowAsync<ModbusTransportDesyncException>(
            transport.SendAsync(0x01, new byte[] { 0x03, 0x00, 0x00, 0x00, 0x01 },
                TestContext.Current.CancellationToken));
    }

    /// <summary>
    ///     In-memory duplex <see cref="Stream"/> test double: records everything written and replays
    ///     <c>respondBytes</c> on read. When <c>stall</c> is set the read blocks until its cancellation
    ///     token fires (simulating a frozen gateway) so the transport's per-op deadline is exercised.
    /// </summary>
    private sealed class CapturingDuplexStream : Stream
    {
        private readonly byte[] _response;
        private readonly bool _stall;
        private readonly MemoryStream _written = new();
        private int _readPos;

        public CapturingDuplexStream(byte[] respondBytes, bool stall = false)
        {
            _response = respondBytes;
            _stall = stall;
        }

        /// <summary>Gets the bytes the transport wrote to this stream (the request ADU).</summary>
        public byte[] Written => _written.ToArray();

        public override bool CanRead => true;
        public override bool CanSeek => false;
        public override bool CanWrite => true;
        public override long Length => throw new NotSupportedException();
        public override long Position { get => throw new NotSupportedException(); set => throw new NotSupportedException(); }

        public override async ValueTask<int> ReadAsync(Memory<byte> buffer, CancellationToken ct = default)
        {
            if (_stall)
            {
                // Frozen gateway: never answer. Block until the per-op deadline (or caller) cancels.
                var tcs = new TaskCompletionSource();
                await using (ct.Register(() => tcs.TrySetCanceled(ct)))
                    await tcs.Task.ConfigureAwait(false);
                return 0; // unreachable — the await above always throws when cancelled
            }

            var remaining = _response.Length - _readPos;
            if (remaining <= 0) return 0;
            var n = Math.Min(remaining, buffer.Length);
            _response.AsSpan(_readPos, n).CopyTo(buffer.Span);
            _readPos += n;
            return n;
        }

        public override async ValueTask WriteAsync(ReadOnlyMemory<byte> buffer, CancellationToken ct = default)
        {
            await _written.WriteAsync(buffer, ct).ConfigureAwait(false);
        }

        public override Task FlushAsync(CancellationToken ct) => Task.CompletedTask;
        public override void Flush() { }
        public override int Read(byte[] buffer, int offset, int count) => throw new NotSupportedException();
        public override void Write(byte[] buffer, int offset, int count) => throw new NotSupportedException();
        public override long Seek(long offset, SeekOrigin origin) => throw new NotSupportedException();
        public override void SetLength(long value) => throw new NotSupportedException();
    }
}
