using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
using ApiOPE.Configuration;
using ApiOPE.Security;
using Microsoft.Extensions.Options;

namespace ApiOPE.Tests;

public sealed class OpeTokenValidatorTests
{
    private static readonly DateTimeOffset Now = DateTimeOffset.FromUnixTimeSeconds(1_800_000_000);

    [Fact]
    public void Validate_AcceptsSignedCurrentToken()
    {
        var options = CreateOptions();
        var validator = CreateValidator(options);
        var token = CreateToken(options, "/genericoperations", Now.ToUnixTimeSeconds(), includeVersion: true);

        var result = validator.Validate(token, "/genericoperations", versionOptional: false);

        Assert.True(result.IsValid);
        Assert.NotNull(result.RequestContext);
        Assert.Equal("/genericoperations", result.RequestContext.Resource);
    }

    [Fact]
    public void Validate_RejectsManipulatedSignature()
    {
        var options = CreateOptions();
        var validator = CreateValidator(options);
        var token = CreateToken(
            options,
            "/genericoperations",
            Now.ToUnixTimeSeconds(),
            includeVersion: true,
            signingSecret: "another-secret-key-that-is-long-enough");

        var result = validator.Validate(token, "/genericoperations", versionOptional: false);

        Assert.False(result.IsValid);
    }

    [Fact]
    public void Validate_RejectsExpiredTimestamp()
    {
        var options = CreateOptions();
        var validator = CreateValidator(options);
        var token = CreateToken(
            options,
            "/genericoperations",
            Now.AddMinutes(-7).ToUnixTimeSeconds(),
            includeVersion: true);

        var result = validator.Validate(token, "/genericoperations", versionOptional: false);

        Assert.False(result.IsValid);
    }

    [Fact]
    public void Validate_RejectsDifferentResource()
    {
        var options = CreateOptions();
        var validator = CreateValidator(options);
        var token = CreateToken(options, "/", Now.ToUnixTimeSeconds(), includeVersion: true);

        var result = validator.Validate(token, "/genericoperations", versionOptional: false);

        Assert.False(result.IsValid);
    }

    [Fact]
    public void Validate_AllowsMissingVersionOnlyForVersionEndpoint()
    {
        var options = CreateOptions();
        var validator = CreateValidator(options);
        var token = CreateToken(options, "/versions/current", Now.ToUnixTimeSeconds(), includeVersion: false);

        var versionResult = validator.Validate(token, "/versions/current", versionOptional: true);
        var operationResult = validator.Validate(token, "/versions/current", versionOptional: false);

        Assert.True(versionResult.IsValid);
        Assert.False(operationResult.IsValid);
    }

    [Fact]
    public void Validate_PreservesOriginalUuidTextForResponseSignature()
    {
        var options = CreateOptions();
        var validator = CreateValidator(options);
        const string uppercaseUuid = "26CD0DA5-C45F-497D-B380-820C03CF3237";
        var token = CreateToken(
            options,
            "/genericoperations",
            Now.ToUnixTimeSeconds(),
            includeVersion: true,
            uuid: uppercaseUuid);

        var result = validator.Validate(token, "/genericoperations", versionOptional: false);

        Assert.True(result.IsValid);
        Assert.Equal(uppercaseUuid, result.RequestContext!.Uuid);
    }

    private static OpeTokenValidator CreateValidator(OpeOptions options)
        => new(Options.Create(options), new FixedTimeProvider(Now));

    private static OpeOptions CreateOptions()
        => new()
        {
            ClientToken = "client-token",
            SecretKey = "test-secret-key-with-more-than-32-characters",
            Version = "1.0",
            TokenMaxAgeSeconds = 300,
            ClockSkewSeconds = 60
        };

    private static string CreateToken(
        OpeOptions options,
        string resource,
        long timestamp,
        bool includeVersion,
        string? signingSecret = null,
        string uuid = "26cd0da5-c45f-497d-b380-820c03cf3237")
    {
        var header = Base64Url(JsonSerializer.SerializeToUtf8Bytes(new { alg = "HS256", typ = "JWT" }));
        var payloadValues = new Dictionary<string, object>
        {
            ["client_token"] = options.ClientToken,
            ["resource"] = resource,
            ["uuid"] = uuid,
            ["transaction_id"] = "37ec011f-4f0b-4a7b-a807-a83e88c73ce9",
            ["timestamp"] = timestamp
        };
        if (includeVersion)
        {
            payloadValues["version"] = options.Version;
        }

        var payload = Base64Url(JsonSerializer.SerializeToUtf8Bytes(payloadValues));
        var signedContent = Encoding.ASCII.GetBytes($"{header}.{payload}");
        var signature = HMACSHA256.HashData(
            Encoding.UTF8.GetBytes(signingSecret ?? options.SecretKey),
            signedContent);

        return $"{header}.{payload}.{Base64Url(signature)}";
    }

    private static string Base64Url(byte[] value)
        => Convert.ToBase64String(value).TrimEnd('=').Replace('+', '-').Replace('/', '_');

    private sealed class FixedTimeProvider : TimeProvider
    {
        private readonly DateTimeOffset _utcNow;

        public FixedTimeProvider(DateTimeOffset utcNow)
        {
            _utcNow = utcNow;
        }

        public override DateTimeOffset GetUtcNow() => _utcNow;
    }
}
