using Shouldly;
using Xunit;
using ZB.MOM.WW.OtOpcUa.Core.ScriptedAlarms;

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

/// <summary>
///     Pure state-machine tests — no engine, no I/O, no async. Every transition rule
///     from Phase 7 plan Stream C.2 / C.3 has at least one locking test so regressions
///     surface as clear failures rather than subtle alarm-behavior drift.
/// </summary>
[Trait("Category", "Unit")]
public sealed class Part9StateMachineTests
{
    private static readonly DateTime T0 = new(2026, 1, 1, 12, 0, 0, DateTimeKind.Utc);
    private static AlarmConditionState Fresh() => AlarmConditionState.Fresh("alarm-1", T0);

    /// <summary>Verifies that an inactive alarm becomes active and emits Activated when predicate becomes true.</summary>
    [Fact]
    public void Predicate_true_on_inactive_becomes_active_and_emits_Activated()
    {
        var r = Part9StateMachine.ApplyPredicate(Fresh(), predicateTrue: true, T0.AddSeconds(1));
        r.State.Active.ShouldBe(AlarmActiveState.Active);
        r.State.Acked.ShouldBe(AlarmAckedState.Unacknowledged);
        r.State.Confirmed.ShouldBe(AlarmConfirmedState.Unconfirmed);
        r.Emission.ShouldBe(EmissionKind.Activated);
        r.State.LastActiveUtc.ShouldNotBeNull();
    }

    /// <summary>Verifies that an active alarm becomes inactive and emits Cleared when predicate becomes false.</summary>
    [Fact]
    public void Predicate_false_on_active_becomes_inactive_and_emits_Cleared()
    {
        var active = Part9StateMachine.ApplyPredicate(Fresh(), true, T0.AddSeconds(1)).State;
        var r = Part9StateMachine.ApplyPredicate(active, false, T0.AddSeconds(2));
        r.State.Active.ShouldBe(AlarmActiveState.Inactive);
        r.Emission.ShouldBe(EmissionKind.Cleared);
        r.State.LastClearedUtc.ShouldNotBeNull();
    }

    /// <summary>Verifies that predicate changes with unchanged state emit None.</summary>
    [Fact]
    public void Predicate_unchanged_state_emits_None()
    {
        var r = Part9StateMachine.ApplyPredicate(Fresh(), false, T0);
        r.Emission.ShouldBe(EmissionKind.None);
    }

    /// <summary>Verifies that a disabled alarm ignores predicate changes.</summary>
    [Fact]
    public void Disabled_alarm_ignores_predicate()
    {
        var disabled = Part9StateMachine.ApplyDisable(Fresh(), "op1", T0.AddSeconds(1)).State;
        var r = Part9StateMachine.ApplyPredicate(disabled, true, T0.AddSeconds(2));
        r.State.Active.ShouldBe(AlarmActiveState.Inactive);
        r.Emission.ShouldBe(EmissionKind.None);
    }

    /// <summary>Verifies that acknowledging an unacknowledged alarm records the user and emits Acknowledged.</summary>
    [Fact]
    public void Acknowledge_from_unacked_records_user_and_emits()
    {
        var active = Part9StateMachine.ApplyPredicate(Fresh(), true, T0.AddSeconds(1)).State;
        var r = Part9StateMachine.ApplyAcknowledge(active, "alice", "looking into it", T0.AddSeconds(2));
        r.State.Acked.ShouldBe(AlarmAckedState.Acknowledged);
        r.State.LastAckUser.ShouldBe("alice");
        r.State.LastAckComment.ShouldBe("looking into it");
        r.State.Comments.Count.ShouldBe(1);
        r.Emission.ShouldBe(EmissionKind.Acknowledged);
    }

    /// <summary>Verifies that acknowledging an already-acknowledged alarm is a no-op.</summary>
    [Fact]
    public void Acknowledge_when_already_acked_is_noop()
    {
        var active = Part9StateMachine.ApplyPredicate(Fresh(), true, T0.AddSeconds(1)).State;
        var acked = Part9StateMachine.ApplyAcknowledge(active, "alice", null, T0.AddSeconds(2)).State;
        var r = Part9StateMachine.ApplyAcknowledge(acked, "alice", null, T0.AddSeconds(3));
        r.Emission.ShouldBe(EmissionKind.None);
    }

    /// <summary>Verifies that acknowledging without a user throws ArgumentException.</summary>
    [Fact]
    public void Acknowledge_without_user_throws()
    {
        Should.Throw<ArgumentException>(() =>
            Part9StateMachine.ApplyAcknowledge(Fresh(), "", null, T0));
    }

    /// <summary>Verifies that confirming after clear records the user and emits Confirmed.</summary>
    [Fact]
    public void Confirm_after_clear_records_user_and_emits()
    {
        // Walk: activate -> ack -> clear -> confirm
        var s = Fresh();
        s = Part9StateMachine.ApplyPredicate(s, true, T0.AddSeconds(1)).State;
        s = Part9StateMachine.ApplyAcknowledge(s, "alice", null, T0.AddSeconds(2)).State;
        s = Part9StateMachine.ApplyPredicate(s, false, T0.AddSeconds(3)).State;

        var r = Part9StateMachine.ApplyConfirm(s, "bob", "resolved", T0.AddSeconds(4));
        r.State.Confirmed.ShouldBe(AlarmConfirmedState.Confirmed);
        r.State.LastConfirmUser.ShouldBe("bob");
        r.Emission.ShouldBe(EmissionKind.Confirmed);
    }

    /// <summary>Verifies that one-shot shelve suppresses the next activation emission.</summary>
    [Fact]
    public void OneShotShelve_suppresses_next_activation_emission()
    {
        var s = Part9StateMachine.ApplyOneShotShelve(Fresh(), "alice", T0.AddSeconds(1)).State;
        var r = Part9StateMachine.ApplyPredicate(s, true, T0.AddSeconds(2));
        r.State.Active.ShouldBe(AlarmActiveState.Active, "state still advances");
        r.Emission.ShouldBe(EmissionKind.Suppressed, "but subscribers don't see it");
    }

    /// <summary>Verifies that one-shot shelve expires when the alarm clears.</summary>
    [Fact]
    public void OneShotShelve_expires_on_clear()
    {
        var s = Fresh();
        s = Part9StateMachine.ApplyOneShotShelve(s, "alice", T0.AddSeconds(1)).State;
        s = Part9StateMachine.ApplyPredicate(s, true, T0.AddSeconds(2)).State;
        var r = Part9StateMachine.ApplyPredicate(s, false, T0.AddSeconds(3));
        r.State.Shelving.Kind.ShouldBe(ShelvingKind.Unshelved, "OneShot expires on clear");
    }

    /// <summary>Verifies that timed shelve requires a future unshelve time.</summary>
    [Fact]
    public void TimedShelve_requires_future_unshelve_time()
    {
        Should.Throw<ArgumentOutOfRangeException>(() =>
            Part9StateMachine.ApplyTimedShelve(Fresh(), "alice", T0, T0.AddSeconds(5)));
    }

    /// <summary>Verifies that timed shelve expires via shelving check at the specified time.</summary>
    [Fact]
    public void TimedShelve_expires_via_shelving_check()
    {
        var until = T0.AddMinutes(5);
        var shelved = Part9StateMachine.ApplyTimedShelve(Fresh(), "alice", until, T0).State;
        shelved.Shelving.Kind.ShouldBe(ShelvingKind.Timed);

        // Before expiry — still shelved.
        var earlier = Part9StateMachine.ApplyShelvingCheck(shelved, T0.AddMinutes(3));
        earlier.State.Shelving.Kind.ShouldBe(ShelvingKind.Timed);
        earlier.Emission.ShouldBe(EmissionKind.None);

        // After expiry — auto-unshelved + emission.
        var after = Part9StateMachine.ApplyShelvingCheck(shelved, T0.AddMinutes(6));
        after.State.Shelving.Kind.ShouldBe(ShelvingKind.Unshelved);
        after.Emission.ShouldBe(EmissionKind.Unshelved);
        after.State.Comments.Any(c => c.Kind == "AutoUnshelve").ShouldBeTrue();
    }

    /// <summary>Verifies that unshelving an unshelved alarm is a no-op.</summary>
    [Fact]
    public void Unshelve_from_unshelved_is_noop()
    {
        var r = Part9StateMachine.ApplyUnshelve(Fresh(), "alice", T0);
        r.Emission.ShouldBe(EmissionKind.None);
    }

    /// <summary>Verifies that explicit unshelve emits an Unshelved event.</summary>
    [Fact]
    public void Explicit_Unshelve_emits_event()
    {
        var s = Part9StateMachine.ApplyOneShotShelve(Fresh(), "alice", T0).State;
        var r = Part9StateMachine.ApplyUnshelve(s, "bob", T0.AddSeconds(30));
        r.State.Shelving.Kind.ShouldBe(ShelvingKind.Unshelved);
        r.Emission.ShouldBe(EmissionKind.Unshelved);
    }

    /// <summary>Verifies that adding a comment appends to the audit trail and emits CommentAdded.</summary>
    [Fact]
    public void AddComment_appends_to_audit_trail_with_event()
    {
        var r = Part9StateMachine.ApplyAddComment(Fresh(), "alice", "investigating", T0.AddSeconds(5));
        r.State.Comments.Count.ShouldBe(1);
        r.State.Comments[0].Kind.ShouldBe("AddComment");
        r.State.Comments[0].User.ShouldBe("alice");
        r.State.Comments[0].Text.ShouldBe("investigating");
        r.Emission.ShouldBe(EmissionKind.CommentAdded);
    }

    /// <summary>Verifies that comments are append-only and never rewritten.</summary>
    [Fact]
    public void Comments_are_append_only_never_rewritten()
    {
        var s = Part9StateMachine.ApplyAddComment(Fresh(), "alice", "first", T0.AddSeconds(1)).State;
        s = Part9StateMachine.ApplyAddComment(s, "bob", "second", T0.AddSeconds(2)).State;
        s = Part9StateMachine.ApplyAddComment(s, "carol", "third", T0.AddSeconds(3)).State;
        s.Comments.Count.ShouldBe(3);
        s.Comments[0].User.ShouldBe("alice");
        s.Comments[1].User.ShouldBe("bob");
        s.Comments[2].User.ShouldBe("carol");
    }

    /// <summary>Verifies that a full alarm lifecycle walk produces all expected emissions.</summary>
    [Fact]
    public void Full_lifecycle_walk_produces_every_expected_emission()
    {
        // Walk a condition through its whole lifecycle and make sure emissions line up.
        var emissions = new List<EmissionKind>();
        var s = Fresh();

        s = Capture(Part9StateMachine.ApplyPredicate(s, true, T0.AddSeconds(1)));
        s = Capture(Part9StateMachine.ApplyAcknowledge(s, "alice", null, T0.AddSeconds(2)));
        s = Capture(Part9StateMachine.ApplyAddComment(s, "alice", "need to investigate", T0.AddSeconds(3)));
        s = Capture(Part9StateMachine.ApplyPredicate(s, false, T0.AddSeconds(4)));
        s = Capture(Part9StateMachine.ApplyConfirm(s, "bob", null, T0.AddSeconds(5)));

        emissions.ShouldBe(new[] {
            EmissionKind.Activated,
            EmissionKind.Acknowledged,
            EmissionKind.CommentAdded,
            EmissionKind.Cleared,
            EmissionKind.Confirmed,
        });

        AlarmConditionState Capture(TransitionResult r) { emissions.Add(r.Emission); return r.State; }
    }
}
