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.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: TimeSpan.FromMilliseconds(500));

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

    /// <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);
        }
    }
}
