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>
///     Verifies the IReadable + ISubscribable adapter that DriverNodeManager dispatches
///     to for NodeSource.Virtual per ADR-002. Key contract: OPC UA clients see virtual
///     tags via the same capability interfaces as driver tags, so dispatch stays
///     source-agnostic.
/// </summary>
[Trait("Category", "Unit")]
public sealed class VirtualTagSourceTests
{
    private static (VirtualTagEngine engine, VirtualTagSource source, FakeUpstream up) Build()
    {
        var up = new FakeUpstream();
        up.Set("In", 10);
        var logger = new LoggerConfiguration().CreateLogger();
        var engine = new VirtualTagEngine(up, new ScriptLoggerFactory(logger), logger);
        engine.Load([new VirtualTagDefinition(
            "Out", DriverDataType.Int32, """return (int)ctx.GetTag("In").Value * 2;""")]);
        return (engine, new VirtualTagSource(engine), up);
    }

    /// <summary>Verifies that ReadAsync returns cached engine values.</summary>
    [Fact]
    public async Task ReadAsync_returns_engine_cached_values()
    {
        var (engine, source, _) = Build();
        await engine.EvaluateAllAsync(TestContext.Current.CancellationToken);

        var results = await source.ReadAsync(["Out"], TestContext.Current.CancellationToken);
        results.Count.ShouldBe(1);
        results[0].Value.ShouldBe(20);
        results[0].StatusCode.ShouldBe(0u);
        engine.Dispose();
    }

    /// <summary>Verifies that unknown paths return Bad status quality.</summary>
    [Fact]
    public async Task ReadAsync_unknown_path_returns_Bad_quality()
    {
        var (engine, source, _) = Build();
        var results = await source.ReadAsync(["NoSuchTag"], TestContext.Current.CancellationToken);
        results[0].StatusCode.ShouldBe(0x80340000u);
        engine.Dispose();
    }

    /// <summary>Verifies that subscribe fires the initial data callback immediately.</summary>
    [Fact]
    public async Task SubscribeAsync_fires_initial_data_callback()
    {
        var (engine, source, _) = Build();
        await engine.EvaluateAllAsync(TestContext.Current.CancellationToken);

        var events = new List<DataChangeEventArgs>();
        source.OnDataChange += (_, e) => events.Add(e);

        var handle = await source.SubscribeAsync(["Out"], TimeSpan.FromMilliseconds(100),
            TestContext.Current.CancellationToken);
        handle.ShouldNotBeNull();

        // Per OPC UA convention, initial-data callback fires on subscribe.
        events.Count.ShouldBeGreaterThanOrEqualTo(1);
        events[0].FullReference.ShouldBe("Out");
        events[0].Snapshot.Value.ShouldBe(20);

        await source.UnsubscribeAsync(handle, TestContext.Current.CancellationToken);
        engine.Dispose();
    }

    /// <summary>Verifies that subscription fires on upstream changes via engine cascade.</summary>
    [Fact]
    public async Task SubscribeAsync_fires_on_upstream_change_via_engine_cascade()
    {
        var (engine, source, up) = Build();
        await engine.EvaluateAllAsync(TestContext.Current.CancellationToken);

        var events = new List<DataChangeEventArgs>();
        source.OnDataChange += (_, e) => events.Add(e);
        var handle = await source.SubscribeAsync(["Out"], TimeSpan.Zero,
            TestContext.Current.CancellationToken);

        var initialCount = events.Count;
        up.Push("In", 50);

        // Wait for the cascade.
        var deadline = DateTime.UtcNow.AddSeconds(2);
        while (DateTime.UtcNow < deadline && events.Count <= initialCount) await Task.Delay(25);

        events.Count.ShouldBeGreaterThan(initialCount);
        events[^1].Snapshot.Value.ShouldBe(100);

        await source.UnsubscribeAsync(handle, TestContext.Current.CancellationToken);
        engine.Dispose();
    }

    /// <summary>Verifies that unsubscribe stops further event emissions.</summary>
    [Fact]
    public async Task UnsubscribeAsync_stops_further_events()
    {
        var (engine, source, up) = Build();
        await engine.EvaluateAllAsync(TestContext.Current.CancellationToken);

        var events = new List<DataChangeEventArgs>();
        source.OnDataChange += (_, e) => events.Add(e);
        var handle = await source.SubscribeAsync(["Out"], TimeSpan.Zero,
            TestContext.Current.CancellationToken);

        await source.UnsubscribeAsync(handle, TestContext.Current.CancellationToken);
        var countAfterUnsub = events.Count;

        up.Push("In", 99);
        await Task.Delay(200);

        events.Count.ShouldBe(countAfterUnsub, "Unsubscribe must stop OnDataChange emissions");
        engine.Dispose();
    }

    /// <summary>Verifies that null arguments are rejected.</summary>
    [Fact]
    public async Task Null_arguments_rejected()
    {
        var (engine, source, _) = Build();
        await Should.ThrowAsync<ArgumentNullException>(async () =>
            await source.ReadAsync(null!, TestContext.Current.CancellationToken));
        await Should.ThrowAsync<ArgumentNullException>(async () =>
            await source.SubscribeAsync(null!, TimeSpan.Zero, TestContext.Current.CancellationToken));
        await Should.ThrowAsync<ArgumentNullException>(async () =>
            await source.UnsubscribeAsync(null!, TestContext.Current.CancellationToken));
        engine.Dispose();
    }
}
