using System.Security.Claims;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.DependencyInjection;
using ZB.MOM.WW.ScadaBridge.CentralUI.Auth;

namespace ZB.MOM.WW.ScadaBridge.CentralUI.Tests.Auth;

/// <summary>
/// Regression tests for CentralUI-020. The Blazor circuit's
/// <c>CookieAuthenticationStateProvider</c> serves a frozen constructor-time
/// principal, so <c>SessionExpiry</c> could never observe a server-side cookie
/// expiry by polling the auth state. The fix adds <c>GET /auth/ping</c>, an
/// endpoint evaluated per HTTP request (where the cookie middleware re-validates
/// the cookie): it returns 200 while the session is live and 401 once the
/// cookie has lapsed, giving <c>SessionExpiry</c> a real signal to redirect on.
/// </summary>
public class AuthPingEndpointTests
{
    private static IReadOnlyList<RouteEndpoint> BuildEndpoints()
    {
        var builder = WebApplication.CreateBuilder();
        builder.Services.AddRouting();
        builder.Services.AddAntiforgery();
        // Dispose the host: an undisposed WebApplication leaks its config
        // PhysicalFileProvider (appsettings reload-watch FileSystemWatcher — a
        // process-wide macOS run-loop thread) and a ConsoleLoggerProcessor
        // thread, which keep the test host process alive after the run.
        using var app = builder.Build();
        app.MapAuthEndpoints();

        return ((IEndpointRouteBuilder)app).DataSources
            .SelectMany(ds => ds.Endpoints)
            .OfType<RouteEndpoint>()
            .ToList();
    }

    private static RouteEndpoint? Find(IReadOnlyList<RouteEndpoint> endpoints, string pattern, string method)
        => endpoints.FirstOrDefault(e =>
            e.RoutePattern.RawText == pattern &&
            (e.Metadata.GetMetadata<HttpMethodMetadata>()?.HttpMethods.Contains(method) ?? false));

    [Fact]
    public void AuthPing_GetRoute_IsMapped()
    {
        var ping = Find(BuildEndpoints(), "/auth/ping", "GET");
        Assert.NotNull(ping);
    }

    [Fact]
    public async Task AuthPing_AnonymousUser_Returns401()
    {
        var context = new DefaultHttpContext
        {
            User = new ClaimsPrincipal(new ClaimsIdentity()) // not authenticated
        };

        await AuthEndpoints.HandlePing(context);

        Assert.Equal(StatusCodes.Status401Unauthorized, context.Response.StatusCode);
    }

    [Fact]
    public async Task AuthPing_AuthenticatedUser_Returns200()
    {
        var identity = new ClaimsIdentity(
            new[] { new Claim(ClaimTypes.Name, "alice") }, authenticationType: "TestCookie");
        var context = new DefaultHttpContext
        {
            User = new ClaimsPrincipal(identity)
        };

        await AuthEndpoints.HandlePing(context);

        Assert.Equal(StatusCodes.Status200OK, context.Response.StatusCode);
    }

    [Fact]
    public void AuthPing_DoesNotTriggerCookieRedirect()
    {
        // The endpoint must NOT use RequireAuthorization(): that would make the
        // cookie middleware answer an expired request with a 302 to /login,
        // which a fetch() follows transparently and reads as a 200 login page —
        // SessionExpiry would never see the expiry. The endpoint allows
        // anonymous access and decides 200/401 itself.
        var ping = Find(BuildEndpoints(), "/auth/ping", "GET");
        Assert.NotNull(ping);

        var authorize = ping!.Metadata
            .GetOrderedMetadata<Microsoft.AspNetCore.Authorization.IAuthorizeData>();
        Assert.Empty(authorize);
    }
}
