using Bunit; using Bunit.TestDoubles; using Microsoft.AspNetCore.Components; using Microsoft.Extensions.DependencyInjection; using ScadaLink.CentralUI.Components.Health; using ScadaLink.Commons.Messages.Audit; namespace ScadaLink.CentralUI.Tests.Components.Health; /// /// bUnit tests for (Site Call Audit #22, Task 7). /// The component renders three Bootstrap-card tiles — Buffered, Stuck, Parked — /// from a single snapshot. The tests pin: /// /// /// Three-tile render contract (data-test attributes for stable selectors). /// Tile values render the snapshot's counters. /// Threshold borders fire correctly — danger on Parked > 0, warning /// on Stuck > 0, none when those counts are zero, none on Buffered. /// Unavailable snapshot renders em dashes plus the error message. /// Tile clicks navigate to the correct pre-filtered Site Calls report URL. /// /// public class SiteCallKpiTilesTests : BunitContext { private static SiteCallKpiResponse MakeSnapshot(int buffered, int parked, int stuck) => new( CorrelationId: "k", Success: true, ErrorMessage: null, BufferedCount: buffered, ParkedCount: parked, FailedLastInterval: 0, DeliveredLastInterval: 0, OldestPendingAge: null, StuckCount: stuck); [Fact] public void Renders_ThreeTiles_FromSnapshot() { var cut = Render(p => p .Add(c => c.Snapshot, MakeSnapshot(buffered: 120, parked: 3, stuck: 7)) .Add(c => c.IsAvailable, true)); // Three stable data-test selectors — the contract for both these tests // and any future Playwright sweep. Assert.Contains("data-test=\"site-call-kpi-buffered\"", cut.Markup); Assert.Contains("data-test=\"site-call-kpi-stuck\"", cut.Markup); Assert.Contains("data-test=\"site-call-kpi-parked\"", cut.Markup); // Tile values render the snapshot's counters. Assert.Contains(">120<", cut.Markup); // buffered Assert.Contains(">7<", cut.Markup); // stuck Assert.Contains(">3<", cut.Markup); // parked } [Fact] public void UnavailableSnapshot_RendersEmDashes_AndErrorMessage() { var cut = Render(p => p .Add(c => c.Snapshot, (SiteCallKpiResponse?)null) .Add(c => c.IsAvailable, false) .Add(c => c.ErrorMessage, "site call repository unavailable")); // All three tiles show em dashes — em dash (U+2014) "—" must appear. Assert.Contains("—", cut.Markup); // Inline error message renders below. Assert.Contains("Site Call KPIs unavailable", cut.Markup); Assert.Contains("site call repository unavailable", cut.Markup); } [Fact] public void ParkedTile_GetsDangerBorder_WhenParkedAboveZero() { var cut = Render(p => p .Add(c => c.Snapshot, MakeSnapshot(buffered: 0, parked: 4, stuck: 0)) .Add(c => c.IsAvailable, true)); var tile = cut.Find("[data-test=\"site-call-kpi-parked\"]"); Assert.Contains("border-danger", tile.GetAttribute("class") ?? string.Empty); } [Fact] public void ParkedTile_NoDangerBorder_WhenParkedZero() { var cut = Render(p => p .Add(c => c.Snapshot, MakeSnapshot(buffered: 9, parked: 0, stuck: 0)) .Add(c => c.IsAvailable, true)); var tile = cut.Find("[data-test=\"site-call-kpi-parked\"]"); Assert.DoesNotContain("border-danger", tile.GetAttribute("class") ?? string.Empty); } [Fact] public void StuckTile_GetsWarningBorder_WhenStuckAboveZero() { var cut = Render(p => p .Add(c => c.Snapshot, MakeSnapshot(buffered: 0, parked: 0, stuck: 6)) .Add(c => c.IsAvailable, true)); var tile = cut.Find("[data-test=\"site-call-kpi-stuck\"]"); Assert.Contains("border-warning", tile.GetAttribute("class") ?? string.Empty); // Warning, not danger — Stuck is the softer signal. Assert.DoesNotContain("border-danger", tile.GetAttribute("class") ?? string.Empty); } [Fact] public void StuckTile_NoWarningBorder_WhenStuckZero() { var cut = Render(p => p .Add(c => c.Snapshot, MakeSnapshot(buffered: 9, parked: 0, stuck: 0)) .Add(c => c.IsAvailable, true)); var tile = cut.Find("[data-test=\"site-call-kpi-stuck\"]"); Assert.DoesNotContain("border-warning", tile.GetAttribute("class") ?? string.Empty); } [Fact] public void BufferedTile_HasNoThresholdBorder_EvenWithHighCount() { // A non-zero buffer is normal operation — the Buffered tile is a plain // count tile and never gets a danger/warning border. var cut = Render(p => p .Add(c => c.Snapshot, MakeSnapshot(buffered: 5000, parked: 0, stuck: 0)) .Add(c => c.IsAvailable, true)); var tile = cut.Find("[data-test=\"site-call-kpi-buffered\"]"); var cls = tile.GetAttribute("class") ?? string.Empty; Assert.DoesNotContain("border-danger", cls); Assert.DoesNotContain("border-warning", cls); } [Fact] public void BufferedTile_Click_NavigatesToUnfilteredSiteCallsReport() { var cut = Render(p => p .Add(c => c.Snapshot, MakeSnapshot(buffered: 50, parked: 0, stuck: 0)) .Add(c => c.IsAvailable, true)); var nav = (BunitNavigationManager)Services.GetRequiredService(); var tile = cut.Find("[data-test=\"site-call-kpi-buffered\"]"); tile.Click(); // Unfiltered /site-calls/report — no query string. Assert.EndsWith("/site-calls/report", nav.Uri); } [Fact] public void StuckTile_Click_NavigatesToSiteCallsReport_WithStuckFilter() { var cut = Render(p => p .Add(c => c.Snapshot, MakeSnapshot(buffered: 0, parked: 0, stuck: 6)) .Add(c => c.IsAvailable, true)); var nav = (BunitNavigationManager)Services.GetRequiredService(); var tile = cut.Find("[data-test=\"site-call-kpi-stuck\"]"); tile.Click(); // Spec: Stuck tile drills into the report's "stuck only" filter. Assert.Contains("/site-calls/report?stuck=true", nav.Uri); } [Fact] public void ParkedTile_Click_NavigatesToSiteCallsReport_WithParkedStatusFilter() { var cut = Render(p => p .Add(c => c.Snapshot, MakeSnapshot(buffered: 0, parked: 4, stuck: 0)) .Add(c => c.IsAvailable, true)); var nav = (BunitNavigationManager)Services.GetRequiredService(); var tile = cut.Find("[data-test=\"site-call-kpi-parked\"]"); tile.Click(); // Spec: Parked tile drills into ?status=Parked. Assert.Contains("/site-calls/report?status=Parked", nav.Uri); } }