using Akka.Actor;
using Akka.TestKit.Xunit2;
using Microsoft.Extensions.Logging.Abstractions;
using ZB.MOM.WW.ScadaBridge.Commons.Types.Flattening;
using ZB.MOM.WW.ScadaBridge.SiteRuntime.Actors;
using ZB.MOM.WW.ScadaBridge.SiteRuntime.Persistence;
using ZB.MOM.WW.ScadaBridge.SiteRuntime.Scripts;
using ZB.MOM.WW.ScadaBridge.TestSupport;
using System.Text.Json;

namespace ZB.MOM.WW.ScadaBridge.SiteRuntime.Tests.Actors;

/// <summary>
/// PLAN-03 Task 18 (P2): the Instance Actor routes each attribute change only to the
/// children interested in it. A value-change / conditional script (and a monitored-attribute
/// alarm) subscribes to a single named attribute; an Expression-triggered script/alarm hears
/// every change (it keeps a full attribute snapshot). These tests pin the routing-map
/// construction directly — the behavioral gate inside each child stays as defense in depth.
/// </summary>
public class InstanceActorChildRoutingTests : TestKit, IDisposable
{
    private readonly SiteStorageService _storage;
    private readonly ScriptCompilationService _compilationService;
    private readonly SharedScriptLibrary _sharedScriptLibrary;
    private readonly SiteRuntimeOptions _options;
    private readonly TestLocalDb _localDb;

    public InstanceActorChildRoutingTests()
    {
        _localDb = TestLocalDb.CreateTemp("instance-routing-test");
        _storage = new SiteStorageService(
            _localDb.Db,
            NullLogger<SiteStorageService>.Instance);
        _storage.InitializeAsync().GetAwaiter().GetResult();
        _compilationService = new ScriptCompilationService(
            NullLogger<ScriptCompilationService>.Instance);
        _sharedScriptLibrary = new SharedScriptLibrary(
            _compilationService, NullLogger<SharedScriptLibrary>.Instance);
        _options = new SiteRuntimeOptions
        {
            MaxScriptCallDepth = 10,
            ScriptExecutionTimeoutSeconds = 30
        };
    }

    void IDisposable.Dispose()
    {
        // TestKit teardown first, so no in-flight actor can reach a disposed ILocalDb;
        // then dispose the database before deleting — the master connection anchors the WAL.
        Shutdown();
        var path = _localDb.Path;
        _localDb.Dispose();
        TestLocalDb.DeleteFiles(path);
    }

    private static FlattenedConfiguration ScriptsAB_and_Expression(string instanceName)
        => new()
        {
            InstanceUniqueName = instanceName,
            Attributes =
            [
                new ResolvedAttribute { CanonicalName = "A", Value = "1", DataType = "Double" },
                new ResolvedAttribute { CanonicalName = "B", Value = "2", DataType = "Double" }
            ],
            Scripts =
            [
                new ResolvedScript
                {
                    CanonicalName = "S_A", Code = "return 1;",
                    TriggerType = "ValueChange",
                    TriggerConfiguration = "{\"attributeName\":\"A\"}"
                },
                new ResolvedScript
                {
                    CanonicalName = "S_B", Code = "return 2;",
                    TriggerType = "ValueChange",
                    TriggerConfiguration = "{\"attributeName\":\"B\"}"
                },
                new ResolvedScript
                {
                    CanonicalName = "S_expr", Code = "return 3;",
                    TriggerType = "Expression",
                    TriggerConfiguration = "{\"expression\":\"true\"}"
                }
            ]
        };

    private InstanceActor BuildInstance(FlattenedConfiguration config)
    {
        var testRef = ActorOfAsTestActorRef<InstanceActor>(
            Props.Create(() => new InstanceActor(
                config.InstanceUniqueName,
                JsonSerializer.Serialize(config),
                _storage,
                _compilationService,
                _sharedScriptLibrary,
                null,
                _options,
                NullLogger<InstanceActor>.Instance)),
            "instance-" + Guid.NewGuid().ToString("N"));
        return testRef.UnderlyingActor;
    }

    [Fact]
    public void RoutingMaps_AreBuiltFromTriggerConfigs()
    {
        var actor = BuildInstance(ScriptsAB_and_Expression("RoutePump"));

        // Two value-change scripts route to their own attribute; the expression script
        // routes to the all-changes bucket.
        Assert.Single(actor.ChildrenByMonitoredAttribute["A"]);
        Assert.Single(actor.ChildrenByMonitoredAttribute["B"]);
        Assert.Single(actor.AllChangeChildren);
    }

    [Fact]
    public void MonitoredAttributeAlarm_RoutesToItsAttribute_ExpressionAlarm_RoutesToAllChanges()
    {
        var config = new FlattenedConfiguration
        {
            InstanceUniqueName = "AlarmRoutePump",
            Attributes =
            [
                new ResolvedAttribute { CanonicalName = "Temp", Value = "10", DataType = "Double" }
            ],
            Alarms =
            [
                new ResolvedAlarm
                {
                    CanonicalName = "HiTemp",
                    TriggerType = "RangeViolation",
                    TriggerConfiguration = "{\"attributeName\":\"Temp\",\"min\":0,\"max\":100}",
                    PriorityLevel = 1
                },
                new ResolvedAlarm
                {
                    CanonicalName = "ExprAlarm",
                    TriggerType = "Expression",
                    TriggerConfiguration = "{\"expression\":\"true\"}",
                    PriorityLevel = 1
                }
            ]
        };

        var actor = BuildInstance(config);

        Assert.Single(actor.ChildrenByMonitoredAttribute["Temp"]);
        Assert.Single(actor.AllChangeChildren); // the expression alarm
    }
}
