using System.Collections.Generic;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Cryptography;
using Microsoft.Extensions.Time.Testing;
using Microsoft.Extensions.Options;
using Microsoft.IdentityModel.Tokens;
using StellaOps.Scanner.Core.Security;
using Xunit;

namespace StellaOps.Scanner.Core.Tests.Security;

public sealed class DpopProofValidatorTests
{
    [Fact]
    public async Task ValidateAsync_ReturnsSuccess_ForValidProof()
    {
        var timeProvider = new FakeTimeProvider(new DateTimeOffset(2025, 10, 18, 12, 0, 0, TimeSpan.Zero));
        var validator = new DpopProofValidator(Options.Create(new DpopValidationOptions()), new InMemoryDpopReplayCache(timeProvider), timeProvider);
        using var key = ECDsa.Create(ECCurve.NamedCurves.nistP256);
        var securityKey = new ECDsaSecurityKey(key) { KeyId = Guid.NewGuid().ToString("N") };

        var proof = CreateProof(timeProvider, securityKey, "GET", new Uri("https://scanner.example.com/api/v1/scans"));
        var result = await validator.ValidateAsync(proof, "GET", new Uri("https://scanner.example.com/api/v1/scans"));

        Assert.True(result.IsValid);
        Assert.NotNull(result.PublicKey);
        Assert.NotNull(result.JwtId);
    }

    [Fact]
    public async Task ValidateAsync_Fails_OnNonceMismatch()
    {
        var timeProvider = new FakeTimeProvider(new DateTimeOffset(2025, 10, 18, 12, 0, 0, TimeSpan.Zero));
        var validator = new DpopProofValidator(Options.Create(new DpopValidationOptions()), new InMemoryDpopReplayCache(timeProvider), timeProvider);
        using var key = ECDsa.Create(ECCurve.NamedCurves.nistP256);
        var securityKey = new ECDsaSecurityKey(key) { KeyId = Guid.NewGuid().ToString("N") };

        var proof = CreateProof(timeProvider, securityKey, "POST", new Uri("https://scanner.example.com/api/v1/scans"), nonce: "expected");
        var result = await validator.ValidateAsync(proof, "POST", new Uri("https://scanner.example.com/api/v1/scans"), nonce: "different");

        Assert.False(result.IsValid);
        Assert.Equal("invalid_token", result.ErrorCode);
    }

    [Fact]
    public async Task ValidateAsync_Fails_OnReplay()
    {
        var timeProvider = new FakeTimeProvider(new DateTimeOffset(2025, 10, 18, 12, 0, 0, TimeSpan.Zero));
        var cache = new InMemoryDpopReplayCache(timeProvider);
        var validator = new DpopProofValidator(Options.Create(new DpopValidationOptions()), cache, timeProvider);
        using var key = ECDsa.Create(ECCurve.NamedCurves.nistP256);
        var securityKey = new ECDsaSecurityKey(key) { KeyId = Guid.NewGuid().ToString("N") };
        var jti = Guid.NewGuid().ToString();

        var proof = CreateProof(timeProvider, securityKey, "GET", new Uri("https://scanner.example.com/api/v1/scans"), jti: jti);

        var first = await validator.ValidateAsync(proof, "GET", new Uri("https://scanner.example.com/api/v1/scans"));
        Assert.True(first.IsValid);

        var second = await validator.ValidateAsync(proof, "GET", new Uri("https://scanner.example.com/api/v1/scans"));
        Assert.False(second.IsValid);
        Assert.Equal("replay", second.ErrorCode);
    }

    private static string CreateProof(FakeTimeProvider timeProvider, ECDsaSecurityKey key, string method, Uri uri, string? nonce = null, string? jti = null)
    {
        var handler = new JwtSecurityTokenHandler();
        var signingCredentials = new SigningCredentials(key, SecurityAlgorithms.EcdsaSha256);
        var jwk = JsonWebKeyConverter.ConvertFromECDsaSecurityKey(key);

        var header = new JwtHeader(signingCredentials)
        {
            ["typ"] = "dpop+jwt",
            ["jwk"] = new Dictionary<string, object?>
            {
                ["kty"] = jwk.Kty,
                ["crv"] = jwk.Crv,
                ["x"] = jwk.X,
                ["y"] = jwk.Y
            }
        };

        var payload = new JwtPayload
        {
            ["htm"] = method.ToUpperInvariant(),
            ["htu"] = Normalize(uri),
            ["iat"] = timeProvider.GetUtcNow().ToUnixTimeSeconds(),
            ["jti"] = jti ?? Guid.NewGuid().ToString()
        };

        if (nonce is not null)
        {
            payload["nonce"] = nonce;
        }

        var token = new JwtSecurityToken(header, payload);
        return handler.WriteToken(token);
    }

    private static string Normalize(Uri uri)
    {
        var builder = new UriBuilder(uri)
        {
            Fragment = string.Empty
        };

        builder.Host = builder.Host.ToLowerInvariant();
        builder.Scheme = builder.Scheme.ToLowerInvariant();

        if ((builder.Scheme == "http" && builder.Port == 80) || (builder.Scheme == "https" && builder.Port == 443))
        {
            builder.Port = -1;
        }

        return builder.Uri.GetComponents(UriComponents.SchemeAndServer | UriComponents.PathAndQuery, UriFormat.UriEscaped);
    }
}
