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
{
    [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");
    }

    [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");
    }

    [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)),
        }));
    }

    [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))]));
    }
}
