using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging.Abstractions;
using NSubstitute;
using SIGCM2.Api.Authorization;
using SIGCM2.Application.Abstractions.Persistence;
using SIGCM2.Application.Audit;
using SIGCM2.Domain.Entities;

namespace SIGCM2.Api.Tests.Authorization;

/// <summary>
/// Unit tests for PermissionAuthorizationHandler — SUITE-B-01 (UDT-006) + SUITE-B-AUTHZ-HANDLER (UDT-009).
/// Tests isolated from DB: IRolPermisoRepository and IUsuarioRepository mocked via NSubstitute.
/// </summary>
public sealed class PermissionAuthorizationHandlerTests
{
    private readonly IRolPermisoRepository _rolPermisoRepo = Substitute.For<IRolPermisoRepository>();
    private readonly IUsuarioRepository _usuarioRepo = Substitute.For<IUsuarioRepository>();
    private readonly ISecurityEventLogger _security = Substitute.For<ISecurityEventLogger>();
    private readonly PermissionAuthorizationHandler _handler;

    public PermissionAuthorizationHandlerTests()
    {
        // Default: usuario repo returns null (no overrides) unless overridden in individual tests
        _usuarioRepo.GetByIdAsync(Arg.Any<int>(), Arg.Any<CancellationToken>())
            .Returns((Usuario?)null);

        _handler = new PermissionAuthorizationHandler(
            _rolPermisoRepo,
            _usuarioRepo,
            _security,
            NullLogger<PermissionAuthorizationHandler>.Instance);
    }

    // ── Helpers ──────────────────────────────────────────────────────────────

    /// <summary>Creates an authenticated user with rol claim and sub=42 (needed by UDT-009 handler).</summary>
    private static ClaimsPrincipal AuthenticatedUserWithRol(string rolValue, int userId = 42)
    {
        var identity = new ClaimsIdentity(
            new[]
            {
                new Claim("rol", rolValue),
                new Claim(JwtRegisteredClaimNames.Sub, userId.ToString()),
            },
            authenticationType: "TestAuth");
        return new ClaimsPrincipal(identity);
    }

    private static ClaimsPrincipal AuthenticatedUserWithoutRolClaim()
    {
        var identity = new ClaimsIdentity(
            new[]
            {
                new Claim(ClaimTypes.Name, "someuser"),
                new Claim(JwtRegisteredClaimNames.Sub, "42"),
            },
            authenticationType: "TestAuth");
        return new ClaimsPrincipal(identity);
    }

    private static ClaimsPrincipal AuthenticatedUserWithoutSubClaim()
    {
        var identity = new ClaimsIdentity(
            new[] { new Claim("rol", "cajero") },
            authenticationType: "TestAuth");
        return new ClaimsPrincipal(identity);
    }

    private static ClaimsPrincipal AnonymousUser()
    {
        return new ClaimsPrincipal(new ClaimsIdentity()); // not authenticated
    }

    private static Permiso MakePermiso(int id, string codigo) =>
        Permiso.ForRead(id, codigo, codigo, null, codigo.Split(':')[0], true, DateTime.UtcNow);

    private static AuthorizationHandlerContext MakeContext(
        ClaimsPrincipal user,
        RequirePermissionAttribute requirement,
        HttpContext? httpContext = null)
    {
        var ctx = httpContext ?? new DefaultHttpContext();
        return new AuthorizationHandlerContext(
            requirements: new[] { requirement },
            user: user,
            resource: ctx);
    }

    // ── B-01-01: Usuario con permiso requerido → Succeed ─────────────────────

    [Fact]
    public async Task HandleAsync_Succeeds_WhenUserHasRequiredPermission()
    {
        // Arrange
        var user = AuthenticatedUserWithRol("admin");
        var requirement = new RequirePermissionAttribute("administracion:usuarios:gestionar");

        _rolPermisoRepo.GetByRolCodigoAsync("admin", Arg.Any<CancellationToken>())
            .Returns(new List<Permiso> { MakePermiso(1, "administracion:usuarios:gestionar") }
                .AsReadOnly() as IReadOnlyList<Permiso>);

        var context = MakeContext(user, requirement);

        // Act
        await _handler.HandleAsync(context);

        // Assert
        Assert.True(context.HasSucceeded);
    }

    // ── B-01-02: Usuario sin el permiso requerido → Fail ──────────────────────

    [Fact]
    public async Task HandleAsync_Fails_WhenUserLacksPermission()
    {
        // Arrange
        var user = AuthenticatedUserWithRol("cajero");
        var requirement = new RequirePermissionAttribute("administracion:usuarios:gestionar");

        _rolPermisoRepo.GetByRolCodigoAsync("cajero", Arg.Any<CancellationToken>())
            .Returns(new List<Permiso> { MakePermiso(10, "ventas:contado:crear") }
                .AsReadOnly() as IReadOnlyList<Permiso>);

        var context = MakeContext(user, requirement);

        // Act
        await _handler.HandleAsync(context);

        // Assert
        Assert.False(context.HasSucceeded);
    }

    // ── B-01-03: Multi-permiso OR — tiene uno de los dos → Succeed ────────────

    [Fact]
    public async Task HandleAsync_Succeeds_WhenAnyOfMultiplePermissions_OR()
    {
        // Arrange
        var user = AuthenticatedUserWithRol("cajero");
        var requirement = new RequirePermissionAttribute(
            "ventas:contado:crear",
            "administracion:usuarios:gestionar");

        _rolPermisoRepo.GetByRolCodigoAsync("cajero", Arg.Any<CancellationToken>())
            .Returns(new List<Permiso>
            {
                MakePermiso(10, "ventas:contado:crear"),
                MakePermiso(11, "ventas:contado:cobrar"),
            }.AsReadOnly() as IReadOnlyList<Permiso>);

        var context = MakeContext(user, requirement);

        // Act
        await _handler.HandleAsync(context);

        // Assert
        Assert.True(context.HasSucceeded);
    }

    // ── B-01-04: Multi-permiso OR — no tiene ninguno → Fail ───────────────────

    [Fact]
    public async Task HandleAsync_Fails_WhenNoneOfMultiplePermissions()
    {
        // Arrange
        var user = AuthenticatedUserWithRol("cajero");
        var requirement = new RequirePermissionAttribute(
            "administracion:usuarios:gestionar",
            "administracion:roles:gestionar");

        _rolPermisoRepo.GetByRolCodigoAsync("cajero", Arg.Any<CancellationToken>())
            .Returns(new List<Permiso> { MakePermiso(10, "ventas:contado:crear") }
                .AsReadOnly() as IReadOnlyList<Permiso>);

        var context = MakeContext(user, requirement);

        // Act
        await _handler.HandleAsync(context);

        // Assert
        Assert.False(context.HasSucceeded);
    }

    // ── B-01-05: Claim "rol" ausente → Fail; repo nunca llamado ───────────────

    [Fact]
    public async Task HandleAsync_Fails_WhenRolClaimMissing()
    {
        // Arrange
        var user = AuthenticatedUserWithoutRolClaim();
        var requirement = new RequirePermissionAttribute("administracion:usuarios:gestionar");
        var context = MakeContext(user, requirement);

        // Act
        await _handler.HandleAsync(context);

        // Assert
        Assert.False(context.HasSucceeded);
        // Repo must NOT be called when claim is absent
        await _rolPermisoRepo.DidNotReceive()
            .GetByRolCodigoAsync(Arg.Any<string>(), Arg.Any<CancellationToken>());
    }

    // ── B-01-06: Repo devuelve lista vacía → Fail ─────────────────────────────

    [Fact]
    public async Task HandleAsync_Fails_WhenRoleHasNoPermissions()
    {
        // Arrange
        var user = AuthenticatedUserWithRol("reportes");
        var requirement = new RequirePermissionAttribute("administracion:usuarios:gestionar");

        _rolPermisoRepo.GetByRolCodigoAsync("reportes", Arg.Any<CancellationToken>())
            .Returns(new List<Permiso>().AsReadOnly() as IReadOnlyList<Permiso>);

        var context = MakeContext(user, requirement);

        // Act
        await _handler.HandleAsync(context);

        // Assert
        Assert.False(context.HasSucceeded);
    }

    // ── B-01-07: Rol no existe en RolPermiso (mismo caso que lista vacía) ──────

    [Fact]
    public async Task HandleAsync_Fails_WhenRoleDoesNotExistInRolPermisoTable()
    {
        // Arrange
        var user = AuthenticatedUserWithRol("rol_fantasma");
        var requirement = new RequirePermissionAttribute("administracion:usuarios:gestionar");

        _rolPermisoRepo.GetByRolCodigoAsync("rol_fantasma", Arg.Any<CancellationToken>())
            .Returns(new List<Permiso>().AsReadOnly() as IReadOnlyList<Permiso>);

        var context = MakeContext(user, requirement);

        // Act
        await _handler.HandleAsync(context);

        // Assert
        Assert.False(context.HasSucceeded);
    }

    // ── B-01-08: Fail stashea RequiredPermission en HttpContext.Items ──────────

    [Fact]
    public async Task HandleAsync_StashesRequiredPermission_InHttpContextItems_OnFail()
    {
        // Arrange
        var user = AuthenticatedUserWithRol("cajero");
        var requirement = new RequirePermissionAttribute("administracion:usuarios:gestionar");

        _rolPermisoRepo.GetByRolCodigoAsync("cajero", Arg.Any<CancellationToken>())
            .Returns(new List<Permiso> { MakePermiso(10, "ventas:contado:crear") }
                .AsReadOnly() as IReadOnlyList<Permiso>);

        var httpContext = new DefaultHttpContext();
        var context = MakeContext(user, requirement, httpContext);

        // Act
        await _handler.HandleAsync(context);

        // Assert — context.Resource is the HttpContext where items are stashed
        Assert.False(context.HasSucceeded);
        Assert.Equal("administracion:usuarios:gestionar", httpContext.Items["RequiredPermission"]);
    }

    // ── UDT-009: SUITE-B-AUTHZ-HANDLER (A-01 a A-07) ────────────────────────

    // A-01: Cajero sin override, endpoint requiere permiso ajeno → HasSucceeded == false
    [Fact]
    public async Task A01_Cajero_NoOverride_LacksPermission_Fails()
    {
        var user = AuthenticatedUserWithRol("cajero", userId: 42);
        var requirement = new RequirePermissionAttribute("administracion:usuarios:gestionar");

        _rolPermisoRepo.GetByRolCodigoAsync("cajero", Arg.Any<CancellationToken>())
            .Returns(new List<Permiso> { MakePermiso(10, "ventas:contado:crear") }
                .AsReadOnly() as IReadOnlyList<Permiso>);
        _usuarioRepo.GetByIdAsync(42, Arg.Any<CancellationToken>())
            .Returns(MakeUsuario(42, "cajero", """{"grant":[],"deny":[]}"""));

        var context = MakeContext(user, requirement);
        await _handler.HandleAsync(context);

        Assert.False(context.HasSucceeded);
    }

    // A-02: Cajero con grant del permiso requerido → HasSucceeded == true
    [Fact]
    public async Task A02_Cajero_WithGrant_RequiredPermiso_Succeeds()
    {
        var user = AuthenticatedUserWithRol("cajero", userId: 42);
        var requirement = new RequirePermissionAttribute("administracion:usuarios:gestionar");

        _rolPermisoRepo.GetByRolCodigoAsync("cajero", Arg.Any<CancellationToken>())
            .Returns(new List<Permiso> { MakePermiso(10, "ventas:contado:crear") }
                .AsReadOnly() as IReadOnlyList<Permiso>);
        _usuarioRepo.GetByIdAsync(42, Arg.Any<CancellationToken>())
            .Returns(MakeUsuario(42, "cajero", """{"grant":["administracion:usuarios:gestionar"],"deny":[]}"""));

        var context = MakeContext(user, requirement);
        await _handler.HandleAsync(context);

        Assert.True(context.HasSucceeded);
    }

    // A-03: Admin (tiene el permiso) + deny del permiso requerido → HasSucceeded == false
    [Fact]
    public async Task A03_Admin_WithDeny_RequiredPermiso_Fails()
    {
        var user = AuthenticatedUserWithRol("admin", userId: 1);
        var requirement = new RequirePermissionAttribute("administracion:permisos:ver");

        _rolPermisoRepo.GetByRolCodigoAsync("admin", Arg.Any<CancellationToken>())
            .Returns(new List<Permiso> { MakePermiso(21, "administracion:permisos:ver") }
                .AsReadOnly() as IReadOnlyList<Permiso>);
        _usuarioRepo.GetByIdAsync(1, Arg.Any<CancellationToken>())
            .Returns(MakeUsuario(1, "admin", """{"grant":[],"deny":["administracion:permisos:ver"]}"""));

        var context = MakeContext(user, requirement);
        await _handler.HandleAsync(context);

        Assert.False(context.HasSucceeded);
    }

    // A-04: Token sin claim 'permisos' (post-UDT-009) → handler resuelve desde DB
    [Fact]
    public async Task A04_TokenWithoutPermisosClaim_HandlerResolvesFromDB()
    {
        // Token has sub=42 but no 'permisos' claim (post-UDT-009 JWT)
        var user = AuthenticatedUserWithRol("cajero", userId: 42);
        var requirement = new RequirePermissionAttribute("ventas:contado:crear");

        _rolPermisoRepo.GetByRolCodigoAsync("cajero", Arg.Any<CancellationToken>())
            .Returns(new List<Permiso> { MakePermiso(10, "ventas:contado:crear") }
                .AsReadOnly() as IReadOnlyList<Permiso>);
        _usuarioRepo.GetByIdAsync(42, Arg.Any<CancellationToken>())
            .Returns(MakeUsuario(42, "cajero", """{"grant":[],"deny":[]}"""));

        var context = MakeContext(user, requirement);
        await _handler.HandleAsync(context);

        // Handler correctly resolves from DB (no 'permisos' claim needed)
        Assert.True(context.HasSucceeded);
        await _usuarioRepo.Received(1).GetByIdAsync(42, Arg.Any<CancellationToken>());
    }

    // A-05: IUsuarioRepository.GetByIdAsync called with sub from token
    [Fact]
    public async Task A05_GetByIdAsync_CalledWithSubFromToken()
    {
        var user = AuthenticatedUserWithRol("cajero", userId: 42);
        var requirement = new RequirePermissionAttribute("ventas:contado:crear");

        _rolPermisoRepo.GetByRolCodigoAsync("cajero", Arg.Any<CancellationToken>())
            .Returns(new List<Permiso> { MakePermiso(10, "ventas:contado:crear") }
                .AsReadOnly() as IReadOnlyList<Permiso>);
        _usuarioRepo.GetByIdAsync(42, Arg.Any<CancellationToken>())
            .Returns(MakeUsuario(42, "cajero", """{"grant":[],"deny":[]}"""));

        var context = MakeContext(user, requirement);
        await _handler.HandleAsync(context);

        await _usuarioRepo.Received(1).GetByIdAsync(42, Arg.Any<CancellationToken>());
    }

    // A-06: sub claim absent → Fail, repo NOT called
    [Fact]
    public async Task A06_SubClaimAbsent_Fails_RepoNotCalled()
    {
        var user = AuthenticatedUserWithoutSubClaim();
        var requirement = new RequirePermissionAttribute("ventas:contado:crear");

        _rolPermisoRepo.GetByRolCodigoAsync(Arg.Any<string>(), Arg.Any<CancellationToken>())
            .Returns(new List<Permiso> { MakePermiso(10, "ventas:contado:crear") }
                .AsReadOnly() as IReadOnlyList<Permiso>);

        var context = MakeContext(user, requirement);
        await _handler.HandleAsync(context);

        Assert.False(context.HasSucceeded);
        await _usuarioRepo.DidNotReceive().GetByIdAsync(Arg.Any<int>(), Arg.Any<CancellationToken>());
    }

    // A-07: Usuario not found in DB (null) → Fail, no exception
    [Fact]
    public async Task A07_UsuarioNotFoundInDB_FailsSafely_NoException()
    {
        var user = AuthenticatedUserWithRol("cajero", userId: 9999);
        var requirement = new RequirePermissionAttribute("ventas:contado:crear");

        _rolPermisoRepo.GetByRolCodigoAsync("cajero", Arg.Any<CancellationToken>())
            .Returns(new List<Permiso> { MakePermiso(10, "ventas:contado:crear") }
                .AsReadOnly() as IReadOnlyList<Permiso>);
        _usuarioRepo.GetByIdAsync(9999, Arg.Any<CancellationToken>())
            .Returns((Usuario?)null);

        var context = MakeContext(user, requirement);

        // Should not throw — null usuario → no overrides → resolve with Empty (rol permisos only)
        await _handler.HandleAsync(context);

        // With no overrides, cajero with ventas:contado:crear should succeed
        Assert.True(context.HasSucceeded);
    }

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

    private static Usuario MakeUsuario(int id, string rol, string permisosJson)
        => new(id, "user" + id, "$2a$12$hash", "Test", "User", null, rol, permisosJson, true);
}
