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; /// Initializes a new test instance. public AlarmsViewModelTests() { _service = new FakeOpcUaClientService(); var dispatcher = new SynchronousUiDispatcher(); _vm = new AlarmsViewModel(_service, dispatcher); } /// Verifies that SubscribeCommand cannot execute when disconnected. [Fact] public void SubscribeCommand_CannotExecute_WhenDisconnected() { _vm.IsConnected = false; _vm.SubscribeCommand.CanExecute(null).ShouldBeFalse(); } /// Verifies that SubscribeCommand cannot execute when already subscribed. [Fact] public void SubscribeCommand_CannotExecute_WhenAlreadySubscribed() { _vm.IsConnected = true; _vm.IsSubscribed = true; _vm.SubscribeCommand.CanExecute(null).ShouldBeFalse(); } /// Verifies that SubscribeCommand can execute when connected and not subscribed. [Fact] public void SubscribeCommand_CanExecute_WhenConnectedAndNotSubscribed() { _vm.IsConnected = true; _vm.IsSubscribed = false; _vm.SubscribeCommand.CanExecute(null).ShouldBeTrue(); } /// Verifies that SubscribeCommand sets IsSubscribed flag. [Fact] public async Task SubscribeCommand_SetsIsSubscribed() { _vm.IsConnected = true; await _vm.SubscribeCommand.ExecuteAsync(null); _vm.IsSubscribed.ShouldBeTrue(); _service.SubscribeAlarmsCallCount.ShouldBe(1); } /// Verifies that UnsubscribeCommand cannot execute when not subscribed. [Fact] public void UnsubscribeCommand_CannotExecute_WhenNotSubscribed() { _vm.IsConnected = true; _vm.IsSubscribed = false; _vm.UnsubscribeCommand.CanExecute(null).ShouldBeFalse(); } /// Verifies that UnsubscribeCommand clears IsSubscribed flag. [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); } /// Verifies that RefreshCommand calls the service. [Fact] public async Task RefreshCommand_CallsService() { _vm.IsConnected = true; _vm.IsSubscribed = true; await _vm.RefreshCommand.ExecuteAsync(null); _service.RequestConditionRefreshCallCount.ShouldBe(1); } /// Verifies that RefreshCommand cannot execute when not subscribed. [Fact] public void RefreshCommand_CannotExecute_WhenNotSubscribed() { _vm.IsConnected = true; _vm.IsSubscribed = false; _vm.RefreshCommand.CanExecute(null).ShouldBeFalse(); } /// Verifies that alarm events are added to the collection. [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"); } /// Verifies that Clear resets the view model state. [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(); } /// Verifies that Teardown unregisters the event handler. [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(); } /// Verifies that the default polling interval is 1000ms. [Fact] public void DefaultInterval_Is1000() { _vm.Interval.ShouldBe(1000); } /// /// 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. /// [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"); } }