using System.IdentityModel.Tokens.Jwt;
using System.Security.Cryptography;
using Microsoft.IdentityModel.Tokens;
using SIGCM2.Domain.Entities;
using SIGCM2.Infrastructure.Security;

namespace SIGCM2.Application.Tests.Infrastructure;

public class JwtServiceTests : IDisposable
{
    private readonly RSA _rsa;
    private readonly JwtOptions _options;
    private readonly JwtService _jwtService;

    public JwtServiceTests()
    {
        // Generate a test RSA key pair inline (no files needed for unit tests)
        _rsa = RSA.Create(2048);
        _options = new JwtOptions
        {
            Issuer = "sigcm2.api",
            Audience = "sigcm2.web",
            AccessTokenMinutes = 60
        };
        _jwtService = new JwtService(_rsa, _options, TimeProvider.System);
    }

    public void Dispose() => _rsa.Dispose();

    // Scenario: generated token uses RS256 algorithm
    [Fact]
    public void GenerateAccessToken_UsesRS256Algorithm()
    {
        var usuario = MakeUsuario();
        var token = _jwtService.GenerateAccessToken(usuario);

        var handler = new JwtSecurityTokenHandler();
        var parsed = handler.ReadJwtToken(token);

        Assert.Equal("RS256", parsed.Header.Alg);
    }

    // Scenario: claims contain expected values
    [Fact]
    public void GenerateAccessToken_ContainsExpectedClaims()
    {
        var usuario = MakeUsuario();
        var token = _jwtService.GenerateAccessToken(usuario);

        var handler = new JwtSecurityTokenHandler();
        var parsed = handler.ReadJwtToken(token);

        Assert.Equal("1", parsed.Subject);                          // sub = user ID
        Assert.Equal("sigcm2.api", parsed.Issuer);                 // iss
        Assert.Contains("sigcm2.web", parsed.Audiences);           // aud
        Assert.Contains(parsed.Claims, c => c.Type == "name" && c.Value == "admin");
        Assert.Contains(parsed.Claims, c => c.Type == "rol" && c.Value == "admin");

        // J-01 (UDT-009): token must NOT contain 'permisos' claim post-UDT-009
        Assert.DoesNotContain(parsed.Claims, c => c.Type == "permisos");
    }

    // J-01: token post-UDT-009 does NOT have 'permisos' claim
    [Fact]
    public void GenerateAccessToken_DoesNotContainPermisosClaim()
    {
        var usuario = MakeUsuario();
        var token = _jwtService.GenerateAccessToken(usuario);

        var handler = new JwtSecurityTokenHandler();
        var parsed = handler.ReadJwtToken(token);

        Assert.DoesNotContain(parsed.Claims, c => c.Type == "permisos");
    }

    // J-02: claims present are sub, jti, name, rol (+ iat/exp/nbf) — no extras
    [Fact]
    public void GenerateAccessToken_HasExactlyExpectedClaims_NoPermisos()
    {
        var usuario = MakeUsuario();
        var token = _jwtService.GenerateAccessToken(usuario);

        var handler = new JwtSecurityTokenHandler();
        var parsed = handler.ReadJwtToken(token);

        // Must have sub, name, rol, jti
        Assert.Contains(parsed.Claims, c => c.Type == "sub");
        Assert.Contains(parsed.Claims, c => c.Type == "name");
        Assert.Contains(parsed.Claims, c => c.Type == "rol");
        Assert.Contains(parsed.Claims, c => c.Type == "jti");

        // Must NOT have permisos
        Assert.DoesNotContain(parsed.Claims, c => c.Type == "permisos");
    }

    // J-03: MakeUsuario with '["*"]' PermisosJson → token still has no 'permisos' claim
    [Fact]
    public void GenerateAccessToken_WithLegacyPermisosJson_NoPermisosClaim()
    {
        // MakeUsuario already uses '[\"*\"]' — this explicitly tests J-03
        var usuario = MakeUsuario();
        Assert.Equal("[\"*\"]", usuario.PermisosJson); // verify the helper

        var token = _jwtService.GenerateAccessToken(usuario);

        var handler = new JwtSecurityTokenHandler();
        var parsed = handler.ReadJwtToken(token);

        // Post-UDT-009: JwtService ignores PermisosJson entirely — no claim emitted
        Assert.DoesNotContain(parsed.Claims, c => c.Type == "permisos");
    }

    // Scenario: token is verifiable with the public key
    [Fact]
    public void GenerateAccessToken_IsVerifiableWithPublicKey()
    {
        var usuario = MakeUsuario();
        var token = _jwtService.GenerateAccessToken(usuario);

        var publicKey = RSA.Create();
        publicKey.ImportRSAPublicKey(_rsa.ExportRSAPublicKey(), out _);

        var validationParams = new TokenValidationParameters
        {
            ValidateIssuerSigningKey = true,
            IssuerSigningKey = new RsaSecurityKey(publicKey),
            ValidIssuer = "sigcm2.api",
            ValidAudience = "sigcm2.web",
            ValidateLifetime = true,
            ClockSkew = TimeSpan.Zero
        };

        var handler = new JwtSecurityTokenHandler();
        var principal = handler.ValidateToken(token, validationParams, out var validatedToken);

        Assert.NotNull(principal);
        Assert.IsType<JwtSecurityToken>(validatedToken);
    }

    // Scenario: expiry is 60 minutes from now
    [Fact]
    public void GenerateAccessToken_ExpiryIs60MinutesFromNow()
    {
        var usuario = MakeUsuario();
        var before = DateTime.UtcNow;
        var token = _jwtService.GenerateAccessToken(usuario);
        var after = DateTime.UtcNow;

        var handler = new JwtSecurityTokenHandler();
        var parsed = handler.ReadJwtToken(token);

        var expectedMinExpiry = before.AddMinutes(59).AddSeconds(55);
        var expectedMaxExpiry = after.AddMinutes(60).AddSeconds(5);

        Assert.True(parsed.ValidTo >= expectedMinExpiry, $"exp {parsed.ValidTo} < expected min {expectedMinExpiry}");
        Assert.True(parsed.ValidTo <= expectedMaxExpiry, $"exp {parsed.ValidTo} > expected max {expectedMaxExpiry}");
    }

    // Triangulation: different user produces token with different sub claim
    [Fact]
    public void GenerateAccessToken_DifferentUser_DifferentSubClaim()
    {
        var user1 = MakeUsuario(id: 1, username: "admin");
        var user2 = MakeUsuario(id: 2, username: "vendedor");

        var token1 = _jwtService.GenerateAccessToken(user1);
        var token2 = _jwtService.GenerateAccessToken(user2);

        var handler = new JwtSecurityTokenHandler();
        var parsed1 = handler.ReadJwtToken(token1);
        var parsed2 = handler.ReadJwtToken(token2);

        Assert.NotEqual(parsed1.Subject, parsed2.Subject);
        Assert.Equal("1", parsed1.Subject);
        Assert.Equal("2", parsed2.Subject);
    }

    // T-040: GetPrincipalFromExpiredToken

    [Fact]
    public void GetPrincipalFromExpiredToken_ValidSignatureExpired_ReturnsPrincipal()
    {
        // Generate a token that will expire in 1 second, then manually create an expired JWT
        // using JwtSecurityTokenHandler directly (bypassing the service to control timestamps).
        var signingKey = new RsaSecurityKey(_rsa);
        var credentials = new SigningCredentials(signingKey, SecurityAlgorithms.RsaSha256);

        var past = DateTime.UtcNow.AddHours(-2);
        var descriptor = new SecurityTokenDescriptor
        {
            Subject = new System.Security.Claims.ClaimsIdentity(
                [new System.Security.Claims.Claim("sub", "1")]),
            Issuer = "sigcm2.api",
            Audience = "sigcm2.web",
            IssuedAt = past,
            NotBefore = past,
            Expires = past.AddMinutes(60),   // expired 1h ago
            SigningCredentials = credentials,
        };

        var handler = new JwtSecurityTokenHandler();
        var token = handler.CreateToken(descriptor);
        var expiredToken = handler.WriteToken(token);

        // Now use the service — it must validate the signature but ignore expiry
        var principal = _jwtService.GetPrincipalFromExpiredToken(expiredToken);

        Assert.NotNull(principal);
        // The JWT handler maps "sub" to ClaimTypes.NameIdentifier by default,
        // but our JwtService uses a custom "sub" claim. Check both.
        var sub = principal.FindFirst("sub")?.Value
                  ?? principal.FindFirst(System.Security.Claims.ClaimTypes.NameIdentifier)?.Value;
        Assert.Equal("1", sub);
    }

    [Fact]
    public void GetPrincipalFromExpiredToken_InvalidSignature_Throws()
    {
        // Sign with a different RSA key
        using var otherRsa = System.Security.Cryptography.RSA.Create(2048);
        var otherOptions = new JwtOptions { Issuer = "sigcm2.api", Audience = "sigcm2.web", AccessTokenMinutes = 60 };
        var otherService = new JwtService(otherRsa, otherOptions, TimeProvider.System);
        var tokenFromOtherKey = otherService.GenerateAccessToken(MakeUsuario());

        // Validating with the correct key should throw
        Assert.Throws<Microsoft.IdentityModel.Tokens.SecurityTokenSignatureKeyNotFoundException>(
            () => _jwtService.GetPrincipalFromExpiredToken(tokenFromOtherKey));
    }

    [Fact]
    public void GetPrincipalFromExpiredToken_MalformedToken_Throws()
    {
        Assert.ThrowsAny<Exception>(
            () => _jwtService.GetPrincipalFromExpiredToken("not.a.valid.jwt"));
    }

    private static Usuario MakeUsuario(int id = 1, string username = "admin")
        => new(id, username, "$2a$12$hash", "Administrador", "Sistema", null, "admin", "[\"*\"]", true);
}
