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

namespace ZB.MOM.WW.OtOpcUa.Core.Abstractions.Tests;

/// <summary>
///     Covers <see cref="ConnectionBackoff"/> — the shared capped-exponential backoff extracted
///     from the S7 poll fork (05/STAB-8; the seam plan R2-01 wires into the S7 connect throttle).
///     Two surfaces: the static <see cref="ConnectionBackoff.ComputeDelay"/> schedule and the
///     per-device attempt-throttle instance.
/// </summary>
[Trait("Category", "Unit")]
public sealed class ConnectionBackoffTests
{
    private static readonly TimeSpan Base = TimeSpan.FromSeconds(1);
    private static readonly TimeSpan Cap = TimeSpan.FromSeconds(30);

    /// <summary>Zero (or negative) consecutive failures returns the base interval unchanged.</summary>
    [Theory]
    [InlineData(0)]
    [InlineData(-1)]
    public void ComputeDelay_NoFailures_ReturnsBaseInterval(int failures)
        => ConnectionBackoff.ComputeDelay(Base, failures, Cap).ShouldBe(Base);

    /// <summary>The delay doubles per consecutive failure (1×, 2×, 4×, 8×) until it saturates the cap.</summary>
    [Theory]
    [InlineData(1, 1)]
    [InlineData(2, 2)]
    [InlineData(3, 4)]
    [InlineData(4, 8)]
    [InlineData(5, 16)]
    public void ComputeDelay_DoublesPerFailure(int failures, int expectedSeconds)
        => ConnectionBackoff.ComputeDelay(Base, failures, Cap)
            .ShouldBe(TimeSpan.FromSeconds(expectedSeconds));

    /// <summary>Growth saturates at the cap and never exceeds it, even at a large failure count (overflow guard).</summary>
    [Theory]
    [InlineData(6)]     // 32s would exceed 30s cap
    [InlineData(30)]
    [InlineData(1000)]  // shift saturates; ticks overflow guard returns cap
    public void ComputeDelay_SaturatesAtCap(int failures)
        => ConnectionBackoff.ComputeDelay(Base, failures, Cap).ShouldBe(Cap);

    /// <summary>A fresh throttle permits the first attempt immediately.</summary>
    [Fact]
    public void ShouldAttempt_FreshInstance_AllowsImmediately()
    {
        var backoff = new ConnectionBackoff(Base, Cap);
        backoff.ShouldAttempt(DateTime.UtcNow).ShouldBeTrue();
    }

    /// <summary>After a failure the throttle blocks inside the backoff window and reopens once it elapses.</summary>
    [Fact]
    public void RecordFailure_BlocksWithinWindow_ReopensAfter()
    {
        var backoff = new ConnectionBackoff(Base, Cap);
        var t0 = new DateTime(2026, 1, 1, 0, 0, 0, DateTimeKind.Utc);

        backoff.RecordFailure(t0);                                    // window = 1s (first failure)
        backoff.ShouldAttempt(t0).ShouldBeFalse();                    // still inside window
        backoff.ShouldAttempt(t0.AddMilliseconds(500)).ShouldBeFalse();
        backoff.ShouldAttempt(t0.AddSeconds(1)).ShouldBeTrue();       // window elapsed
    }

    /// <summary>Consecutive failures widen the window (1s then 2s).</summary>
    [Fact]
    public void RecordFailure_ConsecutiveFailures_WidenWindow()
    {
        var backoff = new ConnectionBackoff(Base, Cap);
        var t0 = new DateTime(2026, 1, 1, 0, 0, 0, DateTimeKind.Utc);

        backoff.RecordFailure(t0);                                    // 1s
        backoff.RecordFailure(t0.AddSeconds(1));                      // 2nd failure → 2s window
        backoff.ShouldAttempt(t0.AddSeconds(2)).ShouldBeFalse();      // within the 2s window
        backoff.ShouldAttempt(t0.AddSeconds(3)).ShouldBeTrue();
    }

    /// <summary>Success resets immediately — recovery is never delayed by a residual window.</summary>
    [Fact]
    public void RecordSuccess_ResetsWindowImmediately()
    {
        var backoff = new ConnectionBackoff(Base, Cap);
        var t0 = new DateTime(2026, 1, 1, 0, 0, 0, DateTimeKind.Utc);

        backoff.RecordFailure(t0);
        backoff.RecordFailure(t0);                                    // deep in a widened window
        backoff.ShouldAttempt(t0).ShouldBeFalse();

        backoff.RecordSuccess();
        backoff.ShouldAttempt(t0).ShouldBeTrue();                     // reset, no residual delay

        // And the schedule restarts from the base window on the next failure.
        backoff.RecordFailure(t0);
        backoff.ShouldAttempt(t0.AddMilliseconds(999)).ShouldBeFalse();
        backoff.ShouldAttempt(t0.AddSeconds(1)).ShouldBeTrue();
    }
}
