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;

/// <summary>
/// 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.
/// </summary>
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<AuthenticationStateProvider>(stubAuth);
        Services.AddScoped(_ => new SiteScopeService(stubAuth));
        Services.AddScoped<IDialogService, DialogService>();
        JSInterop.Mode = JSRuntimeMode.Loose;
    }

    [Theory]
    [InlineData("PrevPage")]
    [InlineData("NextPage")]
    public async Task ParkedMessages_Paging_ClosesTheDrawer(string pagingMethod)
    {
        RegisterCommonServices();

        var siteRepo = Substitute.For<ISiteRepository>();
        siteRepo.GetAllSitesAsync().Returns(new List<Site>());
        Services.AddSingleton(siteRepo);
        Services.AddSingleton(new CommunicationService(
            Options.Create(new CommunicationOptions()),
            NullLogger<CommunicationService>.Instance));

        var cut = Render<ParkedMessagesPage>();

        // 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<ICentralUiRepository>());

        var cut = Render<AuditLogPage>();

        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<AuthenticationState> GetAuthenticationStateAsync()
            => Task.FromResult(_state);
    }
}
