using System.Text.Json;
using Akka.Actor;
using Akka.TestKit.Xunit2;
using Microsoft.Extensions.Logging.Abstractions;
using ZB.MOM.WW.ScadaBridge.Commons.Messages.DataConnection;
using ZB.MOM.WW.ScadaBridge.Commons.Messages.DebugView;
using ZB.MOM.WW.ScadaBridge.Commons.Messages.Instance;
using ZB.MOM.WW.ScadaBridge.Commons.Messages.Streaming;
using ZB.MOM.WW.ScadaBridge.Commons.Types.Alarms;
using ZB.MOM.WW.ScadaBridge.Commons.Types.Enums;
using ZB.MOM.WW.ScadaBridge.Commons.Types.Flattening;
using ZB.MOM.WW.ScadaBridge.SiteRuntime;
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;

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

/// <summary>
/// Task 16: InstanceActor spawns NativeAlarmActor children from the flattened
/// configuration and surfaces enriched native alarm state in the DebugView snapshot.
/// </summary>
public class InstanceActorNativeAlarmTests : TestKit, IDisposable
{
    private readonly SiteStorageService _storage;
    private readonly ScriptCompilationService _compilationService;
    private readonly SharedScriptLibrary _sharedScriptLibrary;
    private readonly SiteRuntimeOptions _options = new();
    private readonly TestLocalDb _localDb;

    public InstanceActorNativeAlarmTests()
    {
        _localDb = TestLocalDb.CreateTemp("instance-native");
        _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);
    }

    private IActorRef CreateInstanceActorWithDcl(string instanceName, FlattenedConfiguration config, IActorRef dclManager) =>
        ActorOf(Props.Create(() => new InstanceActor(
            instanceName,
            JsonSerializer.Serialize(config),
            _storage,
            _compilationService,
            _sharedScriptLibrary,
            null,
            _options,
            NullLogger<InstanceActor>.Instance,
            dclManager)));

    private static FlattenedConfiguration ConfigWithNativeSource(string instanceName) => new()
    {
        InstanceUniqueName = instanceName,
        NativeAlarmSources =
        [
            new ResolvedNativeAlarmSource
            {
                CanonicalName = "Pressure",
                ConnectionName = "Opc",
                SourceReference = "ns=2;s=T01"
            }
        ]
    };

    [Fact]
    public void SpawnsNativeAlarmActor_WhichSubscribesViaDcl()
    {
        var dcl = CreateTestProbe();
        CreateInstanceActorWithDcl("inst", ConfigWithNativeSource("inst"), dcl.Ref);

        var req = dcl.ExpectMsg<SubscribeAlarmsRequest>();
        Assert.Equal("inst", req.InstanceUniqueName);
        Assert.Equal("Opc", req.ConnectionName);
        Assert.Equal("ns=2;s=T01", req.SourceReference);
    }

    [Fact]
    public void DebugViewSnapshot_IncludesNativeAlarm_AfterTransition()
    {
        var dcl = CreateTestProbe();
        var actor = CreateInstanceActorWithDcl("inst", ConfigWithNativeSource("inst"), dcl.Ref);

        // Simulate the NativeAlarmActor emitting an enriched event upward.
        actor.Tell(new AlarmStateChanged("inst", "T01.Hi", AlarmState.Active, 800, DateTimeOffset.UtcNow)
        {
            Kind = AlarmKind.NativeOpcUa,
            SourceReference = "T01.Hi",
            Condition = new AlarmConditionState(true, false, null, AlarmShelveState.Unshelved, false, 800)
        });

        actor.Tell(new SubscribeDebugViewRequest("inst", "c"));
        var snap = ExpectMsg<DebugViewSnapshot>();

        Assert.Contains(snap.AlarmStates, a =>
            a.SourceReference == "T01.Hi" && a.Kind == AlarmKind.NativeOpcUa && a.Condition.Severity == 800);
    }

    [Fact]
    public void Snapshot_QuietNativeBinding_EmitsPlaceholder()
    {
        var dcl = CreateTestProbe();
        var actor = CreateInstanceActorWithDcl("inst", ConfigWithNativeSource("inst"), dcl.Ref);
        dcl.ExpectMsg<SubscribeAlarmsRequest>();

        // No live condition is emitted: the configured "Pressure" binding is quiet.
        actor.Tell(new SubscribeDebugViewRequest("inst", "c"));
        var snap = ExpectMsg<DebugViewSnapshot>();

        var placeholders = snap.AlarmStates
            .Where(a => a.NativeSourceCanonicalName == "Pressure" && a.IsConfiguredPlaceholder)
            .ToList();

        Assert.Single(placeholders);
        var placeholder = placeholders[0];
        Assert.Equal(AlarmState.Normal, placeholder.State);
        Assert.Equal("Pressure", placeholder.NativeSourceCanonicalName);
        Assert.Equal(AlarmKind.NativeOpcUa, placeholder.Kind);
    }

    [Fact]
    public void Snapshot_NativeBindingWithLiveCondition_NoPlaceholder()
    {
        var dcl = CreateTestProbe();
        var actor = CreateInstanceActorWithDcl("inst", ConfigWithNativeSource("inst"), dcl.Ref);
        dcl.ExpectMsg<SubscribeAlarmsRequest>();

        // The NativeAlarmActor emits a live condition stamped with the binding's
        // canonical name (DV-1: NativeSourceCanonicalName), so the binding is "active".
        actor.Tell(new AlarmStateChanged("inst", "Pressure.Hi", AlarmState.Active, 800, DateTimeOffset.UtcNow)
        {
            Kind = AlarmKind.NativeOpcUa,
            SourceReference = "ns=2;s=T01.Hi",
            NativeSourceCanonicalName = "Pressure",
            Condition = new AlarmConditionState(true, false, null, AlarmShelveState.Unshelved, false, 800)
        });

        actor.Tell(new SubscribeDebugViewRequest("inst", "c"));
        var snap = ExpectMsg<DebugViewSnapshot>();

        // The live condition is present...
        Assert.Contains(snap.AlarmStates, a =>
            a.SourceReference == "ns=2;s=T01.Hi" && a.NativeSourceCanonicalName == "Pressure"
            && a.Kind == AlarmKind.NativeOpcUa && a.Condition.Severity == 800);

        // ...and NO placeholder row is emitted for that binding.
        Assert.DoesNotContain(snap.AlarmStates, a =>
            a.NativeSourceCanonicalName == "Pressure" && a.IsConfiguredPlaceholder);
    }

    // ── MES alarm-status API §5.2: the script-facing alarm snapshot ────────

    [Fact]
    public void GetAlarmSnapshot_ReturnsTheSameRowsAsTheDebugViewSnapshot()
    {
        // Alarms.CurrentAsync() must not diverge from what the operator sees in the Debug
        // View — both are served from BuildAlarmStatesSnapshot, so a live native condition
        // AND the quiet-binding placeholder appear in each.
        var dcl = CreateTestProbe();
        var actor = CreateInstanceActorWithDcl("inst", ConfigWithNativeSource("inst"), dcl.Ref);
        dcl.ExpectMsg<SubscribeAlarmsRequest>();

        var ackedAt = new DateTimeOffset(2026, 8, 1, 12, 0, 0, TimeSpan.Zero);
        actor.Tell(new AlarmStateChanged("inst", "T01.Hi", AlarmState.Active, 900, DateTimeOffset.UtcNow)
        {
            Kind = AlarmKind.NativeOpcUa,
            SourceReference = "T01.Hi",
            NativeSourceCanonicalName = "Pressure",
            Condition = new AlarmConditionState(true, true, null, AlarmShelveState.Unshelved, false, 900),
            AckTime = ackedAt
        });

        actor.Tell(new GetAlarmSnapshotRequest("c1", "inst", DateTimeOffset.UtcNow));
        var reply = ExpectMsg<GetAlarmSnapshotResponse>();

        Assert.Equal("c1", reply.CorrelationId);
        Assert.Equal("inst", reply.InstanceUniqueName);

        var live = Assert.Single(reply.Alarms, a => a.SourceReference == "T01.Hi");
        Assert.Equal(AlarmState.Active, live.State);
        Assert.Equal(900, live.Condition.Severity);
        // §6.4: the ack instant reaches the script accessor, not just the gRPC stream.
        Assert.Equal(ackedAt, live.AckTime);

        actor.Tell(new SubscribeDebugViewRequest("inst", "c"));
        var debug = ExpectMsg<DebugViewSnapshot>();
        Assert.Equal(
            debug.AlarmStates.Select(a => a.AlarmName).OrderBy(n => n),
            reply.Alarms.Select(a => a.AlarmName).OrderBy(n => n));
    }

    [Fact]
    public void GetAlarmSnapshot_WithNoAlarms_RepliesWithTheConfiguredPlaceholdersOnly()
    {
        var dcl = CreateTestProbe();
        var actor = CreateInstanceActorWithDcl("inst", ConfigWithNativeSource("inst"), dcl.Ref);
        dcl.ExpectMsg<SubscribeAlarmsRequest>();

        actor.Tell(new GetAlarmSnapshotRequest("c2", "inst", DateTimeOffset.UtcNow));
        var reply = ExpectMsg<GetAlarmSnapshotResponse>();

        var placeholder = Assert.Single(reply.Alarms);
        Assert.True(placeholder.IsConfiguredPlaceholder);
        Assert.Equal("Pressure", placeholder.NativeSourceCanonicalName);
        Assert.Null(placeholder.AckTime);
    }

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