using Opc.Ua;
using Shouldly;
using Xunit;
using ZB.MOM.WW.OtOpcUa.Client.Shared.Models;
using ZB.MOM.WW.OtOpcUa.Client.UI.Services;
using ZB.MOM.WW.OtOpcUa.Client.UI.Tests.Fakes;
using ZB.MOM.WW.OtOpcUa.Client.UI.ViewModels;

namespace ZB.MOM.WW.OtOpcUa.Client.UI.Tests;

public class AlarmsViewModelTests
{
    private readonly FakeOpcUaClientService _service;
    private readonly AlarmsViewModel _vm;

    /// <summary>Initializes a new test instance.</summary>
    public AlarmsViewModelTests()
    {
        _service = new FakeOpcUaClientService();
        var dispatcher = new SynchronousUiDispatcher();
        _vm = new AlarmsViewModel(_service, dispatcher);
    }

    /// <summary>Verifies that SubscribeCommand cannot execute when disconnected.</summary>
    [Fact]
    public void SubscribeCommand_CannotExecute_WhenDisconnected()
    {
        _vm.IsConnected = false;
        _vm.SubscribeCommand.CanExecute(null).ShouldBeFalse();
    }

    /// <summary>Verifies that SubscribeCommand cannot execute when already subscribed.</summary>
    [Fact]
    public void SubscribeCommand_CannotExecute_WhenAlreadySubscribed()
    {
        _vm.IsConnected = true;
        _vm.IsSubscribed = true;
        _vm.SubscribeCommand.CanExecute(null).ShouldBeFalse();
    }

    /// <summary>Verifies that SubscribeCommand can execute when connected and not subscribed.</summary>
    [Fact]
    public void SubscribeCommand_CanExecute_WhenConnectedAndNotSubscribed()
    {
        _vm.IsConnected = true;
        _vm.IsSubscribed = false;
        _vm.SubscribeCommand.CanExecute(null).ShouldBeTrue();
    }

    /// <summary>Verifies that SubscribeCommand sets IsSubscribed flag.</summary>
    [Fact]
    public async Task SubscribeCommand_SetsIsSubscribed()
    {
        _vm.IsConnected = true;

        await _vm.SubscribeCommand.ExecuteAsync(null);

        _vm.IsSubscribed.ShouldBeTrue();
        _service.SubscribeAlarmsCallCount.ShouldBe(1);
    }

    /// <summary>Verifies that UnsubscribeCommand cannot execute when not subscribed.</summary>
    [Fact]
    public void UnsubscribeCommand_CannotExecute_WhenNotSubscribed()
    {
        _vm.IsConnected = true;
        _vm.IsSubscribed = false;
        _vm.UnsubscribeCommand.CanExecute(null).ShouldBeFalse();
    }

    /// <summary>Verifies that UnsubscribeCommand clears IsSubscribed flag.</summary>
    [Fact]
    public async Task UnsubscribeCommand_ClearsIsSubscribed()
    {
        _vm.IsConnected = true;
        await _vm.SubscribeCommand.ExecuteAsync(null);

        await _vm.UnsubscribeCommand.ExecuteAsync(null);

        _vm.IsSubscribed.ShouldBeFalse();
        _service.UnsubscribeAlarmsCallCount.ShouldBe(1);
    }

    /// <summary>Verifies that RefreshCommand calls the service.</summary>
    [Fact]
    public async Task RefreshCommand_CallsService()
    {
        _vm.IsConnected = true;
        _vm.IsSubscribed = true;

        await _vm.RefreshCommand.ExecuteAsync(null);

        _service.RequestConditionRefreshCallCount.ShouldBe(1);
    }

    /// <summary>Verifies that RefreshCommand cannot execute when not subscribed.</summary>
    [Fact]
    public void RefreshCommand_CannotExecute_WhenNotSubscribed()
    {
        _vm.IsConnected = true;
        _vm.IsSubscribed = false;
        _vm.RefreshCommand.CanExecute(null).ShouldBeFalse();
    }

    /// <summary>Verifies that alarm events are added to the collection.</summary>
    [Fact]
    public void AlarmEvent_AddsToCollection()
    {
        var alarm = new AlarmEventArgs(
            "Source1", "HighAlarm", 500, "Temperature high",
            true, true, false, DateTime.UtcNow);

        _service.RaiseAlarmEvent(alarm);

        _vm.AlarmEvents.Count.ShouldBe(1);
        _vm.AlarmEvents[0].SourceName.ShouldBe("Source1");
        _vm.AlarmEvents[0].ConditionName.ShouldBe("HighAlarm");
        _vm.AlarmEvents[0].Severity.ShouldBe((ushort)500);
        _vm.AlarmEvents[0].Message.ShouldBe("Temperature high");
    }

    /// <summary>Verifies that Clear resets the view model state.</summary>
    [Fact]
    public void Clear_ResetsState()
    {
        _vm.IsSubscribed = true;
        _vm.AlarmEvents.Add(new AlarmEventViewModel("Src", "Cond", 100, "Msg", true, true, false, DateTime.UtcNow));

        _vm.Clear();

        _vm.AlarmEvents.ShouldBeEmpty();
        _vm.IsSubscribed.ShouldBeFalse();
    }

    /// <summary>Verifies that Teardown unregisters the event handler.</summary>
    [Fact]
    public void Teardown_UnhooksEventHandler()
    {
        _vm.Teardown();

        var alarm = new AlarmEventArgs(
            "Source1", "HighAlarm", 500, "Test",
            true, true, false, DateTime.UtcNow);
        _service.RaiseAlarmEvent(alarm);

        _vm.AlarmEvents.ShouldBeEmpty();
    }

    /// <summary>Verifies that the default polling interval is 1000ms.</summary>
    [Fact]
    public void DefaultInterval_Is1000()
    {
        _vm.Interval.ShouldBe(1000);
    }

    // --- Alarm update and non-retain remove paths (Client.UI-012 / Client.UI-013) ---

    /// <summary>
    /// Regression test for Client.UI-013 / Client.UI-012 — a second event for the same
    /// source+condition must update the existing row in place rather than adding a duplicate.
    /// </summary>
    [Fact]
    public void AlarmEvent_ExistingAlarm_UpdatesInPlace()
    {
        var first = new AlarmEventArgs(
            "Source1", "HighAlarm", 500, "Temperature high",
            retain: true, activeState: true, ackedState: false, time: DateTime.UtcNow);
        _service.RaiseAlarmEvent(first);

        var updated = new AlarmEventArgs(
            "Source1", "HighAlarm", 750, "Temperature very high",
            retain: true, activeState: true, ackedState: false, time: DateTime.UtcNow);
        _service.RaiseAlarmEvent(updated);

        _vm.AlarmEvents.Count.ShouldBe(1);
        _vm.AlarmEvents[0].Severity.ShouldBe((ushort)750);
        _vm.AlarmEvents[0].Message.ShouldBe("Temperature very high");
    }

    /// <summary>
    /// Regression test for Client.UI-013 — when an existing retained alarm becomes
    /// non-retained the row must be removed from the collection with exactly ONE collection
    /// mutation (not a Replace + Remove pair).
    /// </summary>
    [Fact]
    public void AlarmEvent_ExistingAlarmBecomesNonRetained_IsRemovedCleanly()
    {
        // Seed a retained alarm.
        var first = new AlarmEventArgs(
            "Source1", "HighAlarm", 500, "Active",
            retain: true, activeState: true, ackedState: false, time: DateTime.UtcNow);
        _service.RaiseAlarmEvent(first);
        _vm.AlarmEvents.Count.ShouldBe(1);

        // Track collection-change notifications.
        var changeCount = 0;
        _vm.AlarmEvents.CollectionChanged += (_, _) => changeCount++;

        // Now the alarm is cleared (Retain = false).
        var cleared = new AlarmEventArgs(
            "Source1", "HighAlarm", 500, "Cleared",
            retain: false, activeState: false, ackedState: false, time: DateTime.UtcNow);
        _service.RaiseAlarmEvent(cleared);

        // Row must be removed.
        _vm.AlarmEvents.ShouldBeEmpty();
        // Exactly one collection-change notification (Remove), not two (Replace then Remove).
        changeCount.ShouldBe(1);
    }

    /// <summary>
    /// Verifies that a non-retained alarm that has no existing row in the collection
    /// is silently dropped (not added).
    /// </summary>
    [Fact]
    public void AlarmEvent_NonRetained_WithNoExistingRow_IsNotAdded()
    {
        var nonRetained = new AlarmEventArgs(
            "Source1", "HighAlarm", 500, "Already cleared",
            retain: false, activeState: false, ackedState: false, time: DateTime.UtcNow);
        _service.RaiseAlarmEvent(nonRetained);

        _vm.AlarmEvents.ShouldBeEmpty();
    }

    /// <summary>
    /// Verifies that ActiveAlarmCount is updated correctly when an alarm is acknowledged
    /// via an event update (AckedState changes from false to true).
    /// </summary>
    [Fact]
    public void AlarmEvent_AcknowledgedUpdate_DecrementsActiveAlarmCount()
    {
        // Seed an unacknowledged active alarm.
        var active = new AlarmEventArgs(
            "Source1", "HighAlarm", 500, "Active",
            retain: true, activeState: true, ackedState: false, time: DateTime.UtcNow);
        _service.RaiseAlarmEvent(active);
        _vm.ActiveAlarmCount.ShouldBe(1);

        // Acknowledge the alarm.
        var acked = new AlarmEventArgs(
            "Source1", "HighAlarm", 500, "Acked",
            retain: true, activeState: true, ackedState: true, time: DateTime.UtcNow);
        _service.RaiseAlarmEvent(acked);

        _vm.AlarmEvents.Count.ShouldBe(1);
        _vm.AlarmEvents[0].AckedState.ShouldBeTrue();
        _vm.ActiveAlarmCount.ShouldBe(0);
    }

    /// <summary>
    /// Regression test for Client.UI-006 — when SubscribeAlarmsAsync throws, the failure must be
    /// surfaced to the operator via the view model's StatusMessage rather than silently swallowed.
    /// </summary>
    [Fact]
    public async Task Subscribe_OnFailure_SurfacesStatusMessage()
    {
        _vm.IsConnected = true;
        _service.SubscribeAlarmsException = new Exception("Server returned BadSubscriptionIdInvalid");

        await _vm.SubscribeCommand.ExecuteAsync(null);

        _vm.IsSubscribed.ShouldBeFalse();
        _vm.StatusMessage.ShouldNotBeNullOrWhiteSpace();
        _vm.StatusMessage.ShouldContain("BadSubscriptionIdInvalid");
    }

    /// <summary>Builds a representative alarm row for shelve/confirm tests.</summary>
    /// <param name="ackedState">Whether the alarm is already acknowledged.</param>
    /// <param name="hasEventId">Whether the row carries an EventId; <c>false</c> models a missing EventId.</param>
    /// <param name="conditionNodeId">The condition node id; pass <c>null</c> to model a missing ConditionId.</param>
    private static AlarmEventViewModel BuildAlarm(
        bool ackedState = false,
        bool hasEventId = true,
        string? conditionNodeId = "ns=2;s=Cond1")
    {
        return new AlarmEventViewModel(
            "Source1", "HighAlarm", 500, "Temperature high",
            true, true, ackedState, DateTime.UtcNow,
            hasEventId ? [1, 2, 3] : null, conditionNodeId);
    }

    // --- ShelveAlarmAsync ---

    /// <summary>Verifies that a OneShot shelve calls the service and reports success.</summary>
    [Fact]
    public async Task ShelveAlarm_Connected_OneShot_Succeeds()
    {
        _vm.IsConnected = true;
        var alarm = BuildAlarm();

        var (ok, _) = await _vm.ShelveAlarmAsync(alarm, ShelveKind.OneShot, 0);

        ok.ShouldBeTrue();
        _service.ShelveCallCount.ShouldBe(1);
        _service.LastShelveCall.ShouldNotBeNull();
        _service.LastShelveCall!.Value.Kind.ShouldBe(ShelveKind.OneShot);
        _service.LastShelveCall.Value.ConditionNodeId.ShouldBe(alarm.ConditionNodeId);
    }

    /// <summary>Verifies that a Timed shelve passes the duration through to the service.</summary>
    [Fact]
    public async Task ShelveAlarm_Connected_Timed_PassesDuration()
    {
        _vm.IsConnected = true;
        var alarm = BuildAlarm();

        var (ok, _) = await _vm.ShelveAlarmAsync(alarm, ShelveKind.Timed, 300);

        ok.ShouldBeTrue();
        _service.LastShelveCall.ShouldNotBeNull();
        _service.LastShelveCall!.Value.Kind.ShouldBe(ShelveKind.Timed);
        _service.LastShelveCall.Value.ShelvingTimeSeconds.ShouldBe(300);
    }

    /// <summary>Verifies that an Unshelve calls the service and reports success.</summary>
    [Fact]
    public async Task ShelveAlarm_Connected_Unshelve_Succeeds()
    {
        _vm.IsConnected = true;
        var alarm = BuildAlarm();

        var (ok, _) = await _vm.ShelveAlarmAsync(alarm, ShelveKind.Unshelve, 0);

        ok.ShouldBeTrue();
        _service.LastShelveCall.ShouldNotBeNull();
        _service.LastShelveCall!.Value.Kind.ShouldBe(ShelveKind.Unshelve);
    }

    /// <summary>Verifies that a Timed shelve with a non-positive duration is rejected without a service call.</summary>
    [Fact]
    public async Task ShelveAlarm_Timed_NonPositiveDuration_Fails_NoServiceCall()
    {
        _vm.IsConnected = true;
        var alarm = BuildAlarm();

        var (ok, _) = await _vm.ShelveAlarmAsync(alarm, ShelveKind.Timed, 0);

        ok.ShouldBeFalse();
        _service.ShelveCallCount.ShouldBe(0);
    }

    /// <summary>Verifies that shelving when disconnected is rejected without a service call.</summary>
    [Fact]
    public async Task ShelveAlarm_NotConnected_Fails_NoServiceCall()
    {
        _vm.IsConnected = false;
        var alarm = BuildAlarm();

        var (ok, _) = await _vm.ShelveAlarmAsync(alarm, ShelveKind.OneShot, 0);

        ok.ShouldBeFalse();
        _service.ShelveCallCount.ShouldBe(0);
    }

    /// <summary>Verifies that shelving an alarm without a ConditionNodeId is rejected without a service call.</summary>
    [Fact]
    public async Task ShelveAlarm_MissingConditionNodeId_Fails_NoServiceCall()
    {
        _vm.IsConnected = true;
        var alarm = BuildAlarm(conditionNodeId: null);

        var (ok, _) = await _vm.ShelveAlarmAsync(alarm, ShelveKind.OneShot, 0);

        ok.ShouldBeFalse();
        _service.ShelveCallCount.ShouldBe(0);
    }

    /// <summary>Verifies that a Bad status code from the service surfaces a "Shelve failed" message.</summary>
    [Fact]
    public async Task ShelveAlarm_ServiceBad_Fails_WithMessage()
    {
        _vm.IsConnected = true;
        _service.ShelveResult = StatusCodes.BadNodeIdUnknown;
        var alarm = BuildAlarm();

        var (ok, message) = await _vm.ShelveAlarmAsync(alarm, ShelveKind.OneShot, 0);

        ok.ShouldBeFalse();
        message.ShouldContain("Shelve failed");
    }

    // --- ConfirmAlarmAsync ---

    /// <summary>Verifies that confirming an alarm calls the service with the supplied comment.</summary>
    [Fact]
    public async Task ConfirmAlarm_Connected_Succeeds_PassesComment()
    {
        _vm.IsConnected = true;
        var alarm = BuildAlarm();

        var (ok, _) = await _vm.ConfirmAlarmAsync(alarm, "operator note");

        ok.ShouldBeTrue();
        _service.ConfirmCallCount.ShouldBe(1);
        _service.LastConfirmCall.ShouldNotBeNull();
        _service.LastConfirmCall!.Value.Comment.ShouldBe("operator note");
    }

    /// <summary>Verifies that confirming when disconnected is rejected without a service call.</summary>
    [Fact]
    public async Task ConfirmAlarm_NotConnected_Fails_NoServiceCall()
    {
        _vm.IsConnected = false;
        var alarm = BuildAlarm();

        var (ok, _) = await _vm.ConfirmAlarmAsync(alarm, "note");

        ok.ShouldBeFalse();
        _service.ConfirmCallCount.ShouldBe(0);
    }

    /// <summary>Verifies that confirming an alarm without an EventId is rejected without a service call.</summary>
    [Fact]
    public async Task ConfirmAlarm_MissingEventId_Fails_NoServiceCall()
    {
        _vm.IsConnected = true;
        var alarm = BuildAlarm(hasEventId: false);

        var (ok, _) = await _vm.ConfirmAlarmAsync(alarm, "note");

        ok.ShouldBeFalse();
        _service.ConfirmCallCount.ShouldBe(0);
    }

    /// <summary>Verifies that a Bad status code from the service surfaces a "Confirm failed" message.</summary>
    [Fact]
    public async Task ConfirmAlarm_ServiceBad_Fails_WithMessage()
    {
        _vm.IsConnected = true;
        _service.ConfirmResult = StatusCodes.BadNodeIdUnknown;
        var alarm = BuildAlarm();

        var (ok, message) = await _vm.ConfirmAlarmAsync(alarm, "note");

        ok.ShouldBeFalse();
        message.ShouldContain("Confirm failed");
    }

    /// <summary>Verifies that confirming an alarm without a ConditionNodeId is rejected without a service call.</summary>
    [Fact]
    public async Task ConfirmAlarm_MissingConditionNodeId_Fails_NoServiceCall()
    {
        _vm.IsConnected = true;
        var alarm = BuildAlarm(conditionNodeId: null);

        var (ok, _) = await _vm.ConfirmAlarmAsync(alarm, "note");

        ok.ShouldBeFalse();
        _service.ConfirmCallCount.ShouldBe(0);
    }

    /// <summary>Verifies that a transport exception from ShelveAlarmAsync is caught and surfaced as an error message.</summary>
    [Fact]
    public async Task ShelveAlarm_ServiceThrows_ReturnsError()
    {
        _vm.IsConnected = true;
        _service.ShelveException = new InvalidOperationException("transport error");
        var alarm = BuildAlarm();

        var (ok, message) = await _vm.ShelveAlarmAsync(alarm, ShelveKind.OneShot, 0);

        ok.ShouldBeFalse();
        message.ShouldContain("Error");
    }

    /// <summary>Verifies that a transport exception from ConfirmAlarmAsync is caught and surfaced as an error message.</summary>
    [Fact]
    public async Task ConfirmAlarm_ServiceThrows_ReturnsError()
    {
        _vm.IsConnected = true;
        _service.ConfirmException = new InvalidOperationException("transport error");
        var alarm = BuildAlarm();

        var (ok, message) = await _vm.ConfirmAlarmAsync(alarm, "note");

        ok.ShouldBeFalse();
        message.ShouldContain("Error");
    }

    // --- CanShelve / CanConfirm predicates ---

    /// <summary>Verifies CanShelve is true when a ConditionNodeId is present.</summary>
    [Fact]
    public void CanShelve_True_WhenConditionNodeIdPresent()
    {
        BuildAlarm(conditionNodeId: "ns=2;s=Cond1").CanShelve.ShouldBeTrue();
    }

    /// <summary>Verifies CanShelve is false when the ConditionNodeId is missing.</summary>
    [Fact]
    public void CanShelve_False_WhenConditionNodeIdNull()
    {
        BuildAlarm(conditionNodeId: null).CanShelve.ShouldBeFalse();
    }

    /// <summary>Verifies CanConfirm is true when acked with EventId and ConditionNodeId present.</summary>
    [Fact]
    public void CanConfirm_True_WhenAckedWithEventIdAndConditionNodeId()
    {
        BuildAlarm(ackedState: true).CanConfirm.ShouldBeTrue();
    }

    /// <summary>Verifies CanConfirm is false when the alarm has not been acknowledged.</summary>
    [Fact]
    public void CanConfirm_False_WhenNotAcked()
    {
        BuildAlarm(ackedState: false).CanConfirm.ShouldBeFalse();
    }

    /// <summary>Verifies CanConfirm is false when the EventId is missing.</summary>
    [Fact]
    public void CanConfirm_False_WhenEventIdNull()
    {
        BuildAlarm(ackedState: true, hasEventId: false).CanConfirm.ShouldBeFalse();
    }

    /// <summary>Verifies CanConfirm is false when the ConditionNodeId is missing.</summary>
    [Fact]
    public void CanConfirm_False_WhenConditionNodeIdNull()
    {
        BuildAlarm(ackedState: true, conditionNodeId: null).CanConfirm.ShouldBeFalse();
    }
}