using System.Security.Claims;
using Microsoft.AspNetCore.Http;
using Shouldly;
using Xunit;
using ZB.MOM.WW.Auth.AspNetCore;
using ZB.MOM.WW.OtOpcUa.Security.Audit;

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

/// <summary>
/// Unit tests for <see cref="HttpAuditActorAccessor"/>.
/// <para>
/// Covers the three cases:
/// <list type="bullet">
///   <item>Authenticated principal with a <see cref="ZbClaimTypes.Username"/> claim →
///   returns the username claim value.</item>
///   <item>Authenticated principal with only a <see cref="ZbClaimTypes.Name"/> / no
///   username claim → falls back to the Name claim.</item>
///   <item>No HTTP context (null) or unauthenticated principal → returns
///   <see langword="null"/>.</item>
/// </list>
/// </para>
/// </summary>
public sealed class HttpAuditActorAccessorTests
{
    // ── helpers ──────────────────────────────────────────────────────────────────

    private static IHttpContextAccessor ContextWith(ClaimsPrincipal principal)
    {
        var context = new DefaultHttpContext { User = principal };
        return new HttpContextAccessorStub(context);
    }

    private static IHttpContextAccessor NoContext() =>
        new HttpContextAccessorStub(null);

    private static ClaimsPrincipal AuthenticatedWith(params Claim[] claims)
    {
        var identity = new ClaimsIdentity(
            claims,
            authenticationType: "TestScheme",   // non-null authenticationType → IsAuthenticated = true
            nameType: ZbClaimTypes.Name,
            roleType: ZbClaimTypes.Role);
        return new ClaimsPrincipal(identity);
    }

    private static ClaimsPrincipal Unauthenticated() =>
        new(new ClaimsIdentity()); // no authenticationType → IsAuthenticated = false

    // ── tests ─────────────────────────────────────────────────────────────────────

    /// <summary>
    /// An authenticated principal that carries <see cref="ZbClaimTypes.Username"/>
    /// returns exactly that claim value — it is the canonical actor string.
    /// </summary>
    [Fact]
    public void Returns_username_claim_for_authenticated_principal()
    {
        var principal = AuthenticatedWith(
            new Claim(ZbClaimTypes.Username, "alice"),
            new Claim(ZbClaimTypes.Name, "alice-name"),
            new Claim(ZbClaimTypes.DisplayName, "Alice User"));

        var sut = new HttpAuditActorAccessor(ContextWith(principal));

        sut.CurrentActor.ShouldBe("alice");
    }

    /// <summary>
    /// When the principal has no <see cref="ZbClaimTypes.Username"/> claim but does have
    /// a <see cref="ZbClaimTypes.Name"/> claim, the Name claim value is returned as the
    /// fallback actor.
    /// </summary>
    [Fact]
    public void Falls_back_to_Name_claim_when_Username_claim_is_absent()
    {
        var principal = AuthenticatedWith(
            new Claim(ZbClaimTypes.Name, "bob"));

        var sut = new HttpAuditActorAccessor(ContextWith(principal));

        sut.CurrentActor.ShouldBe("bob");
    }

    /// <summary>
    /// An unauthenticated principal (Identity.IsAuthenticated == false) returns null —
    /// the caller's fallback (typically <see cref="AuditActor.SystemFallback"/>) is used.
    /// </summary>
    [Fact]
    public void Returns_null_for_unauthenticated_principal()
    {
        var sut = new HttpAuditActorAccessor(ContextWith(Unauthenticated()));

        sut.CurrentActor.ShouldBeNull();
    }

    /// <summary>
    /// When there is no current <c>HttpContext</c> (e.g. background task, actor mailbox
    /// worker), returns null.
    /// </summary>
    [Fact]
    public void Returns_null_when_no_HttpContext()
    {
        var sut = new HttpAuditActorAccessor(NoContext());

        sut.CurrentActor.ShouldBeNull();
    }

    // ── stub ──────────────────────────────────────────────────────────────────────

    private sealed class HttpContextAccessorStub(HttpContext? context) : IHttpContextAccessor
    {
        public HttpContext? HttpContext { get; set; } = context;
    }
}
