using ZB.MOM.WW.ScadaBridge.Commons.Types;

namespace ZB.MOM.WW.ScadaBridge.Commons.Tests.Types;

/// <summary>
/// Both StaleTagMonitor test classes are placed in the same non-parallel collection
/// to prevent wall-clock contention when the full suite runs under load.
/// Within each test, fixed-delay assertions are replaced with poll-until-condition
/// loops that tolerate CPU jitter on a busy machine.
/// </summary>
[Collection("StaleTagMonitor")]
public class StaleTagMonitorTests
{
    // Generous poll timeout: tests use MaxSilence values of 50–200ms; we allow up to
    // 5 seconds of total wait before declaring failure. This absorbs any scheduler lag
    // that would previously cause a tight Task.Delay window to expire before the timer
    // had a chance to fire.
    private static readonly TimeSpan PollTimeout = TimeSpan.FromSeconds(5);
    private static readonly TimeSpan PollInterval = TimeSpan.FromMilliseconds(10);

    private static async Task<bool> PollUntilAsync(Func<bool> condition, TimeSpan timeout)
    {
        var deadline = DateTime.UtcNow + timeout;
        while (DateTime.UtcNow < deadline)
        {
            if (condition()) return true;
            await Task.Delay(PollInterval);
        }
        return condition(); // final check
    }

    [Fact]
    public void Constructor_ZeroTimeSpan_Throws()
    {
        Assert.Throws<ArgumentOutOfRangeException>(() => new StaleTagMonitor(TimeSpan.Zero));
    }

    [Fact]
    public void Constructor_NegativeTimeSpan_Throws()
    {
        Assert.Throws<ArgumentOutOfRangeException>(() => new StaleTagMonitor(TimeSpan.FromSeconds(-1)));
    }

    [Fact]
    public async Task Stale_FiresAfterMaxSilence()
    {
        using var monitor = new StaleTagMonitor(TimeSpan.FromMilliseconds(100));
        var staleCount = 0;
        monitor.Stale += () => Interlocked.Increment(ref staleCount);
        monitor.Start();

        var fired = await PollUntilAsync(() => staleCount >= 1, PollTimeout);
        Assert.True(fired, "Stale event did not fire within the generous timeout.");
        Assert.Equal(1, staleCount);
    }

    [Fact]
    public async Task Stale_FiresOnlyOnce()
    {
        using var monitor = new StaleTagMonitor(TimeSpan.FromMilliseconds(50));
        var staleCount = 0;
        monitor.Stale += () => Interlocked.Increment(ref staleCount);
        monitor.Start();

        // Wait for the first (and only) fire, then hold for a bit to confirm no second fire.
        var fired = await PollUntilAsync(() => staleCount >= 1, PollTimeout);
        Assert.True(fired, "Stale event did not fire within the generous timeout.");

        // Hold long enough to confirm no second fire — the one-shot timer design means
        // a second fire can only happen if OnValueReceived is called; we do not call it.
        await Task.Delay(200);
        Assert.Equal(1, staleCount);
    }

    [Fact]
    public async Task OnValueReceived_ResetsTimer()
    {
        using var monitor = new StaleTagMonitor(TimeSpan.FromMilliseconds(200));
        var staleCount = 0;
        monitor.Stale += () => Interlocked.Increment(ref staleCount);
        monitor.Start();

        // Keep resetting before the 200ms deadline — use the monitor's own MaxSilence
        // divided by 2 as the reset cadence so timing margin is relative, not absolute.
        var resetInterval = (int)(monitor.MaxSilence.TotalMilliseconds / 2);
        for (int i = 0; i < 5; i++)
        {
            await Task.Delay(resetInterval);
            monitor.OnValueReceived();
        }

        // Should not have gone stale during the reset loop.
        Assert.Equal(0, staleCount);
    }

    [Fact]
    public async Task OnValueReceived_AllowsStaleAfterSilence()
    {
        using var monitor = new StaleTagMonitor(TimeSpan.FromMilliseconds(100));
        var staleCount = 0;
        monitor.Stale += () => Interlocked.Increment(ref staleCount);
        monitor.Start();

        // Reset once before stale fires, then go silent and poll for the eventual fire.
        await Task.Delay(50);
        monitor.OnValueReceived();

        var fired = await PollUntilAsync(() => staleCount >= 1, PollTimeout);
        Assert.True(fired, "Stale event did not fire after silence following a reset.");
        Assert.Equal(1, staleCount);
    }

    [Fact]
    public async Task OnValueReceived_ResetsStaleFlag_AllowsSecondFire()
    {
        using var monitor = new StaleTagMonitor(TimeSpan.FromMilliseconds(100));
        var staleCount = 0;
        monitor.Stale += () => Interlocked.Increment(ref staleCount);
        monitor.Start();

        // Wait for first stale via poll.
        var firstFired = await PollUntilAsync(() => staleCount >= 1, PollTimeout);
        Assert.True(firstFired, "First Stale event did not fire.");
        Assert.Equal(1, staleCount);

        // Reset — should allow second stale fire.
        monitor.OnValueReceived();
        var secondFired = await PollUntilAsync(() => staleCount >= 2, PollTimeout);
        Assert.True(secondFired, "Second Stale event did not fire after reset.");
        Assert.Equal(2, staleCount);
    }

    [Fact]
    public async Task Stop_PreventsStale()
    {
        using var monitor = new StaleTagMonitor(TimeSpan.FromMilliseconds(50));
        var staleCount = 0;
        monitor.Stale += () => Interlocked.Increment(ref staleCount);
        monitor.Start();
        monitor.Stop();

        await Task.Delay(200);
        Assert.Equal(0, staleCount);
    }

    [Fact]
    public async Task Dispose_PreventsStale()
    {
        var monitor = new StaleTagMonitor(TimeSpan.FromMilliseconds(50));
        var staleCount = 0;
        monitor.Stale += () => Interlocked.Increment(ref staleCount);
        monitor.Start();
        monitor.Dispose();

        await Task.Delay(200);
        Assert.Equal(0, staleCount);
    }

    [Fact]
    public void MaxSilence_ReturnsConfiguredValue()
    {
        using var monitor = new StaleTagMonitor(TimeSpan.FromSeconds(42));
        Assert.Equal(TimeSpan.FromSeconds(42), monitor.MaxSilence);
    }
}
