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;

/// <summary>
///     End-to-end VirtualTagEngine behavior: load config, subscribe to upstream,
///     evaluate on change, cascade through dependent virtual tags, timer-driven
///     re-evaluation, error isolation, historize flag, cycle rejection.
/// </summary>
[Trait("Category", "Unit")]
public sealed class VirtualTagEngineTests
{
    private static VirtualTagEngine Build(
        FakeUpstream upstream,
        IHistoryWriter? history = null,
        TimeSpan? scriptTimeout = null,
        Func<DateTime>? clock = null)
    {
        var rootLogger = new LoggerConfiguration().CreateLogger();
        return new VirtualTagEngine(
            upstream,
            new ScriptLoggerFactory(rootLogger),
            rootLogger,
            history,
            clock,
            scriptTimeout);
    }

    [Fact]
    public async Task Simple_script_reads_upstream_and_returns_coerced_value()
    {
        var up = new FakeUpstream();
        up.Set("InTag", 10.0);
        using var engine = Build(up);

        engine.Load([new VirtualTagDefinition(
            Path: "LineRate",
            DataType: DriverDataType.Float64,
            ScriptSource: """return (double)ctx.GetTag("InTag").Value * 2.0;""")]);

        await engine.EvaluateAllAsync(TestContext.Current.CancellationToken);

        var result = engine.Read("LineRate");
        result.StatusCode.ShouldBe(0u);
        result.Value.ShouldBe(20.0);
    }

    [Fact]
    public async Task Upstream_change_triggers_cascade_through_two_levels()
    {
        var up = new FakeUpstream();
        up.Set("A", 1.0);
        using var engine = Build(up);

        engine.Load([
            new VirtualTagDefinition("B", DriverDataType.Float64,
                """return (double)ctx.GetTag("A").Value + 10.0;"""),
            new VirtualTagDefinition("C", DriverDataType.Float64,
                """return (double)ctx.GetTag("B").Value * 2.0;"""),
        ]);

        await engine.EvaluateAllAsync(TestContext.Current.CancellationToken);
        engine.Read("B").Value.ShouldBe(11.0);
        engine.Read("C").Value.ShouldBe(22.0);

        // Change upstream — cascade should recompute B (11→15.0) then C (30.0)
        up.Push("A", 5.0);
        await WaitForConditionAsync(() => Equals(engine.Read("B").Value, 15.0));
        engine.Read("B").Value.ShouldBe(15.0);
        engine.Read("C").Value.ShouldBe(30.0);
    }

    [Fact]
    public async Task Cycle_in_virtual_tags_rejected_at_Load()
    {
        var up = new FakeUpstream();
        using var engine = Build(up);

        Should.Throw<DependencyCycleException>(() => engine.Load([
            new VirtualTagDefinition("A", DriverDataType.Int32, """return (int)ctx.GetTag("B").Value + 1;"""),
            new VirtualTagDefinition("B", DriverDataType.Int32, """return (int)ctx.GetTag("A").Value + 1;"""),
        ]));
        await Task.CompletedTask;
    }

    [Fact]
    public async Task Script_compile_error_surfaces_at_Load_with_all_failures()
    {
        var up = new FakeUpstream();
        using var engine = Build(up);

        var ex = Should.Throw<InvalidOperationException>(() => engine.Load([
            new VirtualTagDefinition("A", DriverDataType.Int32, """return undefinedIdentifier;"""),
            new VirtualTagDefinition("B", DriverDataType.Int32, """return 42;"""),
            new VirtualTagDefinition("C", DriverDataType.Int32, """var x = anotherUndefined; return x;"""),
        ]));
        ex.Message.ShouldContain("2 script(s) did not compile");
        ex.Message.ShouldContain("A");
        ex.Message.ShouldContain("C");
        await Task.CompletedTask;
    }

    [Fact]
    public async Task Script_runtime_exception_isolates_to_owning_tag()
    {
        var up = new FakeUpstream();
        up.Set("OK", 10);
        using var engine = Build(up);

        engine.Load([
            new VirtualTagDefinition("GoodTag", DriverDataType.Int32,
                """return (int)ctx.GetTag("OK").Value * 2;"""),
            new VirtualTagDefinition("BadTag", DriverDataType.Int32,
                """throw new InvalidOperationException("boom");"""),
        ]);
        await engine.EvaluateAllAsync(TestContext.Current.CancellationToken);

        engine.Read("GoodTag").StatusCode.ShouldBe(0u);
        engine.Read("GoodTag").Value.ShouldBe(20);
        engine.Read("BadTag").StatusCode.ShouldBe(0x80020000u, "BadInternalError for thrown script");
        engine.Read("BadTag").Value.ShouldBeNull();
    }

    [Fact]
    public async Task Timeout_maps_to_BadInternalError_without_killing_the_engine()
    {
        var up = new FakeUpstream();
        using var engine = Build(up, scriptTimeout: TimeSpan.FromMilliseconds(30));

        engine.Load([
            new VirtualTagDefinition("Hang", DriverDataType.Int32, """
                var end = Environment.TickCount64 + 5000;
                while (Environment.TickCount64 < end) { }
                return 1;
                """),
            new VirtualTagDefinition("Ok", DriverDataType.Int32, """return 42;"""),
        ]);

        await engine.EvaluateAllAsync(TestContext.Current.CancellationToken);
        engine.Read("Hang").StatusCode.ShouldBe(0x80020000u);
        engine.Read("Ok").Value.ShouldBe(42);
    }

    [Fact]
    public async Task Subscribers_receive_engine_emitted_changes()
    {
        var up = new FakeUpstream();
        up.Set("In", 1);
        using var engine = Build(up);

        engine.Load([new VirtualTagDefinition(
            "Out", DriverDataType.Int32, """return (int)ctx.GetTag("In").Value + 100;""")]);
        await engine.EvaluateAllAsync(TestContext.Current.CancellationToken);

        var received = new List<DataValueSnapshot>();
        using var sub = engine.Subscribe("Out", (p, v) => received.Add(v));

        up.Push("In", 5);
        await WaitForConditionAsync(() => received.Count >= 1);

        received[^1].Value.ShouldBe(105);
    }

    [Fact]
    public async Task Historize_flag_routes_to_history_writer()
    {
        var recorded = new List<(string, DataValueSnapshot)>();
        var history = new TestHistory(recorded);
        var up = new FakeUpstream();
        up.Set("In", 1);
        using var engine = Build(up, history);

        engine.Load([
            new VirtualTagDefinition("H", DriverDataType.Int32,
                """return (int)ctx.GetTag("In").Value + 1;""", Historize: true),
            new VirtualTagDefinition("NoH", DriverDataType.Int32,
                """return (int)ctx.GetTag("In").Value - 1;""", Historize: false),
        ]);
        await engine.EvaluateAllAsync(TestContext.Current.CancellationToken);

        recorded.Select(p => p.Item1).ShouldContain("H");
        recorded.Select(p => p.Item1).ShouldNotContain("NoH");
    }

    [Fact]
    public async Task Change_driven_false_ignores_upstream_push()
    {
        var up = new FakeUpstream();
        up.Set("In", 1);
        using var engine = Build(up);
        engine.Load([new VirtualTagDefinition(
            "Manual", DriverDataType.Int32,
            """return (int)ctx.GetTag("In").Value * 10;""",
            ChangeTriggered: false)]);

        // Initial eval seeds the value.
        await engine.EvaluateAllAsync(TestContext.Current.CancellationToken);
        engine.Read("Manual").Value.ShouldBe(10);

        // Upstream change fires but change-driven is off — no recompute.
        up.Push("In", 99);
        await Task.Delay(100);
        engine.Read("Manual").Value.ShouldBe(10, "change-driven=false ignores upstream deltas");
    }

    [Fact]
    public async Task Reload_replaces_existing_tags_and_resubscribes_cleanly()
    {
        var up = new FakeUpstream();
        up.Set("A", 1);
        up.Set("B", 2);
        using var engine = Build(up);

        engine.Load([new VirtualTagDefinition(
            "T", DriverDataType.Int32, """return (int)ctx.GetTag("A").Value * 2;""")]);
        await engine.EvaluateAllAsync(TestContext.Current.CancellationToken);
        engine.Read("T").Value.ShouldBe(2);
        up.ActiveSubscriptionCount.ShouldBe(1);

        // Reload — T now depends on B instead of A.
        engine.Load([new VirtualTagDefinition(
            "T", DriverDataType.Int32, """return (int)ctx.GetTag("B").Value * 3;""")]);
        await engine.EvaluateAllAsync(TestContext.Current.CancellationToken);
        engine.Read("T").Value.ShouldBe(6);
        up.ActiveSubscriptionCount.ShouldBe(1, "previous subscription on A must be disposed");
        await Task.CompletedTask;
    }

    [Fact]
    public async Task Dispose_releases_upstream_subscriptions()
    {
        var up = new FakeUpstream();
        up.Set("A", 1);
        var engine = Build(up);
        engine.Load([new VirtualTagDefinition(
            "T", DriverDataType.Int32, """return (int)ctx.GetTag("A").Value;""")]);
        up.ActiveSubscriptionCount.ShouldBe(1);

        engine.Dispose();
        up.ActiveSubscriptionCount.ShouldBe(0);
        await Task.CompletedTask;
    }

    [Fact]
    public async Task SetVirtualTag_within_script_updates_target_and_triggers_observers()
    {
        var up = new FakeUpstream();
        up.Set("In", 5);
        using var engine = Build(up);

        engine.Load([
            new VirtualTagDefinition("Target", DriverDataType.Int32,
                """return 0;""", ChangeTriggered: false),  // placeholder value, operator-written via SetVirtualTag
            new VirtualTagDefinition("Driver", DriverDataType.Int32,
                """
                var v = (int)ctx.GetTag("In").Value;
                ctx.SetVirtualTag("Target", v * 100);
                return v;
                """),
        ]);
        await engine.EvaluateAllAsync(TestContext.Current.CancellationToken);

        engine.Read("Target").Value.ShouldBe(500);
        engine.Read("Driver").Value.ShouldBe(5);
    }

    [Fact]
    public async Task Type_coercion_from_script_double_to_config_int32()
    {
        var up = new FakeUpstream();
        up.Set("In", 3.7);
        using var engine = Build(up);

        engine.Load([new VirtualTagDefinition(
            "Rounded", DriverDataType.Int32,
            """return (double)ctx.GetTag("In").Value;""")]);
        await engine.EvaluateAllAsync(TestContext.Current.CancellationToken);

        engine.Read("Rounded").Value.ShouldBe(4, "Convert.ToInt32 rounds 3.7 to 4");
    }

    private static async Task WaitForConditionAsync(Func<bool> cond, int timeoutMs = 2000)
    {
        var deadline = DateTime.UtcNow.AddMilliseconds(timeoutMs);
        while (DateTime.UtcNow < deadline)
        {
            if (cond()) return;
            await Task.Delay(25);
        }
        throw new TimeoutException("Condition did not become true in time");
    }

    private sealed class TestHistory : IHistoryWriter
    {
        private readonly List<(string, DataValueSnapshot)> _buf;
        public TestHistory(List<(string, DataValueSnapshot)> buf) => _buf = buf;
        public void Record(string path, DataValueSnapshot value)
        {
            lock (_buf) { _buf.Add((path, value)); }
        }
    }
}
