using System.Reflection;
using System.Security.Claims;
using Bunit;
using Microsoft.AspNetCore.Components.Authorization;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using NSubstitute;
using ZB.MOM.WW.ScadaBridge.CentralUI.Auth;
using ZB.MOM.WW.ScadaBridge.CentralUI.Components.Shared;
using ZB.MOM.WW.ScadaBridge.Commons.Entities.Sites;
using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Repositories;
using ZB.MOM.WW.ScadaBridge.Communication;
using AuditLogPage = ZB.MOM.WW.ScadaBridge.CentralUI.Components.Pages.Audit.ConfigurationAuditLog;
using ParkedMessagesPage = ZB.MOM.WW.ScadaBridge.CentralUI.Components.Pages.Monitoring.ParkedMessages;
namespace ZB.MOM.WW.ScadaBridge.CentralUI.Tests.Pages;
///
/// The detail modal/drawer on these surfaces is gated on a held id — user intent —
/// rather than on the row still resolving, so that a data refresh can never unmount
/// the subtree mid-render and dispose its event handlers (see
/// NotificationReportDetailModalTests.Modal_StaysOpen_WhenItsRowLeavesThePage).
///
/// That makes it the surface's own job to clear the id on navigation actions that
/// invalidate page-scoped state, and the criterion is per-surface:
///
/// • ParkedMessages and ConfigurationAuditLog have NO keyed detail fetch — their
/// modal content resolves from the loaded page alone. An entry paged out of view
/// can never resolve again, so the surface would sit on a permanently empty
/// notice recoverable only by paging back. These must clear on paging.
///
/// • NotificationReport and SiteCallsReport DO fetch detail by id. Their modal
/// still shows real content after the row leaves the page, so they deliberately
/// do NOT clear on paging, and are not covered here.
///
/// Clearing on an explicit navigation action is intent, not a resolve-driven
/// unmount, so it does not reopen the handler-disposal race.
///
public class PageScopedDetailStateTests : BunitContext
{
private static void SetPrivate(object target, string field, object? value) =>
target.GetType()
.GetField(field, BindingFlags.Instance | BindingFlags.NonPublic)!
.SetValue(target, value);
private static object? GetPrivate(object target, string field) =>
target.GetType()
.GetField(field, BindingFlags.Instance | BindingFlags.NonPublic)!
.GetValue(target);
private static Task InvokePrivate(object target, string method, params object[] args) =>
(Task)target.GetType()
.GetMethod(method, BindingFlags.Instance | BindingFlags.NonPublic)!
.Invoke(target, args)!;
private void RegisterCommonServices()
{
var identity = new ClaimsIdentity(
new[] { new Claim(ClaimTypes.Name, "deployer") }, "TestCookie");
var stubAuth = new StubAuthStateProvider(
new AuthenticationState(new ClaimsPrincipal(identity)));
Services.AddSingleton(stubAuth);
Services.AddScoped(_ => new SiteScopeService(stubAuth));
Services.AddScoped();
JSInterop.Mode = JSRuntimeMode.Loose;
}
[Theory]
[InlineData("PrevPage")]
[InlineData("NextPage")]
public async Task ParkedMessages_Paging_ClosesTheDrawer(string pagingMethod)
{
RegisterCommonServices();
var siteRepo = Substitute.For();
siteRepo.GetAllSitesAsync().Returns(new List());
Services.AddSingleton(siteRepo);
Services.AddSingleton(new CommunicationService(
Options.Create(new CommunicationOptions()),
NullLogger.Instance));
var cut = Render();
// Drawer open on some message.
await cut.InvokeAsync(() =>
SetPrivate(cut.Instance, "_drawerMessageId", "msg-being-viewed"));
Assert.Equal("msg-being-viewed", GetPrivate(cut.Instance, "_drawerMessageId"));
await cut.InvokeAsync(() => InvokePrivate(cut.Instance, pagingMethod));
// Paging away invalidates it — this surface has no keyed detail to fall back
// on, so leaving it set would strand the drawer on an empty notice.
Assert.Null(GetPrivate(cut.Instance, "_drawerMessageId"));
}
[Fact]
public async Task ConfigurationAuditLog_Paging_ClosesTheStateModal()
{
RegisterCommonServices();
Services.AddSingleton(Substitute.For());
var cut = Render();
await cut.InvokeAsync(() => SetPrivate(cut.Instance, "_modalEntryId", 4242));
Assert.Equal(4242, GetPrivate(cut.Instance, "_modalEntryId"));
await cut.InvokeAsync(() => InvokePrivate(cut.Instance, "OnPageChanged", 2));
Assert.Null(GetPrivate(cut.Instance, "_modalEntryId"));
}
private sealed class StubAuthStateProvider : AuthenticationStateProvider
{
private readonly AuthenticationState _state;
public StubAuthStateProvider(AuthenticationState state) => _state = state;
public override Task GetAuthenticationStateAsync()
=> Task.FromResult(_state);
}
}