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);
}
}
}