using System.Security.Claims;
using Bunit;
using Microsoft.AspNetCore.Components.Authorization;
using Microsoft.AspNetCore.Components.Web;
using Microsoft.Extensions.DependencyInjection;
using NSubstitute;
using ZB.MOM.WW.ScadaBridge.CentralUI.Services;
using ZB.MOM.WW.ScadaBridge.Commons.Entities.Sites;
using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Repositories;
using ZB.MOM.WW.ScadaBridge.Commons.Messages.Streaming;
using ZB.MOM.WW.ScadaBridge.Commons.Types.Enums;
using ZB.MOM.WW.ScadaBridge.Security;
using AlarmSummaryPage = ZB.MOM.WW.ScadaBridge.CentralUI.Components.Pages.Monitoring.AlarmSummary;
namespace ZB.MOM.WW.ScadaBridge.CentralUI.Tests.Monitoring;
///
/// bUnit rendering tests for the Operator Alarm Summary page — arch-review UA6
/// (sortable-header accessibility). The sortable column headers must be keyboard
/// operable: they carry tabindex="0", expose their sort direction via
/// aria-sort (ascending/descending on the active column,
/// none otherwise), and toggle the sort when Enter is pressed on them —
/// mirroring the existing pointer-click sort.
///
public class AlarmSummaryRenderTests : BunitContext
{
private readonly IAlarmSummaryService _summary = Substitute.For();
private readonly ISiteRepository _siteRepo = Substitute.For();
public AlarmSummaryRenderTests()
{
// Two rows whose default sort (severity DESC) orders Zeta (900) before
// Alpha (500); an ascending sort by instance name flips them.
var rows = new List
{
new("Zeta", new AlarmStateChanged("Zeta", "Z-alarm", AlarmState.Active, 900, DateTimeOffset.UtcNow)),
new("Alpha", new AlarmStateChanged("Alpha", "A-alarm", AlarmState.Active, 500, DateTimeOffset.UtcNow)),
};
_summary.GetSiteAlarmsAsync(Arg.Any(), Arg.Any())
.Returns(Task.FromResult(new AlarmSummaryResult(rows, Array.Empty())));
_summary.ComputeRollup(Arg.Any>())
.Returns(new AlarmRollup(2, 900, 0, new Dictionary()));
Services.AddSingleton(_summary);
_siteRepo.GetAllSitesAsync(Arg.Any())
.Returns(Task.FromResult>(new List { new("Site 1", "site1") { Id = 1 } }));
Services.AddSingleton(_siteRepo);
var claims = new[]
{
new Claim(JwtTokenService.UsernameClaimType, "tester"),
new Claim(JwtTokenService.RoleClaimType, "Administrator"),
};
var user = new ClaimsPrincipal(new ClaimsIdentity(claims, "TestAuth"));
Services.AddSingleton(new TestAuthStateProvider(user));
Services.AddAuthorizationCore();
}
// Renders the page and drives the site picker so the alarm table appears.
private IRenderedComponent RenderWithSiteSelected()
{
var cut = Render();
cut.Find("[data-test='alarm-summary-site']").Change("1");
cut.WaitForAssertion(() =>
Assert.NotEmpty(cut.FindAll("tr[data-test='alarm-summary-row']")));
return cut;
}
[Fact]
public void SortableHeaders_CarryTabindexAndAriaSort()
{
var cut = RenderWithSiteSelected();
var headers = cut.FindAll("thead th");
// 0=Instance (sortable), 1=Alarm (sortable), 2=State/Kind (not sortable), 3=Severity (sortable, active).
var instance = headers[0];
var stateKind = headers[2];
var severity = headers[3];
Assert.Equal("0", instance.GetAttribute("tabindex"));
Assert.Equal("0", severity.GetAttribute("tabindex"));
// Default sort is severity descending.
Assert.Equal("descending", severity.GetAttribute("aria-sort"));
Assert.Equal("none", instance.GetAttribute("aria-sort"));
// The non-sortable column is not keyboard-focusable and carries no aria-sort.
Assert.Null(stateKind.GetAttribute("tabindex"));
Assert.Null(stateKind.GetAttribute("aria-sort"));
}
[Fact]
public void EnterOnInstanceHeader_FlipsRowOrder()
{
var cut = RenderWithSiteSelected();
// Default severity-descending order → Zeta (900) first.
Assert.Equal("Zeta", FirstRowInstance(cut));
// Enter on the Instance header sorts ascending by instance → Alpha first.
cut.FindAll("thead th")[0].TriggerEvent("onkeydown", new KeyboardEventArgs { Key = "Enter" });
cut.WaitForAssertion(() => Assert.Equal("Alpha", FirstRowInstance(cut)));
// aria-sort now reflects the active ascending instance column.
Assert.Equal("ascending", cut.FindAll("thead th")[0].GetAttribute("aria-sort"));
}
private static string FirstRowInstance(IRenderedComponent cut) =>
cut.FindAll("tr[data-test='alarm-summary-row']")[0]
.QuerySelector("td")!.TextContent.Trim();
}