using System.Collections.Concurrent;
using Akka.Actor;
using Shouldly;
using Xunit;
using ZB.MOM.WW.OtOpcUa.Commons.Engines;
using ZB.MOM.WW.OtOpcUa.Commons.Messages.Logging;
using ZB.MOM.WW.OtOpcUa.Commons.OpcUa;
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 VirtualTagActorTests : RuntimeActorTestBase
{
    /// <summary>Verifies the actor accepts dependency value changes and remains alive.</summary>
    [Fact]
    public void DependencyValueChanged_is_accepted_and_actor_stays_alive()
    {
        var actor = Sys.ActorOf(VirtualTagActor.Props("vt-1", "a + b"));
        Watch(actor);
        actor.Tell(new VirtualTagActor.DependencyValueChanged("tag-a", 10, DateTime.UtcNow));
        actor.Tell(new VirtualTagActor.DependencyValueChanged("tag-b", 20, DateTime.UtcNow));
        ExpectNoMsg(TimeSpan.FromMilliseconds(200));
    }

    /// <summary>Verifies evaluator results flow to parent as EvaluationResult.</summary>
    [Fact]
    public void Evaluator_result_flows_to_parent_as_EvaluationResult()
    {
        var parent = CreateTestProbe();
        var evaluator = new SumEvaluator();
        var actor = parent.ChildActorOf(VirtualTagActor.Props("vt-1", "a + b", evaluator: evaluator));

        actor.Tell(new VirtualTagActor.DependencyValueChanged("a", 10, DateTime.UtcNow));
        actor.Tell(new VirtualTagActor.DependencyValueChanged("b", 32, DateTime.UtcNow));

        // First dep: a alone -> 10. Second dep: a + b -> 42.
        var first = parent.ExpectMsg<VirtualTagActor.EvaluationResult>();
        first.Value.ShouldBe(10);
        var second = parent.ExpectMsg<VirtualTagActor.EvaluationResult>();
        second.Value.ShouldBe(42);
    }

    /// <summary>Verifies repeated same values do not emit a second EvaluationResult.</summary>
    [Fact]
    public void Repeated_same_value_does_not_emit_a_second_EvaluationResult()
    {
        var parent = CreateTestProbe();
        var evaluator = new ConstEvaluator(42);
        var actor = parent.ChildActorOf(VirtualTagActor.Props("vt-1", "expr", evaluator: evaluator));

        actor.Tell(new VirtualTagActor.DependencyValueChanged("a", 1, DateTime.UtcNow));
        var first = parent.ExpectMsg<VirtualTagActor.EvaluationResult>();
        first.Value.ShouldBe(42);

        actor.Tell(new VirtualTagActor.DependencyValueChanged("a", 2, DateTime.UtcNow));
        parent.ExpectNoMsg(TimeSpan.FromMilliseconds(200));
    }

    /// <summary>Verifies evaluator failure publishes ScriptLogEntry warning.</summary>
    [Fact]
    public void Evaluator_failure_publishes_ScriptLogEntry_warning()
    {
        var capture = new CapturingPublisher();
        var parent = CreateTestProbe();
        var actor = parent.ChildActorOf(VirtualTagActor.Props(
            "vt-1", "broken",
            evaluator: new FailingEvaluator("syntax error"),
            scriptId: "script-7",
            publisherFactory: () => new DPSPublisher(capture.Publish)));

        actor.Tell(new VirtualTagActor.DependencyValueChanged("a", 1, DateTime.UtcNow));

        AwaitAssert(() =>
        {
            capture.Topics.ShouldContain("script-logs");
            var entry = (ScriptLogEntry)capture.Payloads.Single();
            entry.Level.ShouldBe("Warning");
            entry.Message.ShouldContain("syntax error");
            entry.ScriptId.ShouldBe("script-7");
            entry.VirtualTagId.ShouldBe("vt-1");
        }, duration: PresenceBudget);

        // 02/S13: the failure now ALSO degrades the node — with no declared dependencyRefs the
        // inputs-ready gate is vacuously satisfied, so a Bad EvaluationResult reaches the parent (this
        // replaces the old ExpectNoMsg, which encoded the stale-Good bug).
        parent.ExpectMsg<VirtualTagActor.EvaluationResult>().Quality.ShouldBe(OpcUaQuality.Bad);
    }

    /// <summary>02/S13 once-per-transition: two consecutive failures publish exactly ONE Bad
    /// EvaluationResult — the second failure is suppressed while already Bad.</summary>
    [Fact]
    public void Second_consecutive_failure_does_not_publish_a_second_Bad()
    {
        var parent = CreateTestProbe();
        var evaluator = new QueuedEvaluator(
            VirtualTagEvalResult.Failure("boom1"), VirtualTagEvalResult.Failure("boom2"));
        var actor = parent.ChildActorOf(VirtualTagActor.Props("vt-1", "expr", evaluator: evaluator));

        actor.Tell(new VirtualTagActor.DependencyValueChanged("a", 1, DateTime.UtcNow));
        parent.ExpectMsg<VirtualTagActor.EvaluationResult>().Quality.ShouldBe(OpcUaQuality.Bad);

        actor.Tell(new VirtualTagActor.DependencyValueChanged("a", 2, DateTime.UtcNow));
        parent.ExpectNoMsg(TimeSpan.FromMilliseconds(300));
    }

    /// <summary>02/S13 same-value recovery: Ok(42) → fail → Ok(42) must republish Good with 42 even
    /// though the value equals the pre-failure value (the dedup bypass — else the node stays Bad).</summary>
    [Fact]
    public void Recovery_to_the_same_value_republishes_Good_after_Bad()
    {
        var parent = CreateTestProbe();
        var evaluator = new QueuedEvaluator(
            VirtualTagEvalResult.Ok(42), VirtualTagEvalResult.Failure("boom"), VirtualTagEvalResult.Ok(42));
        var actor = parent.ChildActorOf(VirtualTagActor.Props("vt-1", "expr", evaluator: evaluator));

        actor.Tell(new VirtualTagActor.DependencyValueChanged("a", 1, DateTime.UtcNow));
        var first = parent.ExpectMsg<VirtualTagActor.EvaluationResult>();
        first.Quality.ShouldBe(OpcUaQuality.Good);
        first.Value.ShouldBe(42);

        actor.Tell(new VirtualTagActor.DependencyValueChanged("a", 2, DateTime.UtcNow));
        parent.ExpectMsg<VirtualTagActor.EvaluationResult>().Quality.ShouldBe(OpcUaQuality.Bad);

        actor.Tell(new VirtualTagActor.DependencyValueChanged("a", 3, DateTime.UtcNow));
        var recovered = parent.ExpectMsg<VirtualTagActor.EvaluationResult>();
        recovered.Quality.ShouldBe(OpcUaQuality.Good);
        recovered.Value.ShouldBe(42);
    }

    /// <summary>02/S13: an evaluator that THROWS (not just returns Failure) also degrades quality —
    /// the try/catch path publishes a Bad EvaluationResult too.</summary>
    [Fact]
    public void Evaluator_throw_also_degrades_quality()
    {
        var parent = CreateTestProbe();
        var actor = parent.ChildActorOf(VirtualTagActor.Props(
            "vt-1", "expr", evaluator: new ThrowingEvaluator()));

        actor.Tell(new VirtualTagActor.DependencyValueChanged("a", 1, DateTime.UtcNow));

        parent.ExpectMsg<VirtualTagActor.EvaluationResult>().Quality.ShouldBe(OpcUaQuality.Bad);
    }

    /// <summary>02/S13 inputs-ready gate: a multi-dep script that fails before all its declared
    /// dependencies have arrived publishes NO Bad — the node stays at the materialiser's
    /// BadWaitingForInitialData, avoiding a false Bad flash during deploy/respawn warm-up.</summary>
    [Fact]
    public void Cold_start_failure_with_missing_dependency_publishes_no_Bad()
    {
        var parent = CreateTestProbe();
        var actor = parent.ChildActorOf(VirtualTagActor.Props(
            "vt-1", "expr",
            evaluator: new FailingEvaluator("boom"),
            dependencyRefs: new[] { "a", "b" }));

        // Only "a" has arrived — "b" is still missing, so the gate suppresses the Bad publish.
        actor.Tell(new VirtualTagActor.DependencyValueChanged("a", 1, DateTime.UtcNow));

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

    /// <summary>02/S13 inputs-ready gate: once every declared dependency has arrived at least once, a
    /// failure publishes exactly one Bad EvaluationResult.</summary>
    [Fact]
    public void Failure_after_all_dependencies_arrived_publishes_Bad()
    {
        var parent = CreateTestProbe();
        var actor = parent.ChildActorOf(VirtualTagActor.Props(
            "vt-1", "expr",
            evaluator: new FailingEvaluator("boom"),
            dependencyRefs: new[] { "a", "b" }));

        actor.Tell(new VirtualTagActor.DependencyValueChanged("a", 1, DateTime.UtcNow));
        // Still missing "b" — no publish yet.
        parent.ExpectNoMsg(TimeSpan.FromMilliseconds(200));

        // "b" arrives — all refs now seen, so the failure degrades to Bad.
        actor.Tell(new VirtualTagActor.DependencyValueChanged("b", 2, DateTime.UtcNow));
        parent.ExpectMsg<VirtualTagActor.EvaluationResult>().Quality.ShouldBe(OpcUaQuality.Bad);
    }

    /// <summary>Test evaluator returning a fixed sequence of results, one per Evaluate call; the last
    /// result repeats once the queue is drained.</summary>
    private sealed class QueuedEvaluator : IVirtualTagEvaluator
    {
        private readonly Queue<VirtualTagEvalResult> _results;
        private VirtualTagEvalResult _last;
        /// <summary>Initializes the evaluator with an ordered sequence of results.</summary>
        /// <param name="results">The results returned in order, one per call.</param>
        public QueuedEvaluator(params VirtualTagEvalResult[] results)
        {
            _results = new Queue<VirtualTagEvalResult>(results);
            _last = results[^1];
        }
        /// <summary>Returns the next queued result (or the last one once drained).</summary>
        /// <param name="id">The tag identifier.</param>
        /// <param name="expr">The expression string.</param>
        /// <param name="deps">The dependency values.</param>
        /// <returns>The next queued result.</returns>
        public VirtualTagEvalResult Evaluate(string id, string expr, IReadOnlyDictionary<string, object?> deps)
            => _results.Count > 0 ? _last = _results.Dequeue() : _last;
    }

    /// <summary>Test evaluator that throws on every call — exercises the actor's try/catch degradation
    /// path (distinct from a returned Failure).</summary>
    private sealed class ThrowingEvaluator : IVirtualTagEvaluator
    {
        /// <summary>Always throws.</summary>
        /// <param name="id">The tag identifier.</param>
        /// <param name="expr">The expression string.</param>
        /// <param name="deps">The dependency values.</param>
        /// <returns>Never returns; throws.</returns>
        public VirtualTagEvalResult Evaluate(string id, string expr, IReadOnlyDictionary<string, object?> deps)
            => throw new InvalidOperationException("kaboom");
    }

    /// <summary>Test evaluator that sums integer dependency values.</summary>
    private sealed class SumEvaluator : IVirtualTagEvaluator
    {
        /// <summary>Evaluates the expression by summing integer dependencies.</summary>
        /// <param name="id">The tag identifier.</param>
        /// <param name="expr">The expression string.</param>
        /// <param name="deps">The dependency values.</param>
        /// <returns>The sum of integer values in the dependencies.</returns>
        public VirtualTagEvalResult Evaluate(string id, string expr, IReadOnlyDictionary<string, object?> deps)
        {
            var sum = deps.Values.OfType<int>().Sum();
            return VirtualTagEvalResult.Ok(sum);
        }
    }

    /// <summary>Test evaluator that always returns a constant value.</summary>
    private sealed class ConstEvaluator : IVirtualTagEvaluator
    {
        private readonly object _value;
        /// <summary>Initializes the constant evaluator with a fixed value.</summary>
        /// <param name="value">The constant value to return.</param>
        public ConstEvaluator(object value) { _value = value; }
        /// <summary>Evaluates the expression by returning the constant value.</summary>
        /// <param name="id">The tag identifier.</param>
        /// <param name="expr">The expression string.</param>
        /// <param name="deps">The dependency values.</param>
        /// <returns>The constant value.</returns>
        public VirtualTagEvalResult Evaluate(string id, string expr, IReadOnlyDictionary<string, object?> deps)
            => VirtualTagEvalResult.Ok(_value);
    }

    /// <summary>Test evaluator that always returns a failure.</summary>
    private sealed class FailingEvaluator : IVirtualTagEvaluator
    {
        private readonly string _reason;
        /// <summary>Initializes the failing evaluator with a failure reason.</summary>
        /// <param name="reason">The reason for the failure.</param>
        public FailingEvaluator(string reason) { _reason = reason; }
        /// <summary>Evaluates the expression and returns a failure.</summary>
        /// <param name="id">The tag identifier.</param>
        /// <param name="expr">The expression string.</param>
        /// <param name="deps">The dependency values.</param>
        /// <returns>A failure result with the specified reason.</returns>
        public VirtualTagEvalResult Evaluate(string id, string expr, IReadOnlyDictionary<string, object?> deps)
            => VirtualTagEvalResult.Failure(_reason);
    }

    /// <summary>Test publisher that captures published messages.</summary>
    private sealed class CapturingPublisher
    {
        /// <summary>Gets the list of published topics.</summary>
        public ConcurrentBag<string> Topics { get; } = new();
        /// <summary>Gets the list of published payloads.</summary>
        public ConcurrentBag<object> Payloads { get; } = new();
        /// <summary>Publishes a message with a topic and payload.</summary>
        /// <param name="topic">The message topic.</param>
        /// <param name="payload">The message payload.</param>
        public void Publish(string topic, object payload)
        {
            Topics.Add(topic);
            Payloads.Add(payload);
        }
    }
}
