using Shouldly;
using Xunit;
using ZB.MOM.WW.OtOpcUa.Commons.OpcUa;
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
using ZB.MOM.WW.OtOpcUa.Runtime.Drivers;

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

public class NativeAlarmProjectorTests
{
    [Fact]
    public void Raise_projects_active_unacked_with_constant_fields_and_mapped_severity()
    {
        var sut = new NativeAlarmProjector();

        var snap = sut.Project("n1", Evt(AlarmTransitionKind.Raise, AlarmSeverity.High, "boom"));

        snap.Active.ShouldBeTrue();
        snap.Acknowledged.ShouldBeFalse();
        snap.Confirmed.ShouldBeTrue();
        snap.Enabled.ShouldBeTrue();
        snap.Shelving.ShouldBe(AlarmShelvingKind.Unshelved);
        snap.Severity.ShouldBe((ushort)700);
        snap.Message.ShouldBe("boom");
    }

    [Fact]
    public void Retrigger_projects_active_unacked()
    {
        var sut = new NativeAlarmProjector();

        var snap = sut.Project("n1", Evt(AlarmTransitionKind.Retrigger));

        snap.Active.ShouldBeTrue();
        snap.Acknowledged.ShouldBeFalse();
    }

    [Fact]
    public void Acknowledge_after_raise_acks_and_keeps_active()
    {
        var sut = new NativeAlarmProjector();
        sut.Project("n1", Evt(AlarmTransitionKind.Raise));

        var snap = sut.Project("n1", Evt(AlarmTransitionKind.Acknowledge));

        snap.Active.ShouldBeTrue();
        snap.Acknowledged.ShouldBeTrue();
    }

    [Fact]
    public void Clear_after_raise_deactivates_and_keeps_prior_ack()
    {
        var sut = new NativeAlarmProjector();
        sut.Project("n1", Evt(AlarmTransitionKind.Raise)); // active, unacked

        var snap = sut.Project("n1", Evt(AlarmTransitionKind.Clear));

        snap.Active.ShouldBeFalse();
        snap.Acknowledged.ShouldBeFalse(); // preserved prior (unacked) ack state
    }

    [Fact]
    public void Clear_after_acknowledge_deactivates_and_keeps_acked()
    {
        var sut = new NativeAlarmProjector();
        sut.Project("n1", Evt(AlarmTransitionKind.Raise));
        sut.Project("n1", Evt(AlarmTransitionKind.Acknowledge)); // active, acked

        var snap = sut.Project("n1", Evt(AlarmTransitionKind.Clear));

        snap.Active.ShouldBeFalse();
        snap.Acknowledged.ShouldBeTrue(); // preserved prior (acked) ack state
    }

    [Fact]
    public void Unspecified_carries_prior_active_and_ack_state()
    {
        var sut = new NativeAlarmProjector();
        sut.Project("n1", Evt(AlarmTransitionKind.Raise)); // active, unacked

        var snap = sut.Project("n1", Evt(AlarmTransitionKind.Unspecified));

        snap.Active.ShouldBeTrue();
        snap.Acknowledged.ShouldBeFalse();
    }

    [Fact]
    public void Cold_state_defaults_to_inactive_acked()
    {
        var sut = new NativeAlarmProjector();

        // Acknowledge on a never-seen node: prior Active defaults false, ack set true.
        var snap = sut.Project("nNew", Evt(AlarmTransitionKind.Acknowledge));

        snap.Active.ShouldBeFalse();
        snap.Acknowledged.ShouldBeTrue();
    }

    [Fact]
    public void Per_node_state_is_isolated()
    {
        var sut = new NativeAlarmProjector();
        sut.Project("n1", Evt(AlarmTransitionKind.Raise)); // n1 active

        // n2 is cold; a Clear on it must stay inactive and not borrow n1's active state.
        var n2 = sut.Project("n2", Evt(AlarmTransitionKind.Clear));

        n2.Active.ShouldBeFalse();

        // n1 is unaffected by the n2 transition.
        var n1 = sut.Project("n1", Evt(AlarmTransitionKind.Acknowledge));
        n1.Active.ShouldBeTrue();
    }

    [Theory]
    [InlineData(AlarmSeverity.Low, (ushort)200)]
    [InlineData(AlarmSeverity.Medium, (ushort)500)]
    [InlineData(AlarmSeverity.High, (ushort)700)]
    [InlineData(AlarmSeverity.Critical, (ushort)900)]
    public void Severity_buckets_map_to_opcua_scale(AlarmSeverity sev, ushort expected)
    {
        var sut = new NativeAlarmProjector();

        var snap = sut.Project("n1", Evt(AlarmTransitionKind.Raise, sev));

        snap.Severity.ShouldBe(expected);
    }

    [Fact]
    public void Severity_and_message_always_come_from_the_event()
    {
        var sut = new NativeAlarmProjector();
        sut.Project("n1", Evt(AlarmTransitionKind.Raise, AlarmSeverity.High, "first"));

        // A later Clear carries its own severity + message; the snapshot reflects the event, not prior.
        var snap = sut.Project("n1", Evt(AlarmTransitionKind.Clear, AlarmSeverity.Low, "second"));

        snap.Severity.ShouldBe((ushort)200);
        snap.Message.ShouldBe("second");
    }

    [Fact]
    public void Clear_resets_tracked_state()
    {
        var sut = new NativeAlarmProjector();
        sut.Project("n1", Evt(AlarmTransitionKind.Raise)); // n1 active, unacked

        sut.Clear();

        // After Clear() the node is cold again: an Acknowledge sees prior Active=false.
        var snap = sut.Project("n1", Evt(AlarmTransitionKind.Acknowledge));
        snap.Active.ShouldBeFalse();
        snap.Acknowledged.ShouldBeTrue();
    }

    // #477 — native transitions carry Good quality; comms-loss quality is applied out-of-band via the
    // dedicated WriteAlarmQuality path (connectivity → DriverHostActor), NOT through the projector, so the
    // projector stays quality-agnostic and its transition snapshots default to Good.
    [Fact]
    public void Transition_projection_defaults_to_good_quality()
    {
        var sut = new NativeAlarmProjector();

        var snap = sut.Project("n1", Evt(AlarmTransitionKind.Raise));

        snap.Quality.ShouldBe(OpcUaQuality.Good);
    }

    private static AlarmEventArgs Evt(
        AlarmTransitionKind kind,
        AlarmSeverity sev = AlarmSeverity.High,
        string msg = "m")
        => new(new H(), "Tank1.Hi", "c1", "LimitAlarm.Hi", msg, sev, DateTime.UnixEpoch, Kind: kind);

    private sealed class H : IAlarmSubscriptionHandle
    {
        public string DiagnosticId => "t";
    }
}
