using Shouldly;
using Xunit;
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
using ZB.MOM.WW.OtOpcUa.Driver.TwinCAT;

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

/// <summary>
///     05/STAB-8 — a dead TwinCAT device must not be hammered with a full connect attempt on every
///     data call. The per-device <see cref="ConnectionBackoff"/> gates the connect/create path;
///     the probe loop bypasses it (its own interval is the recovery cadence) and a successful
///     connect resets the window instantly so recovery is never delayed. No TC3 fixture exists —
///     the fake-client suite is authoritative.
/// </summary>
[Trait("Category", "Unit")]
public sealed class TwinCATConnectBackoffTests
{
    private const string Host = "ads://5.23.91.23.1.1:851";

    private static TwinCATDriverOptions Options(bool probeEnabled) => new()
    {
        Devices = [new TwinCATDeviceOptions(Host)],
        RawTags = TwinCATRawTags.Entries([new TwinCATTagDefinition("T", Host, "MAIN.T", TwinCATDataType.DInt)]),
        Probe = new TwinCATProbeOptions
        {
            Enabled = probeEnabled,
            Interval = TimeSpan.FromMilliseconds(50),
            Timeout = TimeSpan.FromMilliseconds(250),
        },
        EnableControllerBrowse = false,
    };

    /// <summary>Rapid data calls against a dead device attempt a single connect, then fail fast inside the backoff window.</summary>
    [Fact]
    public async Task DeadDevice_ConnectAttemptsFollowBackoffSchedule()
    {
        var factory = new FakeTwinCATClientFactory
        {
            Customise = () => new FakeTwinCATClient
            {
                ThrowOnConnect = true,
                Exception = new InvalidOperationException("device down"),
            },
        };
        var drv = new TwinCATDriver(Options(probeEnabled: false), "twincat-1", factory);
        await drv.InitializeAsync("{}", CancellationToken.None);

        for (var i = 0; i < 6; i++)
            await drv.ReadAsync(["T"], CancellationToken.None);

        // One real connect attempt; the remaining five fail fast inside the 1 s window — no new
        // client is created (the fake factory records one client per attempted connect).
        factory.Clients.Count.ShouldBe(1);
    }

    /// <summary>The probe bypasses the backoff window and a successful connect resets it so the next data call succeeds immediately.</summary>
    [Fact]
    public async Task ProbeBypassesBackoff_AndSuccessResets()
    {
        var fail = true;
        var factory = new FakeTwinCATClientFactory
        {
            Customise = () => new FakeTwinCATClient
            {
                ThrowOnConnect = fail,
                Exception = new InvalidOperationException("device down"),
            },
        };
        var drv = new TwinCATDriver(Options(probeEnabled: true), "twincat-1", factory);
        await drv.InitializeAsync("{}", CancellationToken.None);

        // First data call opens the backoff window on a dead device.
        var down = await drv.ReadAsync(["T"], CancellationToken.None);
        down[0].StatusCode.ShouldBe(TwinCATStatusMapper.BadCommunicationError);

        // Device recovers. The data path is still inside the window, but the probe bypasses it and
        // reconnects — resetting the window so the following data read succeeds with no residual delay.
        fail = false;
        var deadline = DateTime.UtcNow + TimeSpan.FromSeconds(3);
        DataValueSnapshot? good = null;
        while (DateTime.UtcNow < deadline)
        {
            var r = await drv.ReadAsync(["T"], CancellationToken.None);
            if (r[0].StatusCode == TwinCATStatusMapper.Good) { good = r[0]; break; }
            await Task.Delay(25, CancellationToken.None);
        }

        good.ShouldNotBeNull("probe-driven reconnect must reset the backoff so data reads recover");
    }
}
