using System; using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; using Xunit; using Moq; using Haoliang.Core.Services; using Haoliang.Data.Repositories; using Haoliang.Models.Models.Device; using Haoliang.Models.Models.System; namespace Haoliang.Tests.Services { public class DeviceStateMachineTests { private readonly Mock _mockDeviceRepository; private readonly Mock _mockCollectionService; private readonly Mock _mockAlarmService; private readonly Mock _mockCacheService; private readonly DeviceStateMachine _deviceStateMachine; public DeviceStateMachineTests() { _mockDeviceRepository = new Mock(); _mockCollectionService = new Mock(); _mockAlarmService = new Mock(); _mockCacheService = new Mock(); _deviceStateMachine = new DeviceStateMachine( _mockDeviceRepository.Object, _mockCollectionService.Object, _mockAlarmService.Object, _mockCacheService.Object ); } [Fact] public async Task GetCurrentState_DeviceInCache_ReturnsState() { // Arrange var deviceId = 1; var expectedState = DeviceState.Running; // Setup device context in cache var deviceContext = new DeviceStateContext { CurrentState = expectedState, PreviousState = DeviceState.Idle, StateChangedAt = DateTime.UtcNow }; // This would require mocking the internal dictionary // For this test, we'll simulate the initial state await _deviceStateMachine.StartAsync(CancellationToken.None); await _deviceStateMachine.ForceStateAsync(deviceId, expectedState, "Test setup"); // Act var result = _deviceStateMachine.GetCurrentState(deviceId); // Assert Assert.Equal(expectedState, result); } [Fact] public async Task CanTransitionAsync_ValidTransition_ReturnsTrue() { // Arrange var deviceId = 1; var fromState = DeviceState.Idle; var targetState = DeviceState.Running; // Initialize device state await _deviceStateMachine.ForceStateAsync(deviceId, fromState, "Test setup"); // Act var result = await _deviceStateMachine.CanTransitionAsync(deviceId, targetState); // Assert Assert.True(result); } [Fact] public async Task CanTransitionAsync_InvalidTransition_ReturnsFalse() { // Arrange var deviceId = 1; var fromState = DeviceState.Running; var targetState = DeviceState.Offline; // Initialize device state await _deviceStateMachine.ForceStateAsync(deviceId, fromState, "Test setup"); // Act var result = await _deviceStateMachine.CanTransitionAsync(deviceId, targetState); // Assert Assert.False(result); } [Fact] public async Task TransitionToStateAsync_ValidTransition_TransitionsState() { // Arrange var deviceId = 1; var fromState = DeviceState.Idle; var targetState = DeviceState.Running; var device = new CNCDevice { Id = deviceId, Name = "Test Device", Status = DeviceStatus.Idle }; var currentStatus = new DeviceCurrentStatus { DeviceId = deviceId, Status = DeviceStatus.Idle, Runtime = TimeSpan.Zero }; _mockDeviceRepository.Setup(repo => repo.GetByIdAsync(deviceId)) .ReturnsAsync(device); _mockCollectionService.Setup(service => service.GetDeviceCurrentStatusAsync(deviceId)) .ReturnsAsync(currentStatus); _mockDeviceRepository.Setup(repo => repo.UpdateDeviceAsync(device)) .Returns(Task.CompletedTask); // Initialize device state await _deviceStateMachine.ForceStateAsync(deviceId, fromState, "Test setup"); // Act var result = await _deviceStateMachine.TransitionToStateAsync(deviceId, targetState); // Assert Assert.True(result.Success); Assert.Equal(fromState, result.FromState); Assert.Equal(targetState, result.ToState); Assert.Equal($"Successfully transitioned from {fromState} to {targetState}", result.Message); } [Fact] public async Task TransitionToStateAsync_InvalidTransition_ReturnsFailure() { // Arrange var deviceId = 1; var fromState = DeviceState.Running; var targetState = DeviceState.Offline; var device = new CNCDevice { Id = deviceId, Name = "Test Device" }; _mockDeviceRepository.Setup(repo => repo.GetByIdAsync(deviceId)) .ReturnsAsync(device); // Initialize device state await _deviceStateMachine.ForceStateAsync(deviceId, fromState, "Test setup"); // Act var result = await _deviceStateMachine.TransitionToStateAsync(deviceId, targetState); // Assert Assert.False(result.Success); Assert.Contains($"Cannot transition from {fromState} to {targetState}", result.Message); } [Fact] public async Task TriggerEventAsync_ValidEvent_TransitionsState() { // Arrange var deviceId = 1; var deviceEvent = DeviceEvent.Start; var targetState = DeviceState.Running; var device = new CNCDevice { Id = deviceId, Name = "Test Device", EnableProduction = true }; var currentStatus = new DeviceCurrentStatus { DeviceId = deviceId, Status = DeviceStatus.Online, Runtime = TimeSpan.Zero }; _mockDeviceRepository.Setup(repo => repo.GetByIdAsync(deviceId)) .ReturnsAsync(device); _mockCollectionService.Setup(service => service.GetDeviceCurrentStatusAsync(deviceId)) .ReturnsAsync(currentStatus); _mockDeviceRepository.Setup(repo => repo.UpdateDeviceAsync(device)) .Returns(Task.CompletedTask); // Initialize device state await _deviceStateMachine.ForceStateAsync(deviceId, DeviceState.Idle, "Test setup"); // Act var result = await _deviceStateMachine.TriggerEventAsync(deviceId, deviceEvent); // Assert Assert.True(result.Success); Assert.Equal(DeviceState.Idle, result.FromState); Assert.Equal(targetState, result.ToState); } [Fact] public async Task TriggerEventAsync_InvalidEvent_ReturnsFailure() { // Arrange var deviceId = 1; var deviceEvent = DeviceEvent.Stop; // Cannot stop when not running var device = new CNCDevice { Id = deviceId, Name = "Test Device" }; _mockDeviceRepository.Setup(repo => repo.GetByIdAsync(deviceId)) .ReturnsAsync(device); // Initialize device state await _deviceStateMachine.ForceStateAsync(deviceId, DeviceState.Idle, "Test setup"); // Act var result = await _deviceStateMachine.TriggerEventAsync(deviceId, deviceEvent); // Assert Assert.False(result.Success); Assert.Contains($"No event handler configured for {deviceEvent} in state DeviceState.Idle", result.Message); } [Fact] public async Task ForceStateAsync_ValidForce_TransitionsState() { // Arrange var deviceId = 1; var targetState = DeviceState.Running; var reason = "Emergency override"; var device = new CNCDevice { Id = deviceId, Name = "Test Device" }; _mockDeviceRepository.Setup(repo => repo.GetByIdAsync(deviceId)) .ReturnsAsync(device); // Act var result = await _deviceStateMachine.ForceStateAsync(deviceId, targetState, reason); // Assert Assert.True(result.Success); Assert.Equal($"Forced state transition to {targetState}: {reason}", result.Message); } [Fact] public async Task ValidateStateAsync_ValidState_ReturnsValid() { // Arrange var deviceId = 1; var device = new CNCDevice { Id = deviceId, Name = "Test Device", Status = DeviceStatus.Idle, EnableProduction = true }; var currentStatus = new DeviceCurrentStatus { DeviceId = deviceId, Status = DeviceStatus.Idle, Runtime = TimeSpan.FromHours(1) }; _mockDeviceRepository.Setup(repo => repo.GetByIdAsync(deviceId)) .ReturnsAsync(device); _mockCollectionService.Setup(service => service.GetDeviceCurrentStatusAsync(deviceId)) .ReturnsAsync(currentStatus); // Initialize device state await _deviceStateMachine.ForceStateAsync(deviceId, DeviceState.Idle, "Test setup"); // Act var result = await _deviceStateMachine.ValidateStateAsync(deviceId); // Assert Assert.True(result.IsValid); Assert.Equal(DeviceState.Idle, result.CurrentState); Assert.Empty(result.Issues); } [Fact] public async Task ValidateStateAsync_StateMismatch_ReturnsInvalid() { // Arrange var deviceId = 1; var device = new CNCDevice { Id = deviceId, Name = "Test Device", Status = DeviceStatus.Running, // Status doesn't match state EnableProduction = true }; var currentStatus = new DeviceCurrentStatus { DeviceId = deviceId, Status = DeviceStatus.Running, Runtime = TimeSpan.FromHours(1) }; _mockDeviceRepository.Setup(repo => repo.GetByIdAsync(deviceId)) .ReturnsAsync(device); _mockCollectionService.Setup(service => service.GetDeviceCurrentStatusAsync(deviceId)) .ReturnsAsync(currentStatus); // Initialize device state as Idle but status is Running await _deviceStateMachine.ForceStateAsync(deviceId, DeviceState.Idle, "Test setup"); // Act var result = await _deviceStateMachine.ValidateStateAsync(deviceId); // Assert Assert.False(result.IsValid); Assert.Contains("State mismatch", result.Issues.First()); } [Fact] public void RegisterStateHandler_ValidHandler_RegistersHandler() { // Arrange var handlerMock = new Mock>(); // Act _deviceStateMachine.RegisterStateHandler((deviceId, fromState, toState) => handlerMock.Object(deviceId, fromState, toState)); // Assert // This test would need to verify the handler was registered // For now, we'll just ensure it doesn't throw Assert.True(true); } [Fact] public void UnregisterStateHandler_ValidHandler_UnregistersHandler() { // Arrange var handlerMock = new Mock>(); // Act _deviceStateMachine.RegisterStateHandler((deviceId, fromState, toState) => handlerMock.Object(deviceId, fromState, toState)); _deviceStateMachine.UnregisterStateHandler((deviceId, fromState, toState) => handlerMock.Object(deviceId, fromState, toState)); // Assert // This test would need to verify the handler was unregistered // For now, we'll just ensure it doesn't throw Assert.True(true); } [Fact] public async Task GetStateHistoryAsync_DeviceWithHistory_ReturnsHistory() { // Arrange var deviceId = 1; var history = new List { new DeviceStateHistory { DeviceId = deviceId, State = DeviceState.Idle, ChangedAt = DateTime.Now.AddHours(-2) }, new DeviceStateHistory { DeviceId = deviceId, State = DeviceState.Running, ChangedAt = DateTime.Now.AddHours(-1) } }; _mockDeviceRepository.Setup(repo => repo.GetDeviceStateHistoryAsync(deviceId, null, null)) .ReturnsAsync(history); // Initialize current state await _deviceStateMachine.ForceStateAsync(deviceId, DeviceState.Stopped, "Test setup"); // Act var result = await _deviceStateMachine.GetStateHistoryAsync(deviceId); // Assert Assert.Equal(3, result.Count); // History entries + current state Assert.Equal(DeviceState.Idle, result[0].State); Assert.Equal(DeviceState.Running, result[1].State); Assert.Equal(DeviceState.Stopped, result[2].State); } [Fact] public async Task GetStateStatisticsAsync_DeviceWithHistory_ReturnsStatistics() { // Arrange var deviceId = 1; var history = new List { new DeviceStateHistory { DeviceId = deviceId, State = DeviceState.Idle, ChangedAt = DateTime.Now.AddHours(-2) }, new DeviceStateHistory { DeviceId = deviceId, State = DeviceState.Running, ChangedAt = DateTime.Now.AddHours(-1) } }; _mockDeviceRepository.Setup(repo => repo.GetDeviceStateHistoryAsync(deviceId, null, null)) .ReturnsAsync(history); // Initialize current state await _deviceStateMachine.ForceStateAsync(deviceId, DeviceState.Stopped, "Test setup"); // Act var result = await _deviceStateMachine.GetStateStatisticsAsync(deviceId); // Assert Assert.NotNull(result); Assert.Equal(deviceId, result.DeviceId); Assert.True(result.StateTransitions > 0); Assert.True(result.StateDurations.ContainsKey(DeviceState.Idle)); Assert.True(result.StateDurations.ContainsKey(DeviceState.Running)); Assert.True(result.StateDurations.ContainsKey(DeviceState.Stopped)); Assert.True(result.StateCounts.ContainsKey(DeviceState.Idle)); Assert.True(result.StateCounts.ContainsKey(DeviceState.Running)); Assert.True(result.StateCounts.ContainsKey(DeviceState.Stopped)); } [Fact] public async Task GetStateStatisticsAsync_WithDateFilter_ReturnsFilteredStatistics() { // Arrange var deviceId = 1; var filterStartDate = DateTime.Now.AddDays(-1); var filterEndDate = DateTime.Now; var history = new List { new DeviceStateHistory { DeviceId = deviceId, State = DeviceState.Idle, ChangedAt = DateTime.Now.AddDays(-2) }, // Outside filter new DeviceStateHistory { DeviceId = deviceId, State = DeviceState.Running, ChangedAt = DateTime.Now.AddHours(-12) }, // Within filter new DeviceStateHistory { DeviceId = deviceId, State = DeviceState.Stopped, ChangedAt = DateTime.Now.AddHours(-6) } // Within filter }; _mockDeviceRepository.Setup(repo => repo.GetDeviceStateHistoryAsync(deviceId, filterStartDate, filterEndDate)) .ReturnsAsync(history); // Initialize current state await _deviceStateMachine.ForceStateAsync(deviceId, DeviceState.Stopped, "Test setup"); // Act var result = await _deviceStateMachine.GetStateStatisticsAsync(deviceId, filterStartDate, filterEndDate); // Assert Assert.NotNull(result); Assert.Equal(deviceId, result.DeviceId); Assert.Equal(filterStartDate, result.PeriodStart); Assert.Equal(filterEndDate, result.PeriodEnd); } [Fact] public async Task TranslateStatusToDeviceState_ValidStatus_ReturnsState() { // Arrange var currentStatus = new DeviceCurrentStatus { Status = DeviceStatus.Running }; // Act // This method would need to be made public or tested through another method // For now, we'll test the translation logic through a state transition var device = new CNCDevice { Id = 1, Name = "Test Device" }; _mockDeviceRepository.Setup(repo => repo.GetByIdAsync(1)) .ReturnsAsync(device); _mockCollectionService.Setup(service => service.GetDeviceCurrentStatusAsync(1)) .ReturnsAsync(currentStatus); // Initialize device state and check if it matches the status await _deviceStateMachine.ForceStateAsync(1, DeviceState.Unknown, "Test setup"); var result = _deviceStateMachine.GetCurrentState(1); // Assert // The state should be Running based on the status Assert.Equal(DeviceState.Unknown, result); // Initially unknown until transition } [Fact] public async Task TranslateStateToDeviceStatus_ValidState_ReturnsStatus() { // Arrange var state = DeviceState.Running; // Act // This would test the reverse translation // We can test it by verifying the device status is updated correctly var device = new CNCDevice { Id = 1, Name = "Test Device" }; _mockDeviceRepository.Setup(repo => repo.GetByIdAsync(1)) .ReturnsAsync(device); _mockDeviceRepository.Setup(repo => repo.UpdateDeviceAsync(device)) .Returns(Task.CompletedTask); // Transition to Running state and check if device status is updated await _deviceStateMachine.TransitionToStateAsync(1, state); // Assert // The device status should be updated to Running Assert.Equal(DeviceStatus.Running, device.Status); } [Fact] public async Task HandleInvalidStateAsync_InvalidState_TransitionsToSafeState() { // Arrange var deviceId = 1; var validationResult = new DeviceValidationResult { DeviceId = deviceId, IsValid = false, Issues = new List { "State mismatch" }, CurrentState = DeviceState.Error }; var device = new CNCDevice { Id = deviceId, Name = "Test Device" }; _mockDeviceRepository.Setup(repo => repo.GetByIdAsync(deviceId)) .ReturnsAsync(device); // Act // This method is private, so we test through ValidateStateAsync which calls it await _deviceStateMachine.ForceStateAsync(deviceId, DeviceState.Error, "Test setup"); var result = await _deviceStateMachine.ValidateStateAsync(deviceId); // The state machine should handle the invalid state // We expect the state to remain Error since it's already a safe state for errors Assert.Equal(DeviceState.Error, _deviceStateMachine.GetCurrentState(deviceId)); } [Fact] public async Task StartAsync_InitializesDeviceStates() { // Arrange var devices = new List { new CNCDevice { Id = 1, Name = "Device 1" }, new CNCDevice { Id = 2, Name = "Device 2" } }; var device1Status = new DeviceCurrentStatus { DeviceId = 1, Status = DeviceStatus.Online, Runtime = TimeSpan.Zero }; var device2Status = new DeviceCurrentStatus { DeviceId = 2, Status = DeviceStatus.Running, Runtime = TimeSpan.FromHours(1) }; _mockDeviceRepository.Setup(repo => repo.GetAllDevicesAsync()) .ReturnsAsync(devices); _mockCollectionService.Setup(service => service.GetDeviceCurrentStatusAsync(1)) .ReturnsAsync(device1Status); _mockCollectionService.Setup(service => service.GetDeviceCurrentStatusAsync(2)) .ReturnsAsync(device2Status); // Act await _deviceStateMachine.StartAsync(CancellationToken.None); // Assert // Allow time for async initialization await Task.Delay(100); // Check if states are initialized based on device status Assert.Equal(DeviceState.Online, _deviceStateMachine.GetCurrentState(1)); Assert.Equal(DeviceState.Running, _deviceStateMachine.GetCurrentState(2)); } } }