using Akka.Actor;
using Akka.TestKit.Xunit2;
using Microsoft.Extensions.Logging.Abstractions;
using ScadaLink.Commons.Messages.Streaming;
using ScadaLink.Commons.Types.Enums;
using ScadaLink.Commons.Types.Flattening;
using ScadaLink.SiteRuntime.Actors;
using ScadaLink.SiteRuntime.Scripts;

namespace ScadaLink.SiteRuntime.Tests.Actors;

/// <summary>
/// WP-16: Alarm Actor tests — value match, range violation, rate of change.
/// WP-21: Alarm on-trigger call direction tests.
/// </summary>
public class AlarmActorTests : TestKit, IDisposable
{
    private readonly SharedScriptLibrary _sharedLibrary;
    private readonly SiteRuntimeOptions _options;
    private readonly ScriptCompilationService _compilationService;

    public AlarmActorTests()
    {
        _compilationService = new ScriptCompilationService(
            NullLogger<ScriptCompilationService>.Instance);
        _sharedLibrary = new SharedScriptLibrary(
            _compilationService, NullLogger<SharedScriptLibrary>.Instance);
        _options = new SiteRuntimeOptions();
    }

    void IDisposable.Dispose()
    {
        Shutdown();
    }

    [Fact]
    public void AlarmActor_ValueMatch_ActivatesOnMatch()
    {
        var alarmConfig = new ResolvedAlarm
        {
            CanonicalName = "HighTemp",
            TriggerType = "ValueMatch",
            TriggerConfiguration = "{\"attributeName\":\"Status\",\"matchValue\":\"Critical\"}",
            PriorityLevel = 1
        };

        var instanceProbe = CreateTestProbe();
        var alarm = ActorOf(Props.Create(() => new AlarmActor(
            "HighTemp", "Pump1", instanceProbe.Ref, alarmConfig,
            null, _sharedLibrary, _options,
            NullLogger<AlarmActor>.Instance)));

        // Send value that matches
        alarm.Tell(new AttributeValueChanged(
            "Pump1", "Status", "Status", "Critical", "Good", DateTimeOffset.UtcNow));

        // Instance Actor should receive AlarmStateChanged
        var msg = instanceProbe.ExpectMsg<AlarmStateChanged>(TimeSpan.FromSeconds(5));
        Assert.Equal(AlarmState.Active, msg.State);
        Assert.Equal("HighTemp", msg.AlarmName);
    }

    [Fact]
    public void AlarmActor_ValueMatch_ClearsOnNonMatch()
    {
        var alarmConfig = new ResolvedAlarm
        {
            CanonicalName = "HighTemp",
            TriggerType = "ValueMatch",
            TriggerConfiguration = "{\"attributeName\":\"Status\",\"matchValue\":\"Critical\"}",
            PriorityLevel = 1
        };

        var instanceProbe = CreateTestProbe();
        var alarm = ActorOf(Props.Create(() => new AlarmActor(
            "HighTemp", "Pump1", instanceProbe.Ref, alarmConfig,
            null, _sharedLibrary, _options,
            NullLogger<AlarmActor>.Instance)));

        // Activate
        alarm.Tell(new AttributeValueChanged(
            "Pump1", "Status", "Status", "Critical", "Good", DateTimeOffset.UtcNow));
        var activateMsg = instanceProbe.ExpectMsg<AlarmStateChanged>(TimeSpan.FromSeconds(5));
        Assert.Equal(AlarmState.Active, activateMsg.State);

        // Clear
        alarm.Tell(new AttributeValueChanged(
            "Pump1", "Status", "Status", "Normal", "Good", DateTimeOffset.UtcNow));
        var clearMsg = instanceProbe.ExpectMsg<AlarmStateChanged>(TimeSpan.FromSeconds(5));
        Assert.Equal(AlarmState.Normal, clearMsg.State);
    }

    [Fact]
    public void AlarmActor_RangeViolation_ActivatesOutsideRange()
    {
        var alarmConfig = new ResolvedAlarm
        {
            CanonicalName = "TempRange",
            TriggerType = "RangeViolation",
            TriggerConfiguration = "{\"attributeName\":\"Temperature\",\"min\":0,\"max\":100}",
            PriorityLevel = 2
        };

        var instanceProbe = CreateTestProbe();
        var alarm = ActorOf(Props.Create(() => new AlarmActor(
            "TempRange", "Pump1", instanceProbe.Ref, alarmConfig,
            null, _sharedLibrary, _options,
            NullLogger<AlarmActor>.Instance)));

        // Value within range -- no alarm
        alarm.Tell(new AttributeValueChanged(
            "Pump1", "Temperature", "Temperature", "50", "Good", DateTimeOffset.UtcNow));
        instanceProbe.ExpectNoMsg(TimeSpan.FromMilliseconds(500));

        // Value outside range -- alarm activates
        alarm.Tell(new AttributeValueChanged(
            "Pump1", "Temperature", "Temperature", "150", "Good", DateTimeOffset.UtcNow));
        var msg = instanceProbe.ExpectMsg<AlarmStateChanged>(TimeSpan.FromSeconds(5));
        Assert.Equal(AlarmState.Active, msg.State);
    }

    [Fact]
    public void AlarmActor_RangeViolation_ClearsWhenBackInRange()
    {
        var alarmConfig = new ResolvedAlarm
        {
            CanonicalName = "TempRange",
            TriggerType = "RangeViolation",
            TriggerConfiguration = "{\"attributeName\":\"Temperature\",\"min\":0,\"max\":100}",
            PriorityLevel = 2
        };

        var instanceProbe = CreateTestProbe();
        var alarm = ActorOf(Props.Create(() => new AlarmActor(
            "TempRange", "Pump1", instanceProbe.Ref, alarmConfig,
            null, _sharedLibrary, _options,
            NullLogger<AlarmActor>.Instance)));

        // Activate
        alarm.Tell(new AttributeValueChanged(
            "Pump1", "Temperature", "Temperature", "150", "Good", DateTimeOffset.UtcNow));
        instanceProbe.ExpectMsg<AlarmStateChanged>(TimeSpan.FromSeconds(5));

        // Clear
        alarm.Tell(new AttributeValueChanged(
            "Pump1", "Temperature", "Temperature", "75", "Good", DateTimeOffset.UtcNow));
        var clearMsg = instanceProbe.ExpectMsg<AlarmStateChanged>(TimeSpan.FromSeconds(5));
        Assert.Equal(AlarmState.Normal, clearMsg.State);
    }

    [Fact]
    public void AlarmActor_IgnoresUnmonitoredAttributes()
    {
        var alarmConfig = new ResolvedAlarm
        {
            CanonicalName = "TempAlarm",
            TriggerType = "ValueMatch",
            TriggerConfiguration = "{\"attributeName\":\"Temperature\",\"matchValue\":\"100\"}",
            PriorityLevel = 1
        };

        var instanceProbe = CreateTestProbe();
        var alarm = ActorOf(Props.Create(() => new AlarmActor(
            "TempAlarm", "Pump1", instanceProbe.Ref, alarmConfig,
            null, _sharedLibrary, _options,
            NullLogger<AlarmActor>.Instance)));

        // Send change for a different attribute
        alarm.Tell(new AttributeValueChanged(
            "Pump1", "Pressure", "Pressure", "100", "Good", DateTimeOffset.UtcNow));

        instanceProbe.ExpectNoMsg(TimeSpan.FromMilliseconds(500));
    }

    [Fact]
    public void AlarmActor_DoesNotReTrigger_WhenAlreadyActive()
    {
        var alarmConfig = new ResolvedAlarm
        {
            CanonicalName = "TempAlarm",
            TriggerType = "ValueMatch",
            TriggerConfiguration = "{\"attributeName\":\"Status\",\"matchValue\":\"Bad\"}",
            PriorityLevel = 1
        };

        var instanceProbe = CreateTestProbe();
        var alarm = ActorOf(Props.Create(() => new AlarmActor(
            "TempAlarm", "Pump1", instanceProbe.Ref, alarmConfig,
            null, _sharedLibrary, _options,
            NullLogger<AlarmActor>.Instance)));

        // First trigger
        alarm.Tell(new AttributeValueChanged(
            "Pump1", "Status", "Status", "Bad", "Good", DateTimeOffset.UtcNow));
        instanceProbe.ExpectMsg<AlarmStateChanged>(TimeSpan.FromSeconds(5));

        // Second trigger with same value -- should NOT re-trigger
        alarm.Tell(new AttributeValueChanged(
            "Pump1", "Status", "Status", "Bad", "Good", DateTimeOffset.UtcNow));
        instanceProbe.ExpectNoMsg(TimeSpan.FromMilliseconds(500));
    }

    [Fact]
    public void AlarmActor_StartsNormal_OnRestart()
    {
        // Per design: on restart, alarm starts normal, re-evaluates from incoming values
        var alarmConfig = new ResolvedAlarm
        {
            CanonicalName = "RestartAlarm",
            TriggerType = "ValueMatch",
            TriggerConfiguration = "{\"attributeName\":\"Status\",\"matchValue\":\"Bad\"}",
            PriorityLevel = 1
        };

        var instanceProbe = CreateTestProbe();
        var alarm = ActorOf(Props.Create(() => new AlarmActor(
            "RestartAlarm", "Pump1", instanceProbe.Ref, alarmConfig,
            null, _sharedLibrary, _options,
            NullLogger<AlarmActor>.Instance)));

        // A "Good" value should not trigger since alarm starts Normal
        alarm.Tell(new AttributeValueChanged(
            "Pump1", "Status", "Status", "Good", "Good", DateTimeOffset.UtcNow));
        instanceProbe.ExpectNoMsg(TimeSpan.FromMilliseconds(500));
    }

    [Fact]
    public void AlarmActor_NoClearScript_OnDeactivation()
    {
        // WP-16: On clear, NO script is executed. Only on activate.
        var alarmConfig = new ResolvedAlarm
        {
            CanonicalName = "ClearTest",
            TriggerType = "ValueMatch",
            TriggerConfiguration = "{\"attributeName\":\"Status\",\"matchValue\":\"Bad\"}",
            PriorityLevel = 1
        };

        var instanceProbe = CreateTestProbe();
        var alarm = ActorOf(Props.Create(() => new AlarmActor(
            "ClearTest", "Pump1", instanceProbe.Ref, alarmConfig,
            null, // no on-trigger script
            _sharedLibrary, _options,
            NullLogger<AlarmActor>.Instance)));

        // Activate
        alarm.Tell(new AttributeValueChanged(
            "Pump1", "Status", "Status", "Bad", "Good", DateTimeOffset.UtcNow));
        instanceProbe.ExpectMsg<AlarmStateChanged>(TimeSpan.FromSeconds(5));

        // Clear -- should send state change but no script execution
        alarm.Tell(new AttributeValueChanged(
            "Pump1", "Status", "Status", "Good", "Good", DateTimeOffset.UtcNow));
        var clearMsg = instanceProbe.ExpectMsg<AlarmStateChanged>(TimeSpan.FromSeconds(5));
        Assert.Equal(AlarmState.Normal, clearMsg.State);

        // No additional messages (no script execution side effects)
        instanceProbe.ExpectNoMsg(TimeSpan.FromMilliseconds(500));
    }
}
