using System.Collections.Concurrent;
using Akka.Actor;
using Microsoft.Extensions.Logging.Abstractions;
using Shouldly;
using Xunit;
using ZB.MOM.WW.OtOpcUa.Commons.Engines;
using ZB.MOM.WW.OtOpcUa.Configuration.Entities;
using ZB.MOM.WW.OtOpcUa.Runtime.ScriptedAlarms;
using ZB.MOM.WW.OtOpcUa.Runtime.Tests.Harness;

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

public sealed class ScriptedAlarmStatePersistenceTests : RuntimeActorTestBase
{
    /// <summary>Verifies that alarm state transitions write to the state store with the correct lastAckUser value.</summary>
    [Fact]
    public async Task Transition_writes_to_state_store_with_lastAckUser()
    {
        var store = new RecordingStateStore();
        var parent = CreateTestProbe();
        var config = new ScriptedAlarmActor.AlarmConfig("a-1", "Pump", "/eq", 700, Predicate: null);
        var actor = parent.ChildActorOf(ScriptedAlarmActor.Props(config, stateStore: store));

        actor.Tell(new ScriptedAlarmActor.ConditionMet("threshold"));
        parent.ExpectMsg<ScriptedAlarmActor.StateChanged>();
        AwaitAssert(() =>
        {
            store.Snapshots.Last().State.ShouldBe("Active");
            store.Snapshots.Last().LastAckUser.ShouldBeNull();
        }, duration: TimeSpan.FromSeconds(2));

        actor.Tell(new ScriptedAlarmActor.AcknowledgeAlarm("operator-jane"));
        parent.ExpectMsg<ScriptedAlarmActor.StateChanged>();
        AwaitAssert(() =>
        {
            var ackedSnap = store.Snapshots.Last(s => s.State == "Acknowledged");
            ackedSnap.LastAckUser.ShouldBe("operator-jane");
        }, duration: TimeSpan.FromSeconds(2));
    }

    /// <summary>Verifies that actor restart restores persisted state so pending acknowledgment is not dropped.</summary>
    [Fact]
    public async Task PreStart_restores_persisted_state_so_restart_does_not_drop_pending_ack()
    {
        var store = new RecordingStateStore();
        await store.SaveAsync(new AlarmActorStateSnapshot(
            AlarmId: "a-1",
            State: "Active",
            LastTransitionUtc: DateTime.UtcNow.AddMinutes(-5),
            LastAckUser: null), CancellationToken.None);

        var parent = CreateTestProbe();
        var config = new ScriptedAlarmActor.AlarmConfig("a-1", "Pump", "/eq", 700, Predicate: null);
        var actor = parent.ChildActorOf(ScriptedAlarmActor.Props(config, stateStore: store));

        // After PreStart's async load, the actor should be in Active — duplicate ConditionMet
        // is then ignored because the existing Active-state check.
        AwaitAssert(() =>
        {
            actor.Tell(new ScriptedAlarmActor.AcknowledgeAlarm("operator-bob"));
            parent.ExpectMsg<ScriptedAlarmActor.StateChanged>(TimeSpan.FromMilliseconds(500))
                .State.ShouldBe(ScriptedAlarmActorState.Acknowledged);
        }, duration: TimeSpan.FromSeconds(3));
    }

    /// <summary>Verifies that alarm boots to inactive state when no persisted state exists.</summary>
    [Fact]
    public async Task PreStart_with_no_persisted_state_boots_inactive()
    {
        var store = new RecordingStateStore();
        var parent = CreateTestProbe();
        var config = new ScriptedAlarmActor.AlarmConfig("never-seen", "X", "/eq", 500, Predicate: null);
        var actor = parent.ChildActorOf(ScriptedAlarmActor.Props(config, stateStore: store));

        // Empty store ⇒ actor sits Inactive; AcknowledgeAlarm is ignored from Inactive so no
        // StateChanged should arrive.
        await Task.Delay(200);
        actor.Tell(new ScriptedAlarmActor.AcknowledgeAlarm("anyone"));
        parent.ExpectNoMsg(TimeSpan.FromMilliseconds(300));
    }

    /// <summary>Verifies that EF-based alarm actor state store correctly persists and restores state through the config database.</summary>
    [Fact]
    public async Task EfAlarmActorStateStore_round_trip_persists_via_ConfigDb()
    {
        var db = NewInMemoryDbFactory();
        var ef = new EfAlarmActorStateStore(db, NullLogger<EfAlarmActorStateStore>.Instance);

        await ef.SaveAsync(new AlarmActorStateSnapshot(
            AlarmId: "alarm-7",
            State: "Active",
            LastTransitionUtc: DateTime.UtcNow,
            LastAckUser: null), CancellationToken.None);

        using (var ctx = db.CreateDbContext())
        {
            var row = ctx.ScriptedAlarmStates.Single(r => r.ScriptedAlarmId == "alarm-7");
            row.AckedState.ShouldBe("Unacknowledged");
        }

        // Acknowledge — same alarmId, transitions to Acknowledged.
        await ef.SaveAsync(new AlarmActorStateSnapshot(
            AlarmId: "alarm-7",
            State: "Acknowledged",
            LastTransitionUtc: DateTime.UtcNow,
            LastAckUser: "jane"), CancellationToken.None);

        var loaded = await ef.LoadAsync("alarm-7", CancellationToken.None);
        loaded.ShouldNotBeNull();
        loaded.State.ShouldBe("Acknowledged");
        loaded.LastAckUser.ShouldBe("jane");

        using (var ctx = db.CreateDbContext())
        {
            ctx.ScriptedAlarmStates.Count(r => r.ScriptedAlarmId == "alarm-7").ShouldBe(1);
            ctx.ScriptedAlarmStates.Single(r => r.ScriptedAlarmId == "alarm-7").LastAckUser.ShouldBe("jane");
        }
    }

    /// <summary>Verifies that loading an alarm state for a missing ID returns null.</summary>
    [Fact]
    public async Task EfAlarmActorStateStore_load_for_missing_id_returns_null()
    {
        var db = NewInMemoryDbFactory();
        var ef = new EfAlarmActorStateStore(db, NullLogger<EfAlarmActorStateStore>.Instance);

        var loaded = await ef.LoadAsync("never-saved", CancellationToken.None);
        loaded.ShouldBeNull();
    }

    private sealed class RecordingStateStore : IAlarmActorStateStore
    {
        private readonly ConcurrentDictionary<string, AlarmActorStateSnapshot> _byId = new(StringComparer.Ordinal);
        private readonly ConcurrentQueue<AlarmActorStateSnapshot> _saves = new();

        /// <summary>Gets all saved alarm state snapshots in order.</summary>
        public List<AlarmActorStateSnapshot> Snapshots => _saves.ToList();

        /// <summary>Loads the alarm state snapshot for the specified alarm ID.</summary>
        /// <param name="alarmId">The alarm ID.</param>
        /// <param name="ct">The cancellation token.</param>
        /// <returns>The alarm state snapshot if found, null otherwise.</returns>
        public Task<AlarmActorStateSnapshot?> LoadAsync(string alarmId, CancellationToken ct)
            => Task.FromResult(_byId.TryGetValue(alarmId, out var v) ? v : null);

        /// <summary>Saves the alarm state snapshot.</summary>
        /// <param name="snapshot">The alarm state snapshot to save.</param>
        /// <param name="ct">The cancellation token.</param>
        /// <returns>A completed task.</returns>
        public Task SaveAsync(AlarmActorStateSnapshot snapshot, CancellationToken ct)
        {
            _byId[snapshot.AlarmId] = snapshot;
            _saves.Enqueue(snapshot);
            return Task.CompletedTask;
        }
    }
}
