using System; using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; using Junction.App.ViewModels; using Junction.Core.Monitoring; using Junction.Domain; using Junction.Domain.Models; using Junction.Domain.Persistence; using Microsoft.Extensions.Logging.Abstractions; using Moq; using Xunit; namespace Junction.Tests.Unit { /// /// Trend-load behaviour of the detail VM: after a load, each row whose history parses as numeric /// gets a populated (ascending order) with /// HasTrend true; a row with non-numeric history stays null / HasTrend false. /// Fixture: Moq'd + + null logger. /// The fire-and-forget trend load is awaited via the VM's internal TrendLoadTask. /// public class MachineDetailViewModelTests { private const string NumericItemId = "temp"; private const string TextItemId = "avail"; private static readonly Guid MachineId = Guid.NewGuid(); private static MachineSnapshot SnapshotWithBothItems() { var now = DateTimeOffset.UtcNow; var items = new List { new DataItem(NumericItemId, "Temperature", "12", "Sample", now), new DataItem(TextItemId, "Availability", "AVAILABLE", "Event", now), }; return new MachineSnapshot(MachineId, now, ConnectionState.Connected, items); } private static IReadOnlyList NumericHistory() { var t = DateTimeOffset.UtcNow; return new List { new HistoryPoint(NumericItemId, "10", t.AddSeconds(-30)), new HistoryPoint(NumericItemId, "12.5", t.AddSeconds(-20)), new HistoryPoint(NumericItemId, "11", t.AddSeconds(-10)), }; } private static IReadOnlyList TextHistory() { var t = DateTimeOffset.UtcNow; return new List { new HistoryPoint(TextItemId, "AVAILABLE", t.AddSeconds(-20)), new HistoryPoint(TextItemId, "UNAVAILABLE", t.AddSeconds(-10)), }; } private static MachineDetailViewModel BuildVm( Mock repo, Mock monitor) { var vm = new MachineDetailViewModel( repo.Object, monitor.Object, NullLogger.Instance); vm.Initialize(MachineId, "VMC Sim (mock)"); return vm; } private static Mock MonitorWithSnapshot(MachineSnapshot snapshot) { var monitor = new Mock(); var cache = new Dictionary { [MachineId] = snapshot }; monitor.SetupGet(m => m.LatestSnapshots) .Returns((IReadOnlyDictionary)cache); return monitor; } private static DataItemRowViewModel Row(MachineDetailViewModel vm, string id) => vm.Items.First(r => r.Id == id); [Fact] public async Task LoadAsync_NumericHistory_PopulatesTrendAscending() { var snapshot = SnapshotWithBothItems(); var repo = new Mock(); repo.Setup(r => r.GetByIdAsync(MachineId, It.IsAny())) .ReturnsAsync(Result.Ok(new Machine(MachineId, "VMC", "mtconnect", null, TimeSpan.FromSeconds(2)))); repo.Setup(r => r.GetHistoryAsync(MachineId, NumericItemId, It.IsAny(), It.IsAny(), It.IsAny())) .ReturnsAsync(Result>.Ok(NumericHistory())); repo.Setup(r => r.GetHistoryAsync(MachineId, TextItemId, It.IsAny(), It.IsAny(), It.IsAny())) .ReturnsAsync(Result>.Ok(TextHistory())); var monitor = MonitorWithSnapshot(snapshot); var vm = BuildVm(repo, monitor); await vm.LoadAsync(); if (vm.TrendLoadTask != null) { await vm.TrendLoadTask; } var numericRow = Row(vm, NumericItemId); Assert.True(numericRow.HasTrend); Assert.NotNull(numericRow.Trend); Assert.Equal(new double[] { 10, 12.5, 11 }, numericRow.Trend!.ToArray()); } [Fact] public async Task LoadAsync_NonNumericHistory_LeavesTrendNull() { var snapshot = SnapshotWithBothItems(); var repo = new Mock(); repo.Setup(r => r.GetByIdAsync(MachineId, It.IsAny())) .ReturnsAsync(Result.Ok(new Machine(MachineId, "VMC", "mtconnect", null, TimeSpan.FromSeconds(2)))); repo.Setup(r => r.GetHistoryAsync(MachineId, NumericItemId, It.IsAny(), It.IsAny(), It.IsAny())) .ReturnsAsync(Result>.Ok(NumericHistory())); repo.Setup(r => r.GetHistoryAsync(MachineId, TextItemId, It.IsAny(), It.IsAny(), It.IsAny())) .ReturnsAsync(Result>.Ok(TextHistory())); var monitor = MonitorWithSnapshot(snapshot); var vm = BuildVm(repo, monitor); await vm.LoadAsync(); if (vm.TrendLoadTask != null) { await vm.TrendLoadTask; } var textRow = Row(vm, TextItemId); Assert.False(textRow.HasTrend); Assert.Null(textRow.Trend); } [Fact] public async Task LoadAsync_HistoryFailureForOneItem_DoesNotThrowOrBlockOthers() { var snapshot = SnapshotWithBothItems(); var repo = new Mock(); repo.Setup(r => r.GetByIdAsync(MachineId, It.IsAny())) .ReturnsAsync(Result.Ok(new Machine(MachineId, "VMC", "mtconnect", null, TimeSpan.FromSeconds(2)))); // Numeric item succeeds; text item fails hard. repo.Setup(r => r.GetHistoryAsync(MachineId, NumericItemId, It.IsAny(), It.IsAny(), It.IsAny())) .ReturnsAsync(Result>.Ok(NumericHistory())); repo.Setup(r => r.GetHistoryAsync(MachineId, TextItemId, It.IsAny(), It.IsAny(), It.IsAny())) .ThrowsAsync(new InvalidOperationException("boom")); var monitor = MonitorWithSnapshot(snapshot); var vm = BuildVm(repo, monitor); await vm.LoadAsync(); if (vm.TrendLoadTask != null) { await vm.TrendLoadTask; } // The failing item leaves the successful item's trend intact. Assert.True(Row(vm, NumericItemId).HasTrend); Assert.False(Row(vm, TextItemId).HasTrend); } } }