using System.Collections.Concurrent;
using Shouldly;
using Xunit;
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
using ZB.MOM.WW.OtOpcUa.Driver.Modbus;

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

[Trait("Category", "Unit")]
public sealed class ModbusProbeTests
{
    /// <summary>
    ///     Transport fake the probe tests flip between "responding" and "unreachable" to
    ///     exercise the state machine. Calls to SendAsync with FC=0x03 count as probe traffic
    ///     (the driver's probe loop issues exactly that shape).
    /// </summary>
    private sealed class FlappyTransport : IModbusTransport
    {
        public volatile bool Reachable = true;
        public int ProbeCount;

        /// <summary>Asynchronously connects the transport.</summary>
        /// <param name="ct">The cancellation token.</param>
        /// <returns>A completed task.</returns>
        public Task ConnectAsync(CancellationToken ct) => Task.CompletedTask;

        /// <summary>Asynchronously sends a Modbus PDU and returns a response.</summary>
        /// <param name="unitId">The Modbus unit ID.</param>
        /// <param name="pdu">The protocol data unit to send.</param>
        /// <param name="ct">The cancellation token.</param>
        /// <returns>A task that returns the response bytes.</returns>
        public Task<byte[]> SendAsync(byte unitId, byte[] pdu, CancellationToken ct)
        {
            if (pdu[0] == 0x03) Interlocked.Increment(ref ProbeCount);
            if (!Reachable)
                return Task.FromException<byte[]>(new IOException("transport unreachable"));

            // Happy path — return a valid FC03 response for 1 register at addr.
            if (pdu[0] == 0x03)
            {
                var qty = (ushort)((pdu[3] << 8) | pdu[4]);
                var resp = new byte[2 + qty * 2];
                resp[0] = 0x03;
                resp[1] = (byte)(qty * 2);
                return Task.FromResult(resp);
            }
            return Task.FromException<byte[]>(new NotSupportedException());
        }

        /// <summary>Asynchronously disposes the transport.</summary>
        /// <returns>A completed task.</returns>
        public ValueTask DisposeAsync() => ValueTask.CompletedTask;
    }

    private static (ModbusDriver drv, FlappyTransport fake) NewDriver(ModbusProbeOptions probe)
    {
        var fake = new FlappyTransport();
        var opts = new ModbusDriverOptions { Host = "fake", Port = 502, Probe = probe };
        return (new ModbusDriver(opts, "modbus-1", _ => fake), fake);
    }

    /// <summary>Verifies that the initial state is Unknown before the first probe tick.</summary>
    [Fact]
    public async Task Initial_state_is_Unknown_before_first_probe_tick()
    {
        var (drv, _) = NewDriver(new ModbusProbeOptions { Enabled = false });
        await drv.InitializeAsync("{}", CancellationToken.None);

        var statuses = drv.GetHostStatuses();
        statuses.Count.ShouldBe(1);
        statuses[0].State.ShouldBe(HostState.Unknown);
        statuses[0].HostName.ShouldBe("fake:502");
    }

    /// <summary>Verifies that the first successful probe transitions the state to Running.</summary>
    [Fact]
    public async Task First_successful_probe_transitions_to_Running()
    {
        var (drv, fake) = NewDriver(new ModbusProbeOptions
        {
            Enabled = true,
            Interval = TimeSpan.FromMilliseconds(150),
            Timeout = TimeSpan.FromSeconds(1),
        });
        var transitions = new ConcurrentQueue<HostStatusChangedEventArgs>();
        drv.OnHostStatusChanged += (_, e) => transitions.Enqueue(e);

        await drv.InitializeAsync("{}", CancellationToken.None);

        // Wait for the first probe to complete.
        var deadline = DateTime.UtcNow + TimeSpan.FromSeconds(2);
        while (fake.ProbeCount == 0 && DateTime.UtcNow < deadline) await Task.Delay(25);

        // Then wait for the event to actually arrive.
        deadline = DateTime.UtcNow + TimeSpan.FromSeconds(1);
        while (transitions.Count == 0 && DateTime.UtcNow < deadline) await Task.Delay(25);

        transitions.Count.ShouldBeGreaterThanOrEqualTo(1);
        transitions.TryDequeue(out var t).ShouldBeTrue();
        t!.OldState.ShouldBe(HostState.Unknown);
        t.NewState.ShouldBe(HostState.Running);
        drv.GetHostStatuses()[0].State.ShouldBe(HostState.Running);

        await drv.ShutdownAsync(CancellationToken.None);
    }

    /// <summary>Verifies that a transport failure transitions the state to Stopped.</summary>
    [Fact]
    public async Task Transport_failure_transitions_to_Stopped()
    {
        var (drv, fake) = NewDriver(new ModbusProbeOptions
        {
            Enabled = true,
            Interval = TimeSpan.FromMilliseconds(150),
            Timeout = TimeSpan.FromSeconds(1),
        });
        var transitions = new ConcurrentQueue<HostStatusChangedEventArgs>();
        drv.OnHostStatusChanged += (_, e) => transitions.Enqueue(e);

        await drv.InitializeAsync("{}", CancellationToken.None);
        await WaitForStateAsync(drv, HostState.Running, TimeSpan.FromSeconds(2));

        fake.Reachable = false;
        await WaitForStateAsync(drv, HostState.Stopped, TimeSpan.FromSeconds(2));

        transitions.Select(t => t.NewState).ShouldContain(HostState.Stopped);
        await drv.ShutdownAsync(CancellationToken.None);
    }

    /// <summary>Verifies that recovery transitions the state from Stopped back to Running.</summary>
    [Fact]
    public async Task Recovery_transitions_Stopped_back_to_Running()
    {
        var (drv, fake) = NewDriver(new ModbusProbeOptions
        {
            Enabled = true,
            Interval = TimeSpan.FromMilliseconds(150),
            Timeout = TimeSpan.FromSeconds(1),
        });
        var transitions = new ConcurrentQueue<HostStatusChangedEventArgs>();
        drv.OnHostStatusChanged += (_, e) => transitions.Enqueue(e);

        await drv.InitializeAsync("{}", CancellationToken.None);
        await WaitForStateAsync(drv, HostState.Running, TimeSpan.FromSeconds(2));

        fake.Reachable = false;
        await WaitForStateAsync(drv, HostState.Stopped, TimeSpan.FromSeconds(2));

        fake.Reachable = true;
        await WaitForStateAsync(drv, HostState.Running, TimeSpan.FromSeconds(2));

        // We expect at minimum: Unknown→Running, Running→Stopped, Stopped→Running.
        transitions.Count.ShouldBeGreaterThanOrEqualTo(3);
        await drv.ShutdownAsync(CancellationToken.None);
    }

    /// <summary>Verifies that repeated successful probes do not generate duplicate Running events.</summary>
    [Fact]
    public async Task Repeated_successful_probes_do_not_generate_duplicate_Running_events()
    {
        var (drv, _) = NewDriver(new ModbusProbeOptions
        {
            Enabled = true,
            Interval = TimeSpan.FromMilliseconds(100),
            Timeout = TimeSpan.FromSeconds(1),
        });
        var transitions = new ConcurrentQueue<HostStatusChangedEventArgs>();
        drv.OnHostStatusChanged += (_, e) => transitions.Enqueue(e);

        await drv.InitializeAsync("{}", CancellationToken.None);
        await WaitForStateAsync(drv, HostState.Running, TimeSpan.FromSeconds(2));
        await Task.Delay(500); // several more probe ticks, all successful — state shouldn't thrash

        transitions.Count.ShouldBe(1); // only the initial Unknown→Running
        await drv.ShutdownAsync(CancellationToken.None);
    }

    /// <summary>Verifies that a disabled probe stays Unknown and fires no events.</summary>
    [Fact]
    public async Task Disabled_probe_stays_Unknown_and_fires_no_events()
    {
        var (drv, _) = NewDriver(new ModbusProbeOptions { Enabled = false });
        var transitions = new ConcurrentQueue<HostStatusChangedEventArgs>();
        drv.OnHostStatusChanged += (_, e) => transitions.Enqueue(e);

        await drv.InitializeAsync("{}", CancellationToken.None);
        await Task.Delay(300);

        transitions.Count.ShouldBe(0);
        drv.GetHostStatuses()[0].State.ShouldBe(HostState.Unknown);
        await drv.ShutdownAsync(CancellationToken.None);
    }

    /// <summary>Verifies that shutdown stops the probe loop.</summary>
    [Fact]
    public async Task Shutdown_stops_the_probe_loop()
    {
        var (drv, fake) = NewDriver(new ModbusProbeOptions
        {
            Enabled = true,
            Interval = TimeSpan.FromMilliseconds(100),
            Timeout = TimeSpan.FromSeconds(1),
        });
        await drv.InitializeAsync("{}", CancellationToken.None);
        await WaitForStateAsync(drv, HostState.Running, TimeSpan.FromSeconds(2));

        var before = fake.ProbeCount;
        await drv.ShutdownAsync(CancellationToken.None);
        await Task.Delay(400);

        // A handful of in-flight ticks may complete after shutdown in a narrow race; the
        // contract is that the loop stops scheduling new ones. Tolerate ≤1 extra.
        (fake.ProbeCount - before).ShouldBeLessThanOrEqualTo(1);
    }

    private static async Task WaitForStateAsync(ModbusDriver drv, HostState expected, TimeSpan timeout)
    {
        var deadline = DateTime.UtcNow + timeout;
        while (DateTime.UtcNow < deadline)
        {
            if (drv.GetHostStatuses()[0].State == expected) return;
            await Task.Delay(25);
        }
    }
}
