using Shouldly;
using Xunit;
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
using ZB.MOM.WW.OtOpcUa.Runtime.ScriptedAlarms;

namespace ZB.MOM.WW.OtOpcUa.Runtime.Tests.ScriptedAlarms;

/// <summary>
///     Unit tests for <see cref="DependencyMuxTagUpstreamSource"/> — the host-actor-fed
///     <c>ITagUpstreamSource</c> the scripted-alarm engine reads + subscribes through.
/// </summary>
public sealed class DependencyMuxTagUpstreamSourceTests
{
    private static DataValueSnapshot Good(object? value, DateTime ts)
        => new(value, 0u, ts, ts);

    [Fact]
    public void Push_then_ReadTag_returns_the_pushed_snapshot()
    {
        var src = new DependencyMuxTagUpstreamSource();
        var now = new DateTime(2026, 06, 10, 12, 0, 0, DateTimeKind.Utc);
        var snap = Good(42, now);

        src.Push("a/b/c", snap);

        src.ReadTag("a/b/c").ShouldBeSameAs(snap);
    }

    [Fact]
    public void ReadTag_of_unknown_path_returns_a_Bad_quality_snapshot()
    {
        var fixedNow = new DateTime(2026, 06, 10, 9, 30, 0, DateTimeKind.Utc);
        var src = new DependencyMuxTagUpstreamSource(clock: () => fixedNow);

        var snap = src.ReadTag("never/pushed");

        snap.Value.ShouldBeNull();
        // Bad: OPC UA Part 4 StatusCode bit 31 set (severity 10).
        (snap.StatusCode & 0x80000000u).ShouldNotBe(0u);
        snap.StatusCode.ShouldNotBe(0u);
        // Timestamp comes from the injected clock.
        snap.ServerTimestampUtc.ShouldBe(fixedNow);
    }

    [Fact]
    public void SubscribeTag_observer_fires_on_next_push_to_same_path_only()
    {
        var src = new DependencyMuxTagUpstreamSource();
        var now = new DateTime(2026, 06, 10, 12, 0, 0, DateTimeKind.Utc);
        var received = new List<(string Path, DataValueSnapshot Snap)>();

        using var sub = src.SubscribeTag("tag/x", (p, s) => received.Add((p, s)));

        // A push to a DIFFERENT path must NOT fire this observer.
        src.Push("tag/other", Good(1, now));
        received.ShouldBeEmpty();

        var snap = Good(7, now);
        src.Push("tag/x", snap);

        received.Count.ShouldBe(1);
        received[0].Path.ShouldBe("tag/x");
        received[0].Snap.ShouldBeSameAs(snap);
    }

    [Fact]
    public void Multiple_observers_on_same_path_all_fire()
    {
        var src = new DependencyMuxTagUpstreamSource();
        var now = new DateTime(2026, 06, 10, 12, 0, 0, DateTimeKind.Utc);
        var a = 0;
        var b = 0;

        using var subA = src.SubscribeTag("tag/y", (_, _) => a++);
        using var subB = src.SubscribeTag("tag/y", (_, _) => b++);

        src.Push("tag/y", Good(1, now));

        a.ShouldBe(1);
        b.ShouldBe(1);
    }

    [Fact]
    public void Disposing_a_subscription_stops_delivery_to_that_observer_only()
    {
        var src = new DependencyMuxTagUpstreamSource();
        var now = new DateTime(2026, 06, 10, 12, 0, 0, DateTimeKind.Utc);
        var a = 0;
        var b = 0;

        var subA = src.SubscribeTag("tag/z", (_, _) => a++);
        using var subB = src.SubscribeTag("tag/z", (_, _) => b++);

        src.Push("tag/z", Good(1, now));
        a.ShouldBe(1);
        b.ShouldBe(1);

        subA.Dispose();

        src.Push("tag/z", Good(2, now));
        a.ShouldBe(1); // disposed — no further delivery
        b.ShouldBe(2); // still subscribed

        // Dispose-after-already-removed must be a no-op, not throw.
        Should.NotThrow(() => subA.Dispose());
    }

    [Fact]
    public async Task Concurrent_push_and_subscribe_does_not_throw()
    {
        var src = new DependencyMuxTagUpstreamSource();
        var now = new DateTime(2026, 06, 10, 12, 0, 0, DateTimeKind.Utc);
        var subs = new List<IDisposable>();
        var subsLock = new object();
        Exception? failure = null;

        var pusher = Task.Run(() =>
        {
            try
            {
                for (var i = 0; i < 2000; i++)
                    src.Push("hot/path", Good(i, now));
            }
            catch (Exception ex) { failure = ex; }
        });

        var subscriber = Task.Run(() =>
        {
            try
            {
                for (var i = 0; i < 2000; i++)
                {
                    var d = src.SubscribeTag("hot/path", (_, _) => { });
                    lock (subsLock) subs.Add(d);
                }
            }
            catch (Exception ex) { failure = ex; }
        });

        await Task.WhenAll(pusher, subscriber);

        failure.ShouldBeNull();
        foreach (var d in subs) d.Dispose();
    }
}
