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

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

/// <summary>
/// SiteRuntime-009: the dedicated script-execution scheduler must run script bodies on
/// its own dedicated threads, not on the shared .NET thread pool, so blocking script
/// I/O cannot starve the global pool.
/// </summary>
public class ScriptExecutionSchedulerTests
{
    [Fact]
    public async Task Scheduler_RunsWork_OffTheThreadPool()
    {
        using var scheduler = new ScriptExecutionScheduler(2);

        bool wasThreadPoolThread = true;
        string? threadName = null;

        await Task.Factory.StartNew(() =>
        {
            wasThreadPoolThread = Thread.CurrentThread.IsThreadPoolThread;
            threadName = Thread.CurrentThread.Name;
        }, CancellationToken.None, TaskCreationOptions.DenyChildAttach, scheduler);

        Assert.False(wasThreadPoolThread,
            "Script work must not run on a shared thread-pool thread.");
        Assert.StartsWith("script-execution-", threadName);
    }

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

    [Fact]
    public void Scheduler_Shared_ReturnsSameInstanceForOptions()
    {
        var options = new SiteRuntimeOptions { ScriptExecutionThreadCount = 3 };
        var a = ScriptExecutionScheduler.Shared(options);
        var b = ScriptExecutionScheduler.Shared(options);
        Assert.Same(a, b);
    }

    // Gitea #18: IsDisposed backs the Shared() recreate guard so a disposed scheduler
    // can never be handed back and silently poison every later execution. Verified on a
    // local instance — deliberately NOT by disposing the process-wide singleton, which
    // would strand any script another test class queued on it (the very static hazard
    // #18 is about).
    [Fact]
    public void IsDisposed_FlipsOnDispose()
    {
        var scheduler = new ScriptExecutionScheduler(1);
        Assert.False(scheduler.IsDisposed);

        scheduler.Dispose();
        Assert.True(scheduler.IsDisposed);

        scheduler.Dispose(); // idempotent — stays disposed, does not throw
        Assert.True(scheduler.IsDisposed);
    }

    // SiteRuntime-S2/UA5: the scheduler exposes observability gauges so a stuck /
    // saturated script-execution pool is visible to operators via site health.
    [Fact]
    public async Task Gauges_ReflectBusyThreadAndQueueDepth()
    {
        using var scheduler = new ScriptExecutionScheduler(1);
        using var gate = new ManualResetEventSlim(false);

        var blocking = Task.Factory.StartNew(() => gate.Wait(),
            CancellationToken.None, TaskCreationOptions.None, scheduler);
        var queued = Task.Factory.StartNew(() => { },
            CancellationToken.None, TaskCreationOptions.None, scheduler);

        await WaitUntilAsync(() => scheduler.BusyThreadCount == 1);
        Assert.Equal(1, scheduler.QueueDepth);          // second task waiting
        // A busy thread reports a non-null age. Not asserting a strictly-positive
        // value: Environment.TickCount64 has ~15 ms granularity, so under load the
        // busy-timestamp and this read can fall in the same tick (age == Zero).
        Assert.NotNull(scheduler.OldestBusyAge);

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

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