using System.Net;
using System.Net.Sockets;
using Shouldly;
using Xunit;
using ZB.MOM.WW.OtOpcUa.Driver.Modbus;

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

/// <summary>
///     Tests for <see cref="ModbusDriverProbe"/>. Each test spins up a real
///     <see cref="TcpListener"/> on 127.0.0.1 port 0 (OS-assigned) so the port is
///     guaranteed available. Server-side behaviour is driven by a background task so
///     the accept is always awaited before the probe fires.
/// </summary>
[Trait("Category", "Unit")]
public sealed class ModbusDriverProbeTests
{
    private readonly ModbusDriverProbe _probe = new();
    private static readonly TimeSpan QuickTimeout = TimeSpan.FromSeconds(3);

    // ── helpers ──────────────────────────────────────────────────────────────

    /// <summary>Allocates a TcpListener on an OS-assigned port and returns it started.</summary>
    private static TcpListener StartListener()
    {
        var l = new TcpListener(IPAddress.Loopback, 0);
        l.Start();
        return l;
    }

    /// <summary>Returns the port assigned to <paramref name="l"/>.</summary>
    private static int ListenerPort(TcpListener l)
        => ((IPEndPoint)l.LocalEndpoint).Port;

    /// <summary>
    ///     Builds a config JSON for 127.0.0.1 at <paramref name="port"/> with optional unitId.
    /// </summary>
    private static string Config(int port, byte unitId = 1)
        => $@"{{""host"":""127.0.0.1"",""port"":{port},""unitId"":{unitId}}}";

    /// <summary>
    ///     Writes a well-formed MBAP FC03 response to <paramref name="stream"/>, echoing the
    ///     transaction-id from the request's first two bytes. Reads and discards the full
    ///     request first so the client's send doesn't block.
    /// </summary>
    private static async Task WriteHappyFc03ResponseAsync(NetworkStream stream, CancellationToken ct)
    {
        // Read the MBAP header (7 bytes) + PDU (5 bytes for FC03 qty=1) = 12 bytes total.
        var req = new byte[12];
        var read = 0;
        while (read < req.Length)
        {
            var n = await stream.ReadAsync(req.AsMemory(read), ct);
            if (n == 0) return; // client closed early
            read += n;
        }

        // Echo TxId from request bytes [0..1].
        var txHi = req[0];
        var txLo = req[1];
        var unitId = req[6];

        // Response PDU: FC=0x03, ByteCount=2, Data=0x00 0x00
        var pdu = new byte[] { 0x03, 0x02, 0x00, 0x00 };

        // MBAP: TxId(2) + Proto=0(2) + Len(2) + UnitId(1) + PDU
        // Length field = unitId(1) + pdu.Length
        var respLen = (ushort)(1 + pdu.Length);
        var resp = new byte[7 + pdu.Length];
        resp[0] = txHi;
        resp[1] = txLo;
        // resp[2..3] = 0 (protocol id)
        resp[4] = (byte)(respLen >> 8);
        resp[5] = (byte)(respLen & 0xFF);
        resp[6] = unitId;
        Buffer.BlockCopy(pdu, 0, resp, 7, pdu.Length);

        await stream.WriteAsync(resp.AsMemory(), ct);
        await stream.FlushAsync(ct);
    }

    /// <summary>
    ///     Writes a Modbus exception PDU response (function 0x83 + exception code 0x02)
    ///     echoing the TxId from the request.
    /// </summary>
    private static async Task WriteExceptionPduResponseAsync(NetworkStream stream, CancellationToken ct)
    {
        // Read request
        var req = new byte[12];
        var read = 0;
        while (read < req.Length)
        {
            var n = await stream.ReadAsync(req.AsMemory(read), ct);
            if (n == 0) return;
            read += n;
        }

        var txHi = req[0];
        var txLo = req[1];
        var unitId = req[6];

        // Exception PDU: error function = 0x83 (0x03 | 0x80), exception code = 0x02
        var pdu = new byte[] { 0x83, 0x02 };
        var respLen = (ushort)(1 + pdu.Length);
        var resp = new byte[7 + pdu.Length];
        resp[0] = txHi;
        resp[1] = txLo;
        resp[4] = (byte)(respLen >> 8);
        resp[5] = (byte)(respLen & 0xFF);
        resp[6] = unitId;
        Buffer.BlockCopy(pdu, 0, resp, 7, pdu.Length);

        await stream.WriteAsync(resp.AsMemory(), ct);
        await stream.FlushAsync(ct);
    }

    // ── Test 1: invalid JSON ──────────────────────────────────────────────────

    /// <summary>Invalid config JSON returns Ok=false with a message containing "invalid".</summary>
    [Fact]
    public async Task InvalidJson_returns_false_with_invalid_message()
    {
        var result = await _probe.ProbeAsync("not valid json {{", QuickTimeout, CancellationToken.None);

        result.Ok.ShouldBeFalse();
        result.Message.ShouldNotBeNull();
        result.Message.ShouldContain("invalid", Case.Insensitive);
    }

    // ── Test 2: no host/port ─────────────────────────────────────────────────

    /// <summary>Config with empty host returns Ok=false with a "no host/port" message.</summary>
    [Fact]
    public async Task Config_with_no_host_returns_false_with_no_host_port_message()
    {
        // host defaults to "127.0.0.1" in ModbusDriverOptions, but port=0 is treated as invalid.
        var result = await _probe.ProbeAsync(@"{""host"":"""",""port"":0}", QuickTimeout, CancellationToken.None);

        result.Ok.ShouldBeFalse();
        result.Message.ShouldNotBeNull();
        result.Message.ShouldContain("no host/port", Case.Insensitive);
    }

    // ── Test 3: unreachable (closed port) ────────────────────────────────────

    /// <summary>A probe against a closed port returns Ok=false (Connect failed / refused).</summary>
    [Fact]
    public async Task Unreachable_closed_port_returns_false()
    {
        // Grab an ephemeral port then immediately stop the listener so nothing is listening.
        var listener = StartListener();
        var port = ListenerPort(listener);
        listener.Stop();

        var result = await _probe.ProbeAsync(Config(port), QuickTimeout, CancellationToken.None);

        result.Ok.ShouldBeFalse();
        result.Latency.ShouldBeNull();
    }

    // ── Test 4: TCP accepts then immediately closes (no Modbus reply) ─────────

    /// <summary>
    ///     When the server accepts the TCP connection and then closes it without sending any
    ///     Modbus bytes, ProbeAsync returns Ok=false with a "handshake failed" message.
    /// </summary>
    [Fact]
    public async Task Tcp_connect_then_immediate_close_returns_handshake_failed()
    {
        using var listener = StartListener();
        var port = ListenerPort(listener);
        using var cts = new CancellationTokenSource(QuickTimeout);

        // Background: accept the TCP client and immediately close it without replying.
        var serverTask = Task.Run(async () =>
        {
            var client = await listener.AcceptTcpClientAsync(cts.Token);
            client.Close(); // close without sending any bytes
        }, cts.Token);

        var result = await _probe.ProbeAsync(Config(port), QuickTimeout, CancellationToken.None);

        // The server task should have completed (or close enough).
        await serverTask.WaitAsync(TimeSpan.FromSeconds(2)).ConfigureAwait(false);

        result.Ok.ShouldBeFalse();
        result.Message.ShouldNotBeNull();
        result.Message.ShouldContain("handshake failed", Case.Insensitive);
        result.Latency.ShouldBeNull();
    }

    // ── Test 5: canned MBAP happy path ───────────────────────────────────────

    /// <summary>
    ///     A listener that accepts, reads the FC03 request, and replies with a valid MBAP
    ///     FC03 response causes ProbeAsync to return Ok=true with message "Modbus FC03 OK".
    /// </summary>
    [Fact]
    public async Task Happy_path_returns_ok_true_with_FC03_OK_message()
    {
        using var listener = StartListener();
        var port = ListenerPort(listener);
        using var cts = new CancellationTokenSource(QuickTimeout);

        var serverTask = Task.Run(async () =>
        {
            var client = await listener.AcceptTcpClientAsync(cts.Token);
            await using var stream = client.GetStream();
            await WriteHappyFc03ResponseAsync(stream, cts.Token);
        }, cts.Token);

        var result = await _probe.ProbeAsync(Config(port), QuickTimeout, CancellationToken.None);

        await serverTask.WaitAsync(TimeSpan.FromSeconds(3)).ConfigureAwait(false);

        result.Ok.ShouldBeTrue();
        result.Message.ShouldBe("Modbus FC03 OK");
        result.Latency.ShouldNotBeNull();
        result.Latency!.Value.ShouldBeGreaterThanOrEqualTo(TimeSpan.Zero);
    }

    // ── Test 6: Modbus exception PDU ─────────────────────────────────────────

    /// <summary>
    ///     When the server replies with a Modbus exception PDU (function 0x83 + exception code),
    ///     the probe still returns Ok=true because a real Modbus device answered.
    /// </summary>
    [Fact]
    public async Task Exception_pdu_response_returns_ok_true_with_exception_pdu_message()
    {
        using var listener = StartListener();
        var port = ListenerPort(listener);
        using var cts = new CancellationTokenSource(QuickTimeout);

        var serverTask = Task.Run(async () =>
        {
            var client = await listener.AcceptTcpClientAsync(cts.Token);
            await using var stream = client.GetStream();
            await WriteExceptionPduResponseAsync(stream, cts.Token);
        }, cts.Token);

        var result = await _probe.ProbeAsync(Config(port), QuickTimeout, CancellationToken.None);

        await serverTask.WaitAsync(TimeSpan.FromSeconds(3)).ConfigureAwait(false);

        result.Ok.ShouldBeTrue();
        result.Message.ShouldNotBeNull();
        result.Message.ShouldContain("exception PDU", Case.Insensitive);
        result.Latency.ShouldNotBeNull();
        result.Latency!.Value.ShouldBeGreaterThanOrEqualTo(TimeSpan.Zero);
    }
}
