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

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

/// <summary>
///     05/STAB-8 — a dead AbCip device must not re-create + re-initialize (Forward-Open) a libplctag
///     runtime per tag per tick. The per-device <see cref="ConnectionBackoff"/> gates the
///     runtime-create path; cache hits are unaffected; the probe loop (its own runtime) drives
///     failure/success so a recovered device resets the window.
/// </summary>
[Trait("Category", "Unit")]
public sealed class AbCipConnectBackoffTests
{
    private static AbCipDriverOptions Options(bool probeEnabled) => new()
    {
        Devices = [new AbCipDeviceOptions("ab://10.0.0.5/1,0")],
        RawTags = AbCipRawTags.From(new AbCipTagDefinition("Speed", "ab://10.0.0.5/1,0", "Motor1.Speed", AbCipDataType.DInt)),
        Probe = new AbCipProbeOptions
        {
            Enabled = probeEnabled,
            ProbeTagPath = "ProbeTag",
            Interval = TimeSpan.FromMilliseconds(50),
            Timeout = TimeSpan.FromMilliseconds(250),
        },
    };

    /// <summary>Rapid reads against a dead device attempt a single Forward-Open, then fail fast inside the backoff window.</summary>
    [Fact]
    public async Task DeadDevice_ConnectAttemptsFollowBackoffSchedule()
    {
        var creates = 0;
        var factory = new FakeAbCipTagFactory
        {
            Customise = p =>
            {
                Interlocked.Increment(ref creates);
                return new FakeAbCipTag(p) { ThrowOnInitialize = true, Exception = new InvalidOperationException("down") };
            },
        };
        var drv = new AbCipDriver(Options(probeEnabled: false), "abcip-1", factory);
        await drv.InitializeAsync("{}", CancellationToken.None);

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

        creates.ShouldBe(1); // one Forward-Open attempt; the rest fail fast inside the window
    }

    /// <summary>A cached healthy runtime is read without ever consulting the backoff or re-creating.</summary>
    [Fact]
    public async Task CacheHit_NeverConsultsBackoff()
    {
        var creates = 0;
        var factory = new FakeAbCipTagFactory
        {
            Customise = p =>
            {
                Interlocked.Increment(ref creates);
                return new FakeAbCipTag(p) { Value = 5 };
            },
        };
        var drv = new AbCipDriver(Options(probeEnabled: false), "abcip-1", factory);
        await drv.InitializeAsync("{}", CancellationToken.None);

        for (var i = 0; i < 5; i++)
        {
            var r = await drv.ReadAsync(["Speed"], CancellationToken.None);
            r[0].StatusCode.ShouldBe(AbCipStatusMapper.Good);
        }

        creates.ShouldBe(1); // created once + cached; cache hits never re-create
    }

    /// <summary>The probe bypasses the backoff window and a successful probe resets it so the next read succeeds.</summary>
    [Fact]
    public async Task ProbeBypassesBackoff_AndSuccessResets()
    {
        var fail = true;
        var factory = new FakeAbCipTagFactory
        {
            Customise = p => new FakeAbCipTag(p)
            {
                ThrowOnInitialize = fail,
                Exception = new InvalidOperationException("down"),
                Value = 7,
            },
        };
        var drv = new AbCipDriver(Options(probeEnabled: true), "abcip-1", factory);
        await drv.InitializeAsync("{}", CancellationToken.None);

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

        fail = false; // device recovers; the probe bypasses the window and resets it
        var deadline = DateTime.UtcNow + TimeSpan.FromSeconds(3);
        DataValueSnapshot? good = null;
        while (DateTime.UtcNow < deadline)
        {
            var r = await drv.ReadAsync(["Speed"], CancellationToken.None);
            if (r[0].StatusCode == AbCipStatusMapper.Good) { good = r[0]; break; }
            await Task.Delay(25, CancellationToken.None);
        }

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