using System.Security.Claims;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Authorization;
using Microsoft.AspNetCore.Components.Web;
using Microsoft.AspNetCore.Components.Web.HtmlRendering;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using ZB.MOM.WW.Auth.AspNetCore;
using ZB.MOM.WW.MxGateway.Server.Dashboard;
using ZB.MOM.WW.MxGateway.Server.Dashboard.Components.Layout;
using ZB.MOM.WW.Secrets.Ui;

namespace ZB.MOM.WW.MxGateway.Tests.Dashboard;

/// <summary>
/// Renders <see cref="MainLayout"/> and asserts whether the side rail emits the Secrets link.
/// </summary>
/// <remarks>
/// <para>
/// The policy-level tests in <c>SecretsNavGateTests</c> are not sufficient on their own, and the
/// reason is worth stating because it is easy to miss: they would stay green if the
/// <c>&lt;AuthorizeView&gt;</c> were deleted outright. They prove the policy decides correctly, not
/// that the rail asks it. The wiring is the part this change actually introduced, so it is the part
/// that needs its own evidence.
/// </para>
/// <para>
/// The load-bearing assertion is the NEGATIVE one. "An Administrator sees the link" is identical to
/// the behaviour before the gate existed, so it cannot distinguish a working gate from an inert one.
/// Only a principal without <c>secrets:manage</c> failing to see the item proves a gate is there at
/// all — which is why <see cref="Rail_OmitsSecretsLink_ForViewer"/> is the test that matters and the
/// Administrator case is its control.
/// </para>
/// <para>
/// Uses the framework's static <see cref="HtmlRenderer"/> rather than a component-testing package:
/// no new dependency, and static rendering is enough because the assertion is about markup the
/// server emits, not about interactivity.
/// </para>
/// </remarks>
public sealed class SecretsNavRenderTests
{
    private const string SecretsHref = "/admin/secrets";

    /// <summary>
    /// The gate's proof. A Viewer holds a real, authenticated identity and still must not be offered
    /// the link, because the page would refuse them.
    /// </summary>
    /// <returns>A task that represents the asynchronous operation.</returns>
    [Fact]
    public async Task Rail_OmitsSecretsLink_ForViewer()
    {
        string html = await RenderRailAsync(DashboardRoles.Viewer);

        Assert.DoesNotContain(SecretsHref, html, StringComparison.Ordinal);
    }

    /// <summary>Anonymous callers (the read-only localhost path) are likewise not offered it.</summary>
    /// <returns>A task that represents the asynchronous operation.</returns>
    [Fact]
    public async Task Rail_OmitsSecretsLink_ForAnonymous()
    {
        string html = await RenderRailAsync();

        Assert.DoesNotContain(SecretsHref, html, StringComparison.Ordinal);
    }

    /// <summary>
    /// The control for the two negatives. Without this, a rail that rendered no nav at all — a
    /// broken layout, a throwing component swallowed somewhere — would satisfy both absence
    /// assertions and the suite would report a working gate over a blank page. The sibling
    /// assertions on the always-present items are what make the absence above mean "gated" rather
    /// than "nothing rendered".
    /// </summary>
    /// <returns>A task that represents the asynchronous operation.</returns>
    [Fact]
    public async Task Rail_EmitsSecretsLink_ForAdministrator()
    {
        string html = await RenderRailAsync(DashboardRoles.Admin);

        Assert.Contains(SecretsHref, html, StringComparison.Ordinal);
        Assert.Contains("/apikeys", html, StringComparison.Ordinal);
    }

    /// <summary>
    /// Pins the deliberate asymmetry: the ungated sibling stays visible to a Viewer. If someone
    /// later "fixes the inconsistency" by wrapping the API Keys item in the same AuthorizeView,
    /// this fails — that page renders read-only for Viewers, so hiding its link would remove
    /// legitimate access.
    /// </summary>
    /// <returns>A task that represents the asynchronous operation.</returns>
    [Fact]
    public async Task Rail_StillEmitsApiKeysLink_ForViewer()
    {
        string html = await RenderRailAsync(DashboardRoles.Viewer);

        Assert.Contains("/apikeys", html, StringComparison.Ordinal);
    }

    private static async Task<string> RenderRailAsync(params string[] roles)
    {
        var services = new ServiceCollection();
        services.AddLogging();
        services.AddAuthorization(options => options.AddSecretsAuthorization());
        services.AddCascadingAuthenticationState();
        services.AddSingleton<AuthenticationStateProvider>(new StubAuthenticationStateProvider(roles));
        services.AddSingleton<NavigationManager, StubNavigationManager>();

        await using ServiceProvider provider = services.BuildServiceProvider();
        await using var renderer = new HtmlRenderer(
            provider,
            provider.GetRequiredService<ILoggerFactory>());

        return await renderer.Dispatcher.InvokeAsync(async () =>
        {
            HtmlRootComponent output = await renderer.RenderComponentAsync<MainLayout>();
            return output.ToHtmlString();
        });
    }

    // Supplies the authentication state the rail's AuthorizeView reads. An empty role list yields an
    // unauthenticated principal; otherwise the identity carries an authentication type, without
    // which every policy would fail for the wrong reason and the negative assertions would pass
    // vacuously.
    private sealed class StubAuthenticationStateProvider(string[] roles) : AuthenticationStateProvider
    {
        public override Task<AuthenticationState> GetAuthenticationStateAsync()
        {
            ClaimsIdentity identity = roles.Length == 0
                ? new ClaimsIdentity()
                : new ClaimsIdentity(
                    roles.Select(role => new Claim(ZbClaimTypes.Role, role)),
                    authenticationType: "Test",
                    nameType: ZbClaimTypes.Name,
                    roleType: ZbClaimTypes.Role);

            return Task.FromResult(new AuthenticationState(new ClaimsPrincipal(identity)));
        }
    }

    // NavLink resolves hrefs against the current URI, so the rail needs a NavigationManager even
    // under static rendering.
    private sealed class StubNavigationManager : NavigationManager
    {
        public StubNavigationManager() => Initialize("https://localhost/", "https://localhost/");
    }
}
