using System.Collections.Concurrent;
using Akka.Actor;
using Akka.TestKit;
using Shouldly;
using Xunit;
using ZB.MOM.WW.OtOpcUa.Commons.Engines;
using ZB.MOM.WW.OtOpcUa.Commons.Messages.Alerts;
using ZB.MOM.WW.OtOpcUa.Commons.Messages.Logging;
using ZB.MOM.WW.OtOpcUa.Runtime.ScriptedAlarms;
using ZB.MOM.WW.OtOpcUa.Runtime.Tests.Harness;
using ZB.MOM.WW.OtOpcUa.Runtime.VirtualTags;

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

public sealed class ScriptedAlarmActorTests : RuntimeActorTestBase
{
    /// <summary>Verifies that full state cycle publishes StateChanged messages to parent at each transition.</summary>
    [Fact]
    public void Full_state_cycle_publishes_StateChanged_to_parent_at_each_transition()
    {
        var parent = CreateTestProbe();
        var actor = parent.ChildActorOf(ScriptedAlarmActor.Props("alarm-1"));

        actor.Tell(new ScriptedAlarmActor.ConditionMet("threshold"));
        var t1 = parent.ExpectMsg<ScriptedAlarmActor.StateChanged>();
        t1.State.ShouldBe(ScriptedAlarmActorState.Active);

        actor.Tell(new ScriptedAlarmActor.AcknowledgeAlarm("joe"));
        var t2 = parent.ExpectMsg<ScriptedAlarmActor.StateChanged>();
        t2.State.ShouldBe(ScriptedAlarmActorState.Acknowledged);

        actor.Tell(new ScriptedAlarmActor.ConditionCleared());
        var t3 = parent.ExpectMsg<ScriptedAlarmActor.StateChanged>();
        t3.State.ShouldBe(ScriptedAlarmActorState.Inactive);
    }

    /// <summary>Verifies that duplicate ConditionMet messages in Active state are ignored.</summary>
    [Fact]
    public void Duplicate_ConditionMet_in_Active_is_ignored()
    {
        var parent = CreateTestProbe();
        var actor = parent.ChildActorOf(ScriptedAlarmActor.Props("alarm-1"));

        actor.Tell(new ScriptedAlarmActor.ConditionMet("first"));
        parent.ExpectMsg<ScriptedAlarmActor.StateChanged>();

        actor.Tell(new ScriptedAlarmActor.ConditionMet("second"));
        parent.ExpectNoMsg(TimeSpan.FromMilliseconds(200));
    }

    /// <summary>Verifies that active transition publishes AlarmTransitionEvent to the alerts topic.</summary>
    [Fact]
    public void Engine_active_transition_publishes_AlarmTransitionEvent_to_alerts_topic()
    {
        var capture = new CapturingPublisher();
        var parent = CreateTestProbe();
        var config = new ScriptedAlarmActor.AlarmConfig(
            AlarmId: "alarm-7",
            AlarmName: "High Temp",
            EquipmentPath: "/site-1/line-A/oven",
            Severity: 800,
            Predicate: "temp > 80");
        var actor = parent.ChildActorOf(ScriptedAlarmActor.Props(
            config,
            evaluator: new ThresholdEvaluator(80),
            publisherFactory: () => new DPSPublisher(capture.Publish)));

        actor.Tell(new ScriptedAlarmActor.DependencyValueChanged("temp", 92, DateTime.UtcNow));
        parent.ExpectMsg<ScriptedAlarmActor.StateChanged>().State.ShouldBe(ScriptedAlarmActorState.Active);

        AwaitAssert(() =>
        {
            var transitionEvt = capture.Payloads.OfType<AlarmTransitionEvent>().SingleOrDefault();
            transitionEvt.ShouldNotBeNull();
            transitionEvt.AlarmId.ShouldBe("alarm-7");
            transitionEvt.AlarmName.ShouldBe("High Temp");
            transitionEvt.EquipmentPath.ShouldBe("/site-1/line-A/oven");
            transitionEvt.Severity.ShouldBe(800);
            transitionEvt.TransitionKind.ShouldBe("Activated");
            transitionEvt.User.ShouldBe("system");

            var log = capture.Payloads.OfType<ScriptLogEntry>().SingleOrDefault();
            log.ShouldNotBeNull();
            log.AlarmId.ShouldBe("alarm-7");
        }, duration: TimeSpan.FromSeconds(1));
    }

    /// <summary>Verifies that clear transition publishes Cleared event.</summary>
    [Fact]
    public void Engine_clear_transition_publishes_Cleared_event()
    {
        var capture = new CapturingPublisher();
        var parent = CreateTestProbe();
        var config = new ScriptedAlarmActor.AlarmConfig("alarm-7", "High Temp", "/p", 500, "temp > 80");
        var evaluator = new ThresholdEvaluator(80);
        var actor = parent.ChildActorOf(ScriptedAlarmActor.Props(
            config, evaluator,
            publisherFactory: () => new DPSPublisher(capture.Publish)));

        actor.Tell(new ScriptedAlarmActor.DependencyValueChanged("temp", 92, DateTime.UtcNow));
        parent.ExpectMsg<ScriptedAlarmActor.StateChanged>();

        actor.Tell(new ScriptedAlarmActor.DependencyValueChanged("temp", 70, DateTime.UtcNow));
        parent.ExpectMsg<ScriptedAlarmActor.StateChanged>().State.ShouldBe(ScriptedAlarmActorState.Inactive);

        AwaitAssert(() =>
        {
            var kinds = capture.Payloads.OfType<AlarmTransitionEvent>().Select(e => e.TransitionKind).ToList();
            kinds.ShouldContain("Activated");
            kinds.ShouldContain("Cleared");
        }, duration: TimeSpan.FromSeconds(1));
    }

    /// <summary>Verifies that manual acknowledge emits Acknowledged transition with the user.</summary>
    [Fact]
    public void Manual_acknowledge_emits_Acknowledged_transition_with_user()
    {
        var capture = new CapturingPublisher();
        var parent = CreateTestProbe();
        var config = new ScriptedAlarmActor.AlarmConfig("a-1", "Pump Fail", "/eq", 700, Predicate: null);
        var actor = parent.ChildActorOf(ScriptedAlarmActor.Props(
            config, evaluator: null,
            publisherFactory: () => new DPSPublisher(capture.Publish)));

        actor.Tell(new ScriptedAlarmActor.ConditionMet("driver-fault"));
        parent.ExpectMsg<ScriptedAlarmActor.StateChanged>();

        actor.Tell(new ScriptedAlarmActor.AcknowledgeAlarm("operator-jane"));
        parent.ExpectMsg<ScriptedAlarmActor.StateChanged>().State.ShouldBe(ScriptedAlarmActorState.Acknowledged);

        AwaitAssert(() =>
        {
            var ackEvt = capture.Payloads.OfType<AlarmTransitionEvent>()
                .SingleOrDefault(e => e.TransitionKind == "Acknowledged");
            ackEvt.ShouldNotBeNull();
            ackEvt.User.ShouldBe("operator-jane");
        }, duration: TimeSpan.FromSeconds(1));
    }

    /// <summary>A threshold-based alarm evaluator for testing.</summary>
    private sealed class ThresholdEvaluator : IScriptedAlarmEvaluator
    {
        private readonly double _threshold;

        /// <summary>Initializes a new instance of the ThresholdEvaluator class.</summary>
        /// <param name="threshold">The threshold value to compare against.</param>
        public ThresholdEvaluator(double threshold) { _threshold = threshold; }

        /// <inheritdoc />
        public ScriptedAlarmEvalResult Evaluate(string id, string predicate, IReadOnlyDictionary<string, object?> deps)
        {
            if (!deps.TryGetValue("temp", out var raw) || raw is null)
                return ScriptedAlarmEvalResult.Failure("missing temp");
            return ScriptedAlarmEvalResult.Ok(Convert.ToDouble(raw) > _threshold);
        }
    }

    /// <summary>A test publisher that captures published messages.</summary>
    private sealed class CapturingPublisher
    {
        /// <summary>Gets the topics that messages were published to.</summary>
        public ConcurrentBag<string> Topics { get; } = new();

        /// <summary>Gets the payloads that were published.</summary>
        public ConcurrentBag<object> Payloads { get; } = new();

        /// <summary>Publishes a message to the specified topic.</summary>
        /// <param name="topic">The topic name.</param>
        /// <param name="payload">The message payload.</param>
        public void Publish(string topic, object payload)
        {
            Topics.Add(topic);
            Payloads.Add(payload);
        }
    }
}
