using Serilog;
using Shouldly;
using Xunit;
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
using ZB.MOM.WW.OtOpcUa.Core.Scripting;
using ZB.MOM.WW.OtOpcUa.Core.VirtualTags;

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

[Trait("Category", "Unit")]
public sealed class TimerTriggerSchedulerTests
{
    /// <summary>Verifies that timer interval causes periodic reevaluation of virtual tags.</summary>
    [Fact]
    public async Task Timer_interval_causes_periodic_reevaluation()
    {
        var up = new FakeUpstream();
        // Counter source — re-eval should pick up new value each tick.
        var counter = 0;
        var logger = new LoggerConfiguration().CreateLogger();

        using var engine = new VirtualTagEngine(up,
            new ScriptLoggerFactory(logger),
            logger);

        engine.Load([new VirtualTagDefinition(
            "Counter", DriverDataType.Int32,
            """return ctx.Now.Millisecond;""",  // changes on every evaluation
            ChangeTriggered: false,
            TimerInterval: TimeSpan.FromMilliseconds(100))]);

        using var sched = new TimerTriggerScheduler(engine, logger);
        sched.Start([new VirtualTagDefinition(
            "Counter", DriverDataType.Int32,
            """return ctx.Now.Millisecond;""",
            ChangeTriggered: false,
            TimerInterval: TimeSpan.FromMilliseconds(100))]);

        // Watch the value change across ticks.
        var snapshots = new List<object?>();
        using var sub = engine.Subscribe("Counter", (_, v) => snapshots.Add(v.Value));

        await Task.Delay(500);

        snapshots.Count.ShouldBeGreaterThanOrEqualTo(3, "At least 3 ticks in 500ms at 100ms cadence");
    }

    /// <summary>Verifies that tags without TimerInterval are not scheduled.</summary>
    [Fact]
    public async Task Tags_without_TimerInterval_not_scheduled()
    {
        var up = new FakeUpstream();
        var logger = new LoggerConfiguration().CreateLogger();
        using var engine = new VirtualTagEngine(up,
            new ScriptLoggerFactory(logger), logger);
        engine.Load([new VirtualTagDefinition(
            "NoTimer", DriverDataType.Int32, """return 1;""")]);

        using var sched = new TimerTriggerScheduler(engine, logger);
        sched.Start([new VirtualTagDefinition(
            "NoTimer", DriverDataType.Int32, """return 1;""")]);

        var events = new List<int>();
        using var sub = engine.Subscribe("NoTimer", (_, v) => events.Add((int)(v.Value ?? 0)));

        await Task.Delay(300);
        events.Count.ShouldBe(0, "No TimerInterval = no timer ticks");
    }

    /// <summary>Verifies that Start groups tags by interval into shared timers.</summary>
    [Fact]
    public void Start_groups_tags_by_interval_into_shared_timers()
    {
        // Smoke test — Start on a definition list with two distinct intervals must not
        // throw. Group count matches unique intervals.
        var up = new FakeUpstream();
        var logger = new LoggerConfiguration().CreateLogger();
        using var engine = new VirtualTagEngine(up,
            new ScriptLoggerFactory(logger), logger);
        engine.Load([
            new VirtualTagDefinition("Fast", DriverDataType.Int32, """return 1;""",
                TimerInterval: TimeSpan.FromSeconds(1)),
            new VirtualTagDefinition("Slow", DriverDataType.Int32, """return 2;""",
                TimerInterval: TimeSpan.FromSeconds(5)),
            new VirtualTagDefinition("AlsoFast", DriverDataType.Int32, """return 3;""",
                TimerInterval: TimeSpan.FromSeconds(1)),
        ]);

        using var sched = new TimerTriggerScheduler(engine, logger);
        Should.NotThrow(() => sched.Start(new[]
        {
            new VirtualTagDefinition("Fast", DriverDataType.Int32, """return 1;""", TimerInterval: TimeSpan.FromSeconds(1)),
            new VirtualTagDefinition("Slow", DriverDataType.Int32, """return 2;""", TimerInterval: TimeSpan.FromSeconds(5)),
            new VirtualTagDefinition("AlsoFast", DriverDataType.Int32, """return 3;""", TimerInterval: TimeSpan.FromSeconds(1)),
        }));
    }

    // ----- Core.VirtualTags-007: timer ticks must not block pool threads and must skip when prior tick is still running -----

    /// <summary>Verifies that tick is skipped when the prior tick for the same group is still running.</summary>
    [Fact]
    public async Task Tick_skips_when_prior_tick_for_the_same_group_is_still_running()
    {
        // Regression for Core.VirtualTags-007: if a single tick takes longer than the
        // interval, subsequent timer callbacks must NOT each pin a thread-pool thread
        // waiting on the same evaluation gate. The scheduler tracks an in-flight flag
        // per group and skips a new tick when the prior one is still running.
        var up = new FakeUpstream();
        up.Set("In", 1);
        var logger = new LoggerConfiguration().CreateLogger();

        // Slow script — each evaluation takes longer than several timer intervals.
        const int slowMs = 250;
        const int intervalMs = 50;
        using var engine = new VirtualTagEngine(up,
            new ScriptLoggerFactory(logger), logger);
        engine.Load([new VirtualTagDefinition(
            "Slow", DriverDataType.Int32,
            $$"""
            var end = DateTime.UtcNow.AddMilliseconds({{slowMs}});
            while (DateTime.UtcNow < end) { }
            return (int)ctx.GetTag("In").Value;
            """,
            ChangeTriggered: false,
            TimerInterval: TimeSpan.FromMilliseconds(intervalMs))]);

        using var sched = new TimerTriggerScheduler(engine, logger);
        sched.Start([new VirtualTagDefinition(
            "Slow", DriverDataType.Int32,
            "",
            ChangeTriggered: false,
            TimerInterval: TimeSpan.FromMilliseconds(intervalMs))]);

        // Wait long enough for many timer ticks at 50ms while one evaluation
        // (~250ms each) holds the engine. Window is 600ms ~ 12 ticks.
        await Task.Delay(600);

        // With the fix in place, ticks that fire while the previous one for the same
        // group is still running are skipped. The skipped count must be measurable; if
        // SkippedTickCount is still 0 after 600ms with ~12 ticks fired and a 250ms eval,
        // the fix is not working — at minimum 3-4 ticks must have been skipped.
        sched.SkippedTickCount.ShouldBeGreaterThan(2,
            "ticks that fire while the prior tick for the same group is still running must be skipped");
    }

    /// <summary>Verifies that a disposed scheduler stops firing timers.</summary>
    [Fact]
    public void Disposed_scheduler_stops_firing()
    {
        var up = new FakeUpstream();
        var logger = new LoggerConfiguration().CreateLogger();
        using var engine = new VirtualTagEngine(up,
            new ScriptLoggerFactory(logger), logger);
        engine.Load([new VirtualTagDefinition(
            "T", DriverDataType.Int32, """return 1;""",
            TimerInterval: TimeSpan.FromMilliseconds(50))]);

        var sched = new TimerTriggerScheduler(engine, logger);
        sched.Start([new VirtualTagDefinition(
            "T", DriverDataType.Int32, """return 1;""",
            TimerInterval: TimeSpan.FromMilliseconds(50))]);
        sched.Dispose();

        // After dispose, second Start throws ObjectDisposedException.
        Should.Throw<ObjectDisposedException>(() =>
            sched.Start([new VirtualTagDefinition(
                "T", DriverDataType.Int32, """return 1;""",
                TimerInterval: TimeSpan.FromMilliseconds(50))]));
    }

    // ----- Core.VirtualTags-015: Start must not be callable twice on the same instance -----

    /// <summary>
    ///     Verifies that a second call to Start on the same (non-disposed) scheduler
    ///     throws InvalidOperationException. Without this guard, calling Start twice
    ///     appends a duplicate set of timers and TickGroups, causing every timer-group
    ///     to fire and evaluate twice per interval with no diagnostic.
    /// </summary>
    [Fact]
    public void Start_called_twice_throws_InvalidOperationException()
    {
        // Regression for Core.VirtualTags-015: without the _started guard, two Start
        // calls silently double the timer count and the evaluation frequency.
        var up = new FakeUpstream();
        var logger = new LoggerConfiguration().CreateLogger();
        using var engine = new VirtualTagEngine(up,
            new ScriptLoggerFactory(logger), logger);
        engine.Load([new VirtualTagDefinition(
            "T", DriverDataType.Int32, """return 1;""",
            TimerInterval: TimeSpan.FromSeconds(10))]);

        using var sched = new TimerTriggerScheduler(engine, logger);
        sched.Start([new VirtualTagDefinition(
            "T", DriverDataType.Int32, """return 1;""",
            TimerInterval: TimeSpan.FromSeconds(10))]);

        // Second call on the same live instance must throw — not silently create duplicates.
        Should.Throw<InvalidOperationException>(() =>
            sched.Start([new VirtualTagDefinition(
                "T", DriverDataType.Int32, """return 1;""",
                TimerInterval: TimeSpan.FromSeconds(10))]));
    }
}
