using Akka.Actor;
using Akka.TestKit.Xunit2;
using Microsoft.Extensions.Logging.Abstractions;
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.SiteRuntime.Scripts;

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

/// <summary>
/// MES alarm-status API §5.2: the script-facing <c>Alarms.CurrentAsync()</c> accessor.
/// Routes a real <see cref="ScriptRuntimeContext"/> against a TestProbe standing in for the
/// Instance Actor (the same harness shape <c>AttributeAccessorWaitAsyncTests</c> uses), so
/// the Ask contract and the <see cref="AlarmStateChanged"/> → <c>ScriptAlarm</c> projection
/// are both exercised for real.
/// </summary>
public class AlarmsAccessorTests : TestKit, IDisposable
{
    private ScriptRuntimeContext MakeContext(IActorRef instanceActor) =>
        new(
            instanceActor,
            sharedScriptLibrary: null!,
            currentCallDepth: 0,
            maxCallDepth: 10,
            askTimeout: TimeSpan.FromSeconds(5),
            instanceName: "CvdReactor01",
            logger: NullLogger<ScriptRuntimeContext>.Instance);

    void IDisposable.Dispose() => Shutdown();

    private static readonly DateTimeOffset RaisedAt = new(2026, 8, 1, 10, 0, 0, TimeSpan.Zero);
    private static readonly DateTimeOffset AckedAt = new(2026, 8, 1, 10, 5, 0, TimeSpan.Zero);
    private static readonly DateTimeOffset ObservedAt = new(2026, 8, 1, 10, 6, 0, TimeSpan.Zero);

    /// <summary>An acknowledged native condition mirrored from a source binding.</summary>
    private static AlarmStateChanged NativeAcked() =>
        new("CvdReactor01", "Z28061.HeartbeatTimeoutAlarm", AlarmState.Active, 950, ObservedAt)
        {
            Kind = AlarmKind.NativeMxAccess,
            Condition = new AlarmConditionState(
                Active: true, Acknowledged: true, Confirmed: null,
                Shelve: AlarmShelveState.TimedShelved, Suppressed: true, Severity: 950),
            SourceReference = "Z28061.HeartbeatTimeoutAlarm",
            NativeSourceCanonicalName = "ReactorAlarms",
            AlarmTypeName = "DiscreteAlarm",
            Category = "Process",
            Message = "Heartbeat lost",
            OperatorUser = "op1",
            OperatorComment = "investigating",
            OriginalRaiseTime = RaisedAt,
            AckTime = AckedAt,
            CurrentValue = "FAULT",
            LimitValue = "0",
        };

    /// <summary>Replies to the accessor's Ask with the supplied alarm rows.</summary>
    private void RespondWith(Akka.TestKit.TestProbe probe, params AlarmStateChanged[] alarms)
    {
        var request = probe.ExpectMsg<GetAlarmSnapshotRequest>(TimeSpan.FromSeconds(5));
        probe.Reply(new GetAlarmSnapshotResponse(
            request.CorrelationId, request.InstanceUniqueName, alarms, DateTimeOffset.UtcNow));
    }

    [Fact]
    public async Task CurrentAsync_AsksTheInstanceActor_ForItsOwnInstance()
    {
        // The script runs inside its own instance's context, so the read is a LOCAL Ask
        // stamped with that instance's name — never a cross-instance or cross-cluster hop.
        var probe = CreateTestProbe();
        var ctx = MakeContext(probe.Ref);

        var pending = ctx.Alarms.CurrentAsync();

        var request = probe.ExpectMsg<GetAlarmSnapshotRequest>(TimeSpan.FromSeconds(5));
        Assert.Equal("CvdReactor01", request.InstanceUniqueName);
        Assert.NotEmpty(request.CorrelationId);

        probe.Reply(new GetAlarmSnapshotResponse(
            request.CorrelationId, request.InstanceUniqueName,
            Array.Empty<AlarmStateChanged>(), DateTimeOffset.UtcNow));

        Assert.Empty(await pending);
    }

    [Fact]
    public async Task CurrentAsync_ProjectsEveryNativeField_IncludingAckTime()
    {
        var probe = CreateTestProbe();
        var ctx = MakeContext(probe.Ref);

        var pending = ctx.Alarms.CurrentAsync();
        RespondWith(probe, NativeAcked());

        var alarm = Assert.Single(await pending);

        Assert.Equal("Z28061.HeartbeatTimeoutAlarm", alarm.Name);
        Assert.Equal("Z28061.HeartbeatTimeoutAlarm", alarm.SourceReference);
        Assert.Equal("ReactorAlarms", alarm.NativeSourceCanonicalName);
        Assert.True(alarm.Active);
        Assert.True(alarm.Acknowledged);
        Assert.Null(alarm.Confirmed);
        Assert.True(alarm.Shelved);      // TimedShelved collapses to a single boolean
        Assert.True(alarm.Suppressed);
        Assert.Equal(950, alarm.Severity);
        Assert.Equal("NativeMxAccess", alarm.Kind);
        Assert.Equal("Heartbeat lost", alarm.Message);
        Assert.Equal("DiscreteAlarm", alarm.AlarmTypeName);
        Assert.Equal("Process", alarm.Category);
        Assert.Equal("op1", alarm.OperatorUser);
        Assert.Equal("investigating", alarm.OperatorComment);
        Assert.Equal(RaisedAt, alarm.OriginalRaiseTime);
        Assert.Equal(ObservedAt, alarm.Timestamp);
        Assert.Equal(AckedAt, alarm.AckTime);   // §6.4 — the whole point of the enrichment
        Assert.Equal("FAULT", alarm.CurrentValue);
        Assert.Equal("0", alarm.LimitValue);
        Assert.False(alarm.IsConfiguredPlaceholder);
    }

    [Fact]
    public async Task CurrentAsync_UnackedNativeAlarm_ReportsNoAckTime()
    {
        var probe = CreateTestProbe();
        var ctx = MakeContext(probe.Ref);

        var pending = ctx.Alarms.CurrentAsync();
        RespondWith(probe, NativeAcked() with
        {
            Condition = new AlarmConditionState(
                true, Acknowledged: false, null, AlarmShelveState.Unshelved, false, 950),
            AckTime = null,
        });

        var alarm = Assert.Single(await pending);
        Assert.False(alarm.Acknowledged);
        Assert.Null(alarm.AckTime);
        Assert.False(alarm.Shelved);
    }

    [Fact]
    public async Task CurrentAsync_ComputedAlarm_ReadsFromTheDerivedCondition()
    {
        // A computed alarm sets no explicit Condition — it is derived from State + Priority.
        // Scripts must still see a coherent Active/Acknowledged/Severity triple so one
        // filter expression works across computed and native alarms alike.
        var probe = CreateTestProbe();
        var ctx = MakeContext(probe.Ref);

        var pending = ctx.Alarms.CurrentAsync();
        RespondWith(probe,
            new AlarmStateChanged("CvdReactor01", "HighTemp", AlarmState.Active, 700, ObservedAt));

        var alarm = Assert.Single(await pending);
        Assert.Equal("HighTemp", alarm.Name);
        Assert.Equal("Computed", alarm.Kind);
        Assert.True(alarm.Active);
        Assert.True(alarm.Acknowledged);     // computed alarms are auto-acked…
        Assert.Null(alarm.AckTime);          // …but carry no ack instant
        Assert.Equal(700, alarm.Severity);
        Assert.Equal(string.Empty, alarm.NativeSourceCanonicalName);
    }

    [Fact]
    public async Task CurrentAsync_SurfacesPlaceholderRows_SoQuietBindingsAreDistinguishable()
    {
        // The accessor deliberately does NOT pre-filter placeholders: a caller needs to tell
        // "this binding is configured and quiet" from "this binding does not exist".
        var probe = CreateTestProbe();
        var ctx = MakeContext(probe.Ref);

        var pending = ctx.Alarms.CurrentAsync();
        RespondWith(probe,
            NativeAcked(),
            new AlarmStateChanged("CvdReactor01", "LeftSideAlarms", AlarmState.Normal, 0, ObservedAt)
            {
                Kind = AlarmKind.NativeMxAccess,
                NativeSourceCanonicalName = "LeftSideAlarms",
                IsConfiguredPlaceholder = true,
            });

        var alarms = await pending;
        Assert.Equal(2, alarms.Count);

        // …and the documented caller-side filter yields only the real alarm.
        var real = Assert.Single(alarms, a => a.Active && !a.IsConfiguredPlaceholder);
        Assert.Equal("Z28061.HeartbeatTimeoutAlarm", real.Name);
    }

    [Fact]
    public async Task CurrentAsync_IsNotScopePrefixed_EveryScopeSeesTheSameList()
    {
        // Unlike Attributes, alarm identity is not a scope-relative name. A composed script
        // must not silently receive a truncated alarm set.
        var probe = CreateTestProbe();
        var ctx = MakeContext(probe.Ref);

        var pending = new AlarmsAccessor(ctx).CurrentAsync();
        RespondWith(probe, NativeAcked());

        Assert.Single(await pending);
    }
}
