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.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.Tests.TestSupport;

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

/// <summary>
/// Task 15: NativeAlarmActor mirrors a source's native alarms — subscribe on start,
/// emit enriched AlarmStateChanged, snapshot atomic swap, retention, persistence.
/// </summary>
public class NativeAlarmActorTests : TestKit, IDisposable
{
    private readonly string _dbFile;
    private readonly SiteStorageService _storage;
    private readonly SiteRuntimeOptions _options = new();

    public NativeAlarmActorTests()
    {
        _dbFile = Path.Combine(Path.GetTempPath(), $"naa-{Guid.NewGuid():N}.db");
        _storage = new SiteStorageService($"Data Source={_dbFile}", NullLogger<SiteStorageService>.Instance);
        _storage.InitializeAsync().GetAwaiter().GetResult();
    }

    private static ResolvedNativeAlarmSource Source() => new()
    {
        CanonicalName = "Pressure",
        ConnectionName = "Opc",
        SourceReference = "ns=2;s=T01"
    };

    private static NativeAlarmTransition Transition(
        string sourceRef, AlarmTransitionKind kind, AlarmConditionState condition, DateTimeOffset? time = null) =>
        new(sourceRef, "T01", "AnalogLimit.Hi", kind, condition,
            "Process", "hi", "hi", "", "", null, time ?? DateTimeOffset.UtcNow, "92", "90");

    private IActorRef Spawn(IActorRef instanceActor, IActorRef dclManager, IServiceProvider? serviceProvider = null) =>
        ActorOf(Props.Create(() => new NativeAlarmActor(
            Source(), "inst", instanceActor, dclManager, _storage, _options,
            NullLogger<NativeAlarmActor>.Instance, AlarmKind.NativeOpcUa, serviceProvider)));

    [Fact]
    public void SubscribeOnStart_SendsRequestForSourceBinding()
    {
        var dcl = CreateTestProbe();
        Spawn(CreateTestProbe().Ref, 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 Raise_EmitsEnrichedAlarmStateChanged()
    {
        var instance = CreateTestProbe();
        var dcl = CreateTestProbe();
        var actor = Spawn(instance.Ref, dcl.Ref);
        dcl.ExpectMsg<SubscribeAlarmsRequest>();

        actor.Tell(new NativeAlarmTransitionUpdate("Opc", Transition(
            "T01.Hi", AlarmTransitionKind.Raise,
            new AlarmConditionState(true, false, null, AlarmShelveState.Unshelved, false, 800))));

        var emitted = instance.ExpectMsg<AlarmStateChanged>();
        Assert.Equal(AlarmKind.NativeOpcUa, emitted.Kind);
        Assert.Equal("T01.Hi", emitted.SourceReference);
        Assert.Equal(AlarmState.Active, emitted.State);
        Assert.Equal(800, emitted.Condition.Severity);
    }

    [Fact]
    public void SnapshotComplete_WithMissingPriorAlarm_EmitsReturnToNormal()
    {
        var instance = CreateTestProbe();
        var dcl = CreateTestProbe();
        var actor = Spawn(instance.Ref, dcl.Ref);
        dcl.ExpectMsg<SubscribeAlarmsRequest>();

        actor.Tell(new NativeAlarmTransitionUpdate("Opc", Transition(
            "T01.Hi", AlarmTransitionKind.Raise,
            new AlarmConditionState(true, false, null, AlarmShelveState.Unshelved, false, 800))));
        instance.ExpectMsg<AlarmStateChanged>(m => m.State == AlarmState.Active);

        // A fresh snapshot that no longer contains T01.Hi means it cleared at the source.
        actor.Tell(new NativeAlarmTransitionUpdate("Opc", Transition(
            "T01.Hi", AlarmTransitionKind.SnapshotComplete,
            new AlarmConditionState(true, false, null, AlarmShelveState.Unshelved, false, 800))));

        var cleared = instance.ExpectMsg<AlarmStateChanged>();
        Assert.Equal("T01.Hi", cleared.SourceReference);
        Assert.Equal(AlarmState.Normal, cleared.State);
    }

    [Fact]
    public void OlderTransition_IsIgnored()
    {
        var instance = CreateTestProbe();
        var dcl = CreateTestProbe();
        var actor = Spawn(instance.Ref, dcl.Ref);
        dcl.ExpectMsg<SubscribeAlarmsRequest>();

        var t0 = DateTimeOffset.UtcNow;
        actor.Tell(new NativeAlarmTransitionUpdate("Opc", Transition(
            "T01.Hi", AlarmTransitionKind.Raise,
            new AlarmConditionState(true, false, null, AlarmShelveState.Unshelved, false, 800), t0)));
        instance.ExpectMsg<AlarmStateChanged>(m => m.State == AlarmState.Active);

        // An out-of-order (older) transition must not overwrite newer state.
        actor.Tell(new NativeAlarmTransitionUpdate("Opc", Transition(
            "T01.Hi", AlarmTransitionKind.Clear,
            new AlarmConditionState(false, true, null, AlarmShelveState.Unshelved, false, 0), t0.AddSeconds(-30))));

        instance.ExpectNoMsg(TimeSpan.FromMilliseconds(300));
    }

    // ── M1.5: site event log `alarm` category ──────────────────────────────

    [Fact]
    public void Raise_EmitsAlarmSiteEvent()
    {
        var siteLog = new FakeSiteEventLogger();
        var instance = CreateTestProbe();
        var dcl = CreateTestProbe();
        var actor = Spawn(instance.Ref, dcl.Ref, new SingleServiceProvider(siteLog));
        dcl.ExpectMsg<SubscribeAlarmsRequest>();

        actor.Tell(new NativeAlarmTransitionUpdate("Opc", Transition(
            "T01.Hi", AlarmTransitionKind.Raise,
            new AlarmConditionState(true, false, null, AlarmShelveState.Unshelved, false, 800))));
        instance.ExpectMsg<AlarmStateChanged>(m => m.State == AlarmState.Active);

        AwaitAssert(() =>
        {
            var rows = siteLog.OfType("alarm");
            Assert.Single(rows);
            var row = rows[0];
            Assert.Equal("Error", row.Severity); // severity 800 → Error
            Assert.Equal("inst", row.InstanceId);
            Assert.Equal("NativeAlarmActor:Pressure", row.Source);
        }, TimeSpan.FromSeconds(2));
    }

    [Fact]
    public void Clear_EmitsInfoAlarmSiteEvent()
    {
        var siteLog = new FakeSiteEventLogger();
        var instance = CreateTestProbe();
        var dcl = CreateTestProbe();
        var actor = Spawn(instance.Ref, dcl.Ref, new SingleServiceProvider(siteLog));
        dcl.ExpectMsg<SubscribeAlarmsRequest>();

        var t0 = DateTimeOffset.UtcNow;
        actor.Tell(new NativeAlarmTransitionUpdate("Opc", Transition(
            "T01.Hi", AlarmTransitionKind.Raise,
            new AlarmConditionState(true, false, null, AlarmShelveState.Unshelved, false, 800), t0)));
        instance.ExpectMsg<AlarmStateChanged>(m => m.State == AlarmState.Active);

        // Clear (inactive but not yet acked → stays mirrored, return-to-normal emit).
        actor.Tell(new NativeAlarmTransitionUpdate("Opc", Transition(
            "T01.Hi", AlarmTransitionKind.Clear,
            new AlarmConditionState(false, false, null, AlarmShelveState.Unshelved, false, 0), t0.AddSeconds(5))));
        instance.ExpectMsg<AlarmStateChanged>(m => m.State == AlarmState.Normal);

        AwaitAssert(() =>
        {
            var rows = siteLog.OfType("alarm");
            Assert.Equal(2, rows.Count); // raise + clear
            Assert.Equal("Error", rows[0].Severity);
            Assert.Equal("Info", rows[1].Severity); // return-to-normal → Info
        }, TimeSpan.FromSeconds(2));
    }

    [Fact]
    public async Task Rehydration_DoesNotEmitSiteEvent()
    {
        // Pre-populate SQLite with an active condition so the actor rehydrates
        // it on PreStart. Rehydration replays last-known state — it is NOT a
        // live transition, so it must surface upward (for the DebugView) but
        // must NOT re-log an `alarm` operational event.
        var condition = new AlarmConditionState(true, false, null, AlarmShelveState.Unshelved, false, 800);
        await _storage.UpsertNativeAlarmAsync(
            "inst", "Pressure", "T01.Hi",
            JsonSerializer.Serialize(condition), DateTimeOffset.UtcNow);

        var siteLog = new FakeSiteEventLogger();
        var instance = CreateTestProbe();
        var dcl = CreateTestProbe();
        Spawn(instance.Ref, dcl.Ref, new SingleServiceProvider(siteLog));

        // The rehydrated condition is surfaced upward...
        var emitted = instance.ExpectMsg<AlarmStateChanged>(TimeSpan.FromSeconds(2));
        Assert.Equal("T01.Hi", emitted.SourceReference);
        Assert.Equal(AlarmState.Active, emitted.State);
        dcl.ExpectMsg<SubscribeAlarmsRequest>();

        // ...but no `alarm` operational event is logged for it.
        AwaitAssert(
            () => Assert.Empty(siteLog.OfType("alarm")),
            TimeSpan.FromSeconds(1));
    }

    [Fact]
    public void SnapshotSwap_ExistingActiveCondition_DoesNotReEmit()
    {
        var siteLog = new FakeSiteEventLogger();
        var instance = CreateTestProbe();
        var dcl = CreateTestProbe();
        var actor = Spawn(instance.Ref, dcl.Ref, new SingleServiceProvider(siteLog));
        dcl.ExpectMsg<SubscribeAlarmsRequest>();

        // Live raise — the one and only `alarm` event we expect.
        actor.Tell(new NativeAlarmTransitionUpdate("Opc", Transition(
            "T01.Hi", AlarmTransitionKind.Raise,
            new AlarmConditionState(true, false, null, AlarmShelveState.Unshelved, false, 800))));
        instance.ExpectMsg<AlarmStateChanged>(m => m.State == AlarmState.Active);
        AwaitAssert(() => Assert.Single(siteLog.OfType("alarm")), TimeSpan.FromSeconds(2));

        // A reconnect snapshot that RE-INCLUDES the same still-active condition is
        // a re-sync, not a live transition. It must NOT re-log a second `alarm`
        // event (regression for the spurious-reconnect-event bug).
        actor.Tell(new NativeAlarmTransitionUpdate("Opc", Transition(
            "T01.Hi", AlarmTransitionKind.Snapshot,
            new AlarmConditionState(true, false, null, AlarmShelveState.Unshelved, false, 800))));
        actor.Tell(new NativeAlarmTransitionUpdate("Opc", Transition(
            "T01.Hi", AlarmTransitionKind.SnapshotComplete,
            new AlarmConditionState(true, false, null, AlarmShelveState.Unshelved, false, 800))));

        // The snapshot still surfaces the condition upward (DebugView re-sync)...
        instance.ExpectMsg<AlarmStateChanged>(m => m.SourceReference == "T01.Hi" && m.State == AlarmState.Active);

        // ...but the `alarm` event count stays at exactly 1 — no re-emit.
        Thread.Sleep(200); // give any spurious fire-and-forget log time to land
        Assert.Single(siteLog.OfType("alarm"));
    }

    [Fact]
    public void Acknowledge_EmitsInfoAlarmSiteEventMentioningAcknowledged()
    {
        var siteLog = new FakeSiteEventLogger();
        var instance = CreateTestProbe();
        var dcl = CreateTestProbe();
        var actor = Spawn(instance.Ref, dcl.Ref, new SingleServiceProvider(siteLog));
        dcl.ExpectMsg<SubscribeAlarmsRequest>();

        var t0 = DateTimeOffset.UtcNow;
        actor.Tell(new NativeAlarmTransitionUpdate("Opc", Transition(
            "T01.Hi", AlarmTransitionKind.Raise,
            new AlarmConditionState(true, false, null, AlarmShelveState.Unshelved, false, 800), t0)));
        instance.ExpectMsg<AlarmStateChanged>(m => m.State == AlarmState.Active);

        // Operator acknowledges the still-active condition. The Acknowledge
        // branch of LogAlarmEvent logs Info and mentions "acknowledged".
        actor.Tell(new NativeAlarmTransitionUpdate("Opc", Transition(
            "T01.Hi", AlarmTransitionKind.Acknowledge,
            new AlarmConditionState(true, true, null, AlarmShelveState.Unshelved, false, 800), t0.AddSeconds(5))));
        instance.ExpectMsg<AlarmStateChanged>();

        AwaitAssert(() =>
        {
            var rows = siteLog.OfType("alarm");
            Assert.Equal(2, rows.Count); // raise + acknowledge
            var ack = rows[1];
            Assert.Equal("Info", ack.Severity);
            Assert.Contains("acknowledged", ack.Message, StringComparison.OrdinalIgnoreCase);
        }, TimeSpan.FromSeconds(2));
    }

    void IDisposable.Dispose()
    {
        Shutdown();
        if (File.Exists(_dbFile))
        {
            File.Delete(_dbFile);
        }
    }
}
