using Akka.Actor;
using Shouldly;
using Xunit;
using ZB.MOM.WW.OtOpcUa.Commons.OpcUa;
using ZB.MOM.WW.OtOpcUa.Runtime.Drivers;
using ZB.MOM.WW.OtOpcUa.Runtime.Tests.Harness;
using ZB.MOM.WW.OtOpcUa.Runtime.VirtualTags;

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

public sealed class DependencyMuxActorTests : RuntimeActorTestBase
{
    /// <summary>Verifies that AttributeValuePublished messages are forwarded only to subscribers interested in the specific reference.</summary>
    [Fact]
    public void AttributeValuePublished_is_forwarded_only_to_subscribers_interested_in_that_ref()
    {
        var mux = Sys.ActorOf(DependencyMuxActor.Props());
        var subA = CreateTestProbe();
        var subB = CreateTestProbe();

        mux.Tell(new DependencyMuxActor.RegisterInterest(new[] { "tag-1", "tag-2" }, subA.Ref));
        mux.Tell(new DependencyMuxActor.RegisterInterest(new[] { "tag-2", "tag-3" }, subB.Ref));

        mux.Tell(new DriverInstanceActor.AttributeValuePublished("driver-1", "tag-1", 10, OpcUaQuality.Good, DateTime.UtcNow));
        mux.Tell(new DriverInstanceActor.AttributeValuePublished("driver-1", "tag-3", 30, OpcUaQuality.Good, DateTime.UtcNow));
        mux.Tell(new DriverInstanceActor.AttributeValuePublished("driver-1", "tag-2", 20, OpcUaQuality.Good, DateTime.UtcNow));

        // subA hears tag-1 + tag-2.
        var aMsgs = new[]
        {
            subA.ExpectMsg<VirtualTagActor.DependencyValueChanged>(),
            subA.ExpectMsg<VirtualTagActor.DependencyValueChanged>(),
        }.OrderBy(m => m.TagId).ToList();
        aMsgs.Select(m => m.TagId).ShouldBe(new[] { "tag-1", "tag-2" });

        // subB hears tag-3 + tag-2.
        var bMsgs = new[]
        {
            subB.ExpectMsg<VirtualTagActor.DependencyValueChanged>(),
            subB.ExpectMsg<VirtualTagActor.DependencyValueChanged>(),
        }.OrderBy(m => m.TagId).ToList();
        bMsgs.Select(m => m.TagId).ShouldBe(new[] { "tag-2", "tag-3" });

        subA.ExpectNoMsg(TimeSpan.FromMilliseconds(100));
        subB.ExpectNoMsg(TimeSpan.FromMilliseconds(100));
    }

    /// <summary>Verifies that publishes for unregistered references are silently dropped.</summary>
    [Fact]
    public void Publish_for_unregistered_ref_is_silently_dropped()
    {
        var mux = Sys.ActorOf(DependencyMuxActor.Props());
        var sub = CreateTestProbe();

        mux.Tell(new DependencyMuxActor.RegisterInterest(new[] { "tag-1" }, sub.Ref));
        mux.Tell(new DriverInstanceActor.AttributeValuePublished("driver-1", "nobody-cares", 99, OpcUaQuality.Good, DateTime.UtcNow));

        sub.ExpectNoMsg(TimeSpan.FromMilliseconds(200));
    }

    /// <summary>Verifies that unregistering interest stops forwarding to that subscriber.</summary>
    [Fact]
    public void UnregisterInterest_stops_forwarding()
    {
        var mux = Sys.ActorOf(DependencyMuxActor.Props());
        var sub = CreateTestProbe();

        mux.Tell(new DependencyMuxActor.RegisterInterest(new[] { "tag-1" }, sub.Ref));
        mux.Tell(new DriverInstanceActor.AttributeValuePublished("driver-1", "tag-1", 10, OpcUaQuality.Good, DateTime.UtcNow));
        sub.ExpectMsg<VirtualTagActor.DependencyValueChanged>();

        mux.Tell(new DependencyMuxActor.UnregisterInterest(sub.Ref));
        mux.Tell(new DriverInstanceActor.AttributeValuePublished("driver-1", "tag-1", 20, OpcUaQuality.Good, DateTime.UtcNow));
        sub.ExpectNoMsg(TimeSpan.FromMilliseconds(200));
    }

    /// <summary>Verifies that re-registering a subscriber replaces the prior interest set.</summary>
    [Fact]
    public void Re_register_replaces_prior_interest_set()
    {
        var mux = Sys.ActorOf(DependencyMuxActor.Props());
        var sub = CreateTestProbe();

        mux.Tell(new DependencyMuxActor.RegisterInterest(new[] { "tag-1" }, sub.Ref));
        mux.Tell(new DependencyMuxActor.RegisterInterest(new[] { "tag-2" }, sub.Ref)); // replaces tag-1

        mux.Tell(new DriverInstanceActor.AttributeValuePublished("driver-1", "tag-1", 10, OpcUaQuality.Good, DateTime.UtcNow));
        sub.ExpectNoMsg(TimeSpan.FromMilliseconds(200));

        mux.Tell(new DriverInstanceActor.AttributeValuePublished("driver-1", "tag-2", 20, OpcUaQuality.Good, DateTime.UtcNow));
        sub.ExpectMsg<VirtualTagActor.DependencyValueChanged>().TagId.ShouldBe("tag-2");
    }

    /// <summary>Verifies that a virtual tag actor registers dependencies with the mux during PreStart and that evaluation fires end-to-end.</summary>
    [Fact]
    public void VirtualTagActor_PreStart_registers_deps_with_mux_and_eval_fires_end_to_end()
    {
        var mux = Sys.ActorOf(DependencyMuxActor.Props());
        var parent = CreateTestProbe();
        var evaluator = new EchoSumEvaluator();
        var actor = parent.ChildActorOf(VirtualTagActor.Props(
            "vt-1", "a+b",
            evaluator: evaluator,
            dependencyRefs: new[] { "ref-a", "ref-b" },
            mux: mux));

        // Race-safe end-to-end check: AwaitAssert retries until the PreStart RegisterInterest
        // has actually landed at the mux + the publish has fanned out. Until then the publish
        // gets dropped (no subscriber for "ref-a" yet), so we re-publish each pass.
        AwaitAssert(() =>
        {
            mux.Tell(new DriverInstanceActor.AttributeValuePublished(
                "driver-1", "ref-a", 10, OpcUaQuality.Good, DateTime.UtcNow));
            parent.ExpectMsg<VirtualTagActor.EvaluationResult>(TimeSpan.FromMilliseconds(200))
                .Value.ShouldBe(10);
        }, duration: TimeSpan.FromSeconds(3));

        // From here the actor is wired — second publish drives the sum.
        mux.Tell(new DriverInstanceActor.AttributeValuePublished("driver-1", "ref-b", 32, OpcUaQuality.Good, DateTime.UtcNow));
        parent.ExpectMsg<VirtualTagActor.EvaluationResult>(TimeSpan.FromSeconds(2)).Value.ShouldBe(42);

        // Unrelated ref shouldn't fire eval.
        mux.Tell(new DriverInstanceActor.AttributeValuePublished("driver-1", "ref-unrelated", 99, OpcUaQuality.Good, DateTime.UtcNow));
        parent.ExpectNoMsg(TimeSpan.FromMilliseconds(200));
    }

    /// <summary>Verifies that the DriverHostActor forwards AttributeValuePublished messages through the dependency mux.</summary>
    [Fact]
    public void DriverHostActor_forwards_AttributeValuePublished_through_to_mux()
    {
        // Spin a mux + a stand-in for DriverHostActor that wraps the real DriverHostActor's
        // forward path. We feed it AttributeValuePublished directly to verify the routing —
        // exercising the actual DriverHost spawn would require a deployment artifact + DI.
        var mux = Sys.ActorOf(DependencyMuxActor.Props());
        var subscriber = CreateTestProbe();
        mux.Tell(new DependencyMuxActor.RegisterInterest(new[] { "ref-1" }, subscriber.Ref));

        var hostProbe = CreateTestProbe();
        var hostActor = Sys.ActorOf(DriverHostActor.Props(
            dbFactory: NewInMemoryDbFactory(),
            localNode: ZB.MOM.WW.OtOpcUa.Commons.Types.NodeId.Parse("host-1"),
            ackRouter: hostProbe.Ref,
            dependencyMux: mux));

        // Tell the host an AttributeValuePublished — it should fan out to the mux + subscriber.
        hostActor.Tell(new DriverInstanceActor.AttributeValuePublished(
            "driver-1", "ref-1", 42, OpcUaQuality.Good, DateTime.UtcNow));

        subscriber.ExpectMsg<VirtualTagActor.DependencyValueChanged>().TagId.ShouldBe("ref-1");
    }

    /// <summary>#478 — the mux forwards the published source quality onto DependencyValueChanged so the
    /// scripted-alarm host can derive a condition's worst-of-input quality.</summary>
    [Fact]
    public void Publish_quality_is_forwarded_on_DependencyValueChanged()
    {
        var mux = Sys.ActorOf(DependencyMuxActor.Props());
        var sub = CreateTestProbe();
        mux.Tell(new DependencyMuxActor.RegisterInterest(new[] { "tag-1" }, sub.Ref));

        mux.Tell(new DriverInstanceActor.AttributeValuePublished(
            "driver-1", "tag-1", 10, OpcUaQuality.Bad, DateTime.UtcNow));

        sub.ExpectMsg<VirtualTagActor.DependencyValueChanged>().Quality.ShouldBe(OpcUaQuality.Bad);
    }

    private sealed class EchoSumEvaluator : ZB.MOM.WW.OtOpcUa.Commons.Engines.IVirtualTagEvaluator
    {
        /// <summary>Evaluates the expression by summing all dependency values as integers.</summary>
        /// <param name="id">The identifier of the virtual tag.</param>
        /// <param name="expression">The expression to evaluate.</param>
        /// <param name="deps">A dictionary of dependency values keyed by reference.</param>
        public ZB.MOM.WW.OtOpcUa.Commons.Engines.VirtualTagEvalResult Evaluate(
            string id, string expression, IReadOnlyDictionary<string, object?> deps)
        {
            var sum = deps.Values.OfType<int>().Sum();
            return ZB.MOM.WW.OtOpcUa.Commons.Engines.VirtualTagEvalResult.Ok(sum);
        }
    }
}
