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

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

/// <summary>
/// Verifies <see cref="VirtualTagHostActor"/> reconciles a desired set of
/// <see cref="EquipmentVirtualTagPlan"/> into child <see cref="VirtualTagActor"/>s and bridges each
/// child's <see cref="VirtualTagActor.EvaluationResult"/> onto an
/// <see cref="OpcUaPublishActor.AttributeValueUpdate"/> carrying the folder-scoped NodeId computed by
/// the materialiser.
/// </summary>
public sealed class VirtualTagHostActorTests : RuntimeActorTestBase
{
    /// <summary>A plan with no FolderPath maps onto NodeId "EquipmentId/Name".</summary>
    private static EquipmentVirtualTagPlan Plan(
        string vtagId, string equipmentId, string name, string folderPath = "") =>
        new(
            VirtualTagId: vtagId,
            EquipmentId: equipmentId,
            FolderPath: folderPath,
            Name: name,
            DataType: "Double",
            Expression: "ctx.GetTag(\"a\")",
            DependencyRefs: new[] { "a" });

    /// <summary>Spawn: an apply with one plan spins up exactly one live child VirtualTagActor.</summary>
    [Fact]
    public void ApplyVirtualTags_spawns_one_child_per_plan()
    {
        var publish = CreateTestProbe();
        var mux = CreateTestProbe();
        var host = Sys.ActorOf(VirtualTagHostActor.Props(publish.Ref, mux.Ref, new StubEvaluator()));

        host.Tell(new VirtualTagHostActor.ApplyVirtualTags(new[] { Plan("vt-1", "eq-1", "speed-rpm") }));

        // The child self-registers with the mux in PreStart, so a RegisterInterest landing on the
        // mux probe is proof the host spawned a live child.
        var reg = mux.ExpectMsg<DependencyMuxActor.RegisterInterest>();
        reg.TagRefs.ShouldContain("a");
    }

    /// <summary>KEY TEST: a child EvaluationResult is bridged to the publish actor with the
    /// folder-scoped NodeId, Value, Good quality, and source timestamp preserved.</summary>
    [Fact]
    public void EvaluationResult_is_bridged_with_folder_scoped_NodeId()
    {
        var publish = CreateTestProbe();
        var host = Sys.ActorOf(VirtualTagHostActor.Props(publish.Ref, mux: null, new StubEvaluator()));

        host.Tell(new VirtualTagHostActor.ApplyVirtualTags(new[] { Plan("vt-1", "eq-1", "speed-rpm") }));

        var ts = new DateTime(2026, 6, 7, 12, 0, 0, DateTimeKind.Utc);
        host.Tell(new VirtualTagActor.EvaluationResult("vt-1", 42.0, ts, CorrelationId.NewId()));

        var update = publish.ExpectMsg<OpcUaPublishActor.AttributeValueUpdate>();
        update.NodeId.ShouldBe("eq-1/speed-rpm");
        update.Value.ShouldBe(42.0);
        update.Quality.ShouldBe(OpcUaQuality.Good);
        update.TimestampUtc.ShouldBe(ts);
    }

    /// <summary>FolderPath is honoured in the published NodeId (EquipmentId/FolderPath/Name).</summary>
    [Fact]
    public void EvaluationResult_NodeId_includes_folder_path_when_set()
    {
        var publish = CreateTestProbe();
        var host = Sys.ActorOf(VirtualTagHostActor.Props(publish.Ref, mux: null, new StubEvaluator()));

        host.Tell(new VirtualTagHostActor.ApplyVirtualTags(
            new[] { Plan("vt-1", "eq-1", "speed-rpm", folderPath: "metrics") }));

        host.Tell(new VirtualTagActor.EvaluationResult("vt-1", 1.0, DateTime.UtcNow, CorrelationId.NewId()));

        var update = publish.ExpectMsg<OpcUaPublishActor.AttributeValueUpdate>();
        update.NodeId.ShouldBe("eq-1/metrics/speed-rpm");
    }

    /// <summary>Stop-removed: a vtag dropped from the desired set is unmapped, so a later result for
    /// it produces NO publish.</summary>
    [Fact]
    public void Removed_vtag_is_no_longer_bridged()
    {
        var publish = CreateTestProbe();
        var host = Sys.ActorOf(VirtualTagHostActor.Props(publish.Ref, mux: null, new StubEvaluator()));

        host.Tell(new VirtualTagHostActor.ApplyVirtualTags(new[] { Plan("vt-1", "eq-1", "speed-rpm") }));
        // Re-apply without vt-1 — it should be stopped + unmapped.
        host.Tell(new VirtualTagHostActor.ApplyVirtualTags(Array.Empty<EquipmentVirtualTagPlan>()));

        host.Tell(new VirtualTagActor.EvaluationResult("vt-1", 99.0, DateTime.UtcNow, CorrelationId.NewId()));

        publish.ExpectNoMsg(TimeSpan.FromMilliseconds(300));
    }

    /// <summary>Unmapped result dropped: a result for an unknown vtagId is silently ignored.</summary>
    [Fact]
    public void Result_for_unknown_vtag_is_dropped()
    {
        var publish = CreateTestProbe();
        var host = Sys.ActorOf(VirtualTagHostActor.Props(publish.Ref, mux: null, new StubEvaluator()));

        host.Tell(new VirtualTagHostActor.ApplyVirtualTags(new[] { Plan("vt-1", "eq-1", "speed-rpm") }));

        host.Tell(new VirtualTagActor.EvaluationResult("vt-unknown", 7.0, DateTime.UtcNow, CorrelationId.NewId()));

        publish.ExpectNoMsg(TimeSpan.FromMilliseconds(300));
    }

    /// <summary>
    /// After a child actor terminates unexpectedly, a subsequent ApplyVirtualTags (still containing
    /// that vtag) must re-spawn it. Proof: two distinct RegisterInterest messages arrive at the mux
    /// probe — one for the original child and one for the replacement.
    /// </summary>
    [Fact]
    public void Child_is_respawned_after_unexpected_termination()
    {
        var publish = CreateTestProbe();
        var mux = CreateTestProbe();
        var host = Sys.ActorOf(VirtualTagHostActor.Props(publish.Ref, mux.Ref, new StubEvaluator()));
        var plan = new[] { Plan("vt-1", "eq-1", "speed-rpm") };

        // First apply — child self-registers; capture the child ref from the message sender.
        host.Tell(new VirtualTagHostActor.ApplyVirtualTags(plan));
        mux.ExpectMsg<DependencyMuxActor.RegisterInterest>();
        var firstChild = mux.LastSender;

        // Watch the child from the test side so we can await its death deterministically before
        // re-applying, avoiding any race between Terminated delivery to the host and the re-apply.
        Watch(firstChild);
        Sys.Stop(firstChild);
        ExpectTerminated(firstChild);

        // The dying child's PostStop sends UnregisterInterest to the mux — drain it so the mux probe
        // mailbox is clean before we look for the new RegisterInterest.
        mux.ExpectMsg<DependencyMuxActor.UnregisterInterest>(TimeSpan.FromSeconds(5));

        // Re-apply with the same plan — host should see vt-1 absent from _children and spawn fresh.
        host.Tell(new VirtualTagHostActor.ApplyVirtualTags(plan));
        var reg2 = mux.ExpectMsg<DependencyMuxActor.RegisterInterest>(TimeSpan.FromSeconds(5));
        reg2.TagRefs.ShouldContain("a");

        // The new child must be a different actor ref than the one we killed.
        var secondChild = mux.LastSender;
        secondChild.ShouldNotBe(firstChild);
    }

    /// <summary>Deterministic no-op evaluator: keeps spawned children inert so tests drive the host's
    /// OnResult path directly via synthetic EvaluationResults.</summary>
    private sealed class StubEvaluator : IVirtualTagEvaluator
    {
        /// <summary>Returns NoChange so the child never emits on its own.</summary>
        /// <param name="id">The tag identifier.</param>
        /// <param name="expr">The expression string.</param>
        /// <param name="deps">The dependency values.</param>
        /// <returns>A NoChange result.</returns>
        public VirtualTagEvalResult Evaluate(string id, string expr, IReadOnlyDictionary<string, object?> deps)
            => VirtualTagEvalResult.NoChange;
    }
}
