using ZB.MOM.WW.ScadaBridge.SiteRuntime;
using ZB.MOM.WW.ScadaBridge.SiteRuntime.Scripts;

namespace ZB.MOM.WW.ScadaBridge.SiteRuntime.Tests.Scripts;

/// <summary>
/// WP3.1 test group 5 — the blocking script pool is no longer a fixed 8 threads forever. It
/// scales with the number of running instances between a configured floor and ceiling, and it
/// is deliberately GROW-ONLY: undeploying instances leaves idle threads (which cost nothing
/// measurable) rather than paying for drain/steal complexity.
/// </summary>
public class ScriptPoolSizingTests
{
    private static SiteRuntimeOptions Options(int floor = 8, int ceiling = 32) => new()
    {
        ScriptExecutionThreadCount = floor,
        ScriptExecutionMaxThreadCount = ceiling
    };

    [Theory]
    // At or below floor * 8 instances the result is exactly the pre-WP3.1 fixed size —
    // existing configurations are byte-for-byte unchanged in behaviour.
    [InlineData(0, 8)]
    [InlineData(1, 8)]
    [InlineData(64, 8)]
    // Past that, one thread per 8 instances, rounding up.
    [InlineData(65, 9)]
    [InlineData(72, 9)]
    [InlineData(200, 25)]
    // …clamped at the ceiling.
    [InlineData(256, 32)]
    [InlineData(10_000, 32)]
    public void ComputeTargetThreads_AppliesFloorRatioAndCeiling(int instances, int expected)
        => Assert.Equal(expected, ScriptExecutionScheduler.ComputeTargetThreads(instances, Options()));

    [Fact]
    public void ComputeTargetThreads_HonoursAnOverriddenFloorAndCeiling()
    {
        var options = Options(floor: 2, ceiling: 4);
        Assert.Equal(2, ScriptExecutionScheduler.ComputeTargetThreads(0, options));
        Assert.Equal(2, ScriptExecutionScheduler.ComputeTargetThreads(16, options));
        Assert.Equal(3, ScriptExecutionScheduler.ComputeTargetThreads(17, options));
        Assert.Equal(4, ScriptExecutionScheduler.ComputeTargetThreads(1000, options));
    }

    [Fact]
    public void ComputeTargetThreads_NeverReturnsLessThanOne_EvenWithADegenerateFloor()
    {
        // The validator rejects these, but a directly-constructed options object must still
        // not produce a zero-thread scheduler.
        var options = new SiteRuntimeOptions { ScriptExecutionThreadCount = 0, ScriptExecutionMaxThreadCount = 0 };
        Assert.Equal(1, ScriptExecutionScheduler.ComputeTargetThreads(0, options));
        Assert.Equal(1, ScriptExecutionScheduler.ComputeTargetThreads(500, options));
    }

    [Fact]
    public void EnsureCapacity_GrowsOnce_IsIdempotent_AndNeverShrinks()
    {
        using var scheduler = new ScriptExecutionScheduler(2);
        Assert.Equal(2, scheduler.MaximumConcurrencyLevel);

        Assert.Equal(5, scheduler.EnsureCapacity(5));
        Assert.Equal(5, scheduler.MaximumConcurrencyLevel);

        // Idempotent: asking for the same target again changes nothing.
        Assert.Equal(5, scheduler.EnsureCapacity(5));
        Assert.Equal(5, scheduler.MaximumConcurrencyLevel);

        // Grow-only: a smaller target is a no-op, not a shrink.
        Assert.Equal(5, scheduler.EnsureCapacity(1));
        Assert.Equal(5, scheduler.MaximumConcurrencyLevel);
    }

    [Fact]
    public async Task EnsureCapacity_WidensTheGauges_SoTheWholePoolIsObservable()
    {
        using var scheduler = new ScriptExecutionScheduler(1);
        scheduler.EnsureCapacity(3);

        using var gate = new ManualResetEventSlim(false);
        var blocking = Enumerable.Range(0, 3)
            .Select(_ => Task.Factory.StartNew(() => gate.Wait(),
                CancellationToken.None, TaskCreationOptions.None, scheduler))
            .ToArray();

        // All three grown workers report busy — the bookkeeping widened with the pool.
        await WaitUntilAsync(() => scheduler.BusyThreadCount == 3);
        Assert.Equal(0, scheduler.QueueDepth);
        Assert.NotNull(scheduler.OldestBusyAge);

        gate.Set();
        await Task.WhenAll(blocking);
        await WaitUntilAsync(() => scheduler.BusyThreadCount == 0);
        Assert.Null(scheduler.OldestBusyAge);
    }

    private static async Task WaitUntilAsync(Func<bool> condition)
    {
        for (var i = 0; i < 200 && !condition(); i++)
            await Task.Delay(25);
        Assert.True(condition(), "condition not met within timeout");
    }
}
