using System.Security.Claims;
using Microsoft.AspNetCore.Http;
using ZB.MOM.WW.Auth.AspNetCore;
using ZB.MOM.WW.MxGateway.Server.Dashboard;
using ZB.MOM.WW.MxGateway.Server.Security.Audit;

namespace ZB.MOM.WW.MxGateway.Tests.Security.Audit;

/// <summary>
/// Tests <see cref="HttpAuditActorAccessor"/> — the HTTP-backed <see cref="IAuditActorAccessor"/>
/// that reads the dashboard operator's username from the current HTTP context.
/// </summary>
public sealed class HttpAuditActorAccessorTests
{
    /// <summary>
    /// When the HTTP context carries an authenticated principal with <see cref="ZbClaimTypes.Username"/>,
    /// <see cref="IAuditActorAccessor.CurrentActor"/> returns that username.
    /// </summary>
    [Fact]
    public void CurrentActor_AuthenticatedUserWithUsernamelaim_ReturnsUsername()
    {
        HttpAuditActorAccessor accessor = CreateAccessor(
            CreateAuthenticatedUser(username: "alice", displayName: "Alice Admin"));

        Assert.Equal("alice", accessor.CurrentActor);
    }

    /// <summary>
    /// When the principal has no <see cref="ZbClaimTypes.Username"/> but has a
    /// <see cref="ClaimTypes.Name"/> claim (display name), the accessor falls back to
    /// <see cref="System.Security.Principal.IIdentity.Name"/>.
    /// </summary>
    [Fact]
    public void CurrentActor_AuthenticatedUserWithoutUsernameClaim_FallsBackToIdentityName()
    {
        // Build a principal with display-name but no zb:username.
        ClaimsIdentity identity = new(
            [new Claim(ClaimTypes.Name, "Alice Admin")],
            DashboardAuthenticationDefaults.AuthenticationScheme,
            ClaimTypes.Name,
            ClaimTypes.Role);
        HttpAuditActorAccessor accessor = CreateAccessor(new ClaimsPrincipal(identity));

        Assert.Equal("Alice Admin", accessor.CurrentActor);
    }

    /// <summary>
    /// An unauthenticated (anonymous) principal yields <see langword="null"/>.
    /// </summary>
    [Fact]
    public void CurrentActor_UnauthenticatedUser_ReturnsNull()
    {
        HttpAuditActorAccessor accessor = CreateAccessor(new ClaimsPrincipal(new ClaimsIdentity()));

        Assert.Null(accessor.CurrentActor);
    }

    /// <summary>
    /// When there is no HTTP context at all (e.g. a background thread), the accessor returns
    /// <see langword="null"/> rather than throwing.
    /// </summary>
    [Fact]
    public void CurrentActor_NoHttpContext_ReturnsNull()
    {
        HttpContextAccessor contextAccessor = new() { HttpContext = null };
        HttpAuditActorAccessor accessor = new(contextAccessor);

        Assert.Null(accessor.CurrentActor);
    }

    /// <summary>
    /// When the <see cref="ZbClaimTypes.Username"/> claim is present it is always preferred
    /// over the <see cref="ClaimTypes.Name"/> (display-name) value.
    /// </summary>
    [Fact]
    public void CurrentActor_UsernameClaimPreferredOverDisplayName()
    {
        // login username = "jsmith"; display name = "John Smith"
        HttpAuditActorAccessor accessor = CreateAccessor(
            CreateAuthenticatedUser(username: "jsmith", displayName: "John Smith"));

        Assert.Equal("jsmith", accessor.CurrentActor);
    }

    // ── helpers ─────────────────────────────────────────────────────────────

    private static HttpAuditActorAccessor CreateAccessor(ClaimsPrincipal user)
    {
        DefaultHttpContext httpContext = new() { User = user };
        return new HttpAuditActorAccessor(new HttpContextAccessor { HttpContext = httpContext });
    }

    private static ClaimsPrincipal CreateAuthenticatedUser(string username, string displayName)
    {
        ClaimsIdentity identity = new(
            [
                new Claim(ZbClaimTypes.Username, username),
                new Claim(ZbClaimTypes.Name, displayName),
                new Claim(ClaimTypes.Role, DashboardRoles.Admin),
            ],
            DashboardAuthenticationDefaults.AuthenticationScheme,
            ZbClaimTypes.Name,
            ZbClaimTypes.Role);

        return new ClaimsPrincipal(identity);
    }
}
