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

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

/// <summary>
///     05/STAB-8 — a dead FOCAS CNC must not pay a full two-socket reconnect on every tick of each
///     read / probe / fixed-tree / recycle loop. The per-device <see cref="ConnectionBackoff"/>
///     gates the connect path; the probe bypasses it and a successful connect resets it. No FOCAS
///     fixture exists — the fake-client suite is authoritative.
/// </summary>
[Trait("Category", "Unit")]
public sealed class FocasConnectBackoffTests
{
    private static FocasDriverOptions Options(bool probeEnabled) => new()
    {
        Devices = [new FocasDeviceOptions("focas://10.0.0.5:8193")],
        RawTags = [FocasRawTags.Entry(new FocasTagDefinition("R", "focas://10.0.0.5:8193", "R100", FocasDataType.Byte))],
        Probe = new FocasProbeOptions
        {
            Enabled = probeEnabled,
            Interval = TimeSpan.FromMilliseconds(50),
            Timeout = TimeSpan.FromMilliseconds(250),
        },
        // FixedTree / HandleRecycle / AlarmProjection default off — keep only the data path (+ probe).
    };

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

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

        // One real connect attempt; the remaining five fail fast inside the window — no new socket
        // 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 FakeFocasClientFactory
        {
            Customise = () => new FakeFocasClient
            {
                ThrowOnConnect = fail,
                Exception = new InvalidOperationException("cnc down"),
            },
        };
        var drv = new FocasDriver(Options(probeEnabled: true), "focas-1", factory);
        await drv.InitializeAsync("{}", CancellationToken.None);

        var down = await drv.ReadAsync(["R"], CancellationToken.None);
        down[0].StatusCode.ShouldBe(FocasStatusMapper.BadCommunicationError);

        // Device recovers. The probe bypasses the still-open window, reconnects, and resets it 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(["R"], CancellationToken.None);
            if (r[0].StatusCode == FocasStatusMapper.Good) { good = r[0]; break; }
            await Task.Delay(25, CancellationToken.None);
        }

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