using System; using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; using FluentAssertions; using Newtonsoft.Json.Linq; using Xunit; using ZB.MOM.WW.LmxProxy.Host.Domain; using ZB.MOM.WW.LmxProxy.Host.Health; using HealthCheckService = ZB.MOM.WW.LmxProxy.Host.Health.HealthCheckService; using ZB.MOM.WW.LmxProxy.Host.Metrics; using ZB.MOM.WW.LmxProxy.Host.Status; using ZB.MOM.WW.LmxProxy.Host.Subscriptions; namespace ZB.MOM.WW.LmxProxy.Host.Tests.Status { public class StatusReportServiceTests { private class FakeScadaClient : IScadaClient { public bool IsConnected { get; set; } = true; public ConnectionState ConnectionState { get; set; } = ConnectionState.Connected; public DateTime ConnectedSince => DateTime.UtcNow; public int ReconnectCount => 0; public event EventHandler? ConnectionStateChanged; public Task ConnectAsync(CancellationToken ct = default) => Task.CompletedTask; public Task DisconnectAsync(CancellationToken ct = default) => Task.CompletedTask; public Task ReadAsync(string address, CancellationToken ct = default) => Task.FromResult(Vtq.Good(42.0)); public Task> ReadBatchAsync(IEnumerable addresses, CancellationToken ct = default) => Task.FromResult>(new Dictionary()); public Task WriteAsync(string address, object value, CancellationToken ct = default) => Task.CompletedTask; public Task WriteBatchAsync(IReadOnlyDictionary values, CancellationToken ct = default) => Task.CompletedTask; public Task<(bool flagReached, int elapsedMs)> WriteBatchAndWaitAsync( IReadOnlyDictionary values, string flagTag, object flagValue, int timeoutMs, int pollIntervalMs, CancellationToken ct = default) => Task.FromResult((false, 0)); public Task UnsubscribeByAddressAsync(IEnumerable addresses) => Task.CompletedTask; public Task SubscribeAsync(IEnumerable addresses, Action callback, CancellationToken ct = default) => Task.FromResult(new FakeHandle()); public ValueTask DisposeAsync() => default; internal void FireEvent() => ConnectionStateChanged?.Invoke(this, null!); private class FakeHandle : IAsyncDisposable { public ValueTask DisposeAsync() => default; } } private (StatusReportService svc, PerformanceMetrics pm, SubscriptionManager sm) CreateService( bool connected = true) { var client = new FakeScadaClient { IsConnected = connected, ConnectionState = connected ? ConnectionState.Connected : ConnectionState.Disconnected }; var sm = new SubscriptionManager(client); var pm = new PerformanceMetrics(); var health = new HealthCheckService(client, sm, pm); var svc = new StatusReportService(client, sm, pm, health); return (svc, pm, sm); } [Fact] public async Task GenerateJsonReportAsync_ReturnsCamelCaseJson() { var (svc, pm, sm) = CreateService(); using (pm) using (sm) { var json = await svc.GenerateJsonReportAsync(); json.Should().Contain("\"serviceName\""); json.Should().Contain("\"connection\""); json.Should().Contain("\"isConnected\""); } } [Fact] public async Task GenerateHtmlReportAsync_ContainsAutoRefresh() { var (svc, pm, sm) = CreateService(); using (pm) using (sm) { var html = await svc.GenerateHtmlReportAsync(); html.Should().Contain(""); } } [Fact] public async Task IsHealthyAsync_ReturnsTrueWhenHealthy() { var (svc, pm, sm) = CreateService(connected: true); using (pm) using (sm) { var result = await svc.IsHealthyAsync(); result.Should().BeTrue(); } } [Fact] public async Task IsHealthyAsync_ReturnsFalseWhenUnhealthy() { var (svc, pm, sm) = CreateService(connected: false); using (pm) using (sm) { var result = await svc.IsHealthyAsync(); result.Should().BeFalse(); } } [Fact] public async Task GenerateJsonReportAsync_IncludesPerformanceMetrics() { var (svc, pm, sm) = CreateService(); using (pm) using (sm) { pm.RecordOperation("Read", TimeSpan.FromMilliseconds(15), true); pm.RecordOperation("Write", TimeSpan.FromMilliseconds(25), true); var json = await svc.GenerateJsonReportAsync(); var parsed = JObject.Parse(json); var operations = parsed["performance"]?["operations"]; operations.Should().NotBeNull(); // Newtonsoft CamelCasePropertyNamesContractResolver camelCases dictionary keys operations!["read"].Should().NotBeNull(); operations!["write"].Should().NotBeNull(); ((long)operations!["read"]!["totalCount"]!).Should().Be(1); } } } }