using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
using ZB.MOM.WW.Auth.Abstractions.ApiKeys;
using ZB.MOM.WW.Auth.ApiKeys;

namespace ZB.MOM.WW.MxGateway.Tests.Security.Authentication;

/// <summary>
/// Parity tests for the shared <c>ZB.MOM.WW.Auth.ApiKeys</c> verifier as the gateway relies on it:
/// the <c>mxgw</c> token format, peppered HMAC-SHA256 secret hashing, constant-time comparison,
/// fail-closed discrimination (missing/unknown/revoked/wrong-secret/missing-pepper), and that the
/// raw secret never leaks into the result. The expected hash is computed here independently to keep
/// the test honest against the library's internal hasher.
/// </summary>
public sealed class ApiKeyVerifierTests
{
    private static readonly ApiKeyOptions Options = new() { TokenPrefix = "mxgw" };

    /// <summary>Verifies that VerifyAsync returns identity and scopes for a valid key.</summary>
    /// <returns>A task that represents the asynchronous operation.</returns>
    [Fact]
    public async Task VerifyAsync_ValidKey_ReturnsIdentityAndScopes()
    {
        FakeApiKeyStore store = new(CreateRecord("pepper", revokedUtc: null));
        ApiKeyVerifier verifier = new(Options, store, new FakePepperProvider("pepper"));

        ApiKeyVerification result = await verifier.VerifyAsync(
            "Bearer mxgw_operator01_correct-secret",
            CancellationToken.None);

        Assert.True(result.Succeeded);
        Assert.NotNull(result.Identity);
        Assert.Equal("operator01", result.Identity.KeyId);
        Assert.Equal("Operator Key", result.Identity.DisplayName);
        Assert.Contains("session:open", result.Identity.Scopes);
        Assert.Contains("events:read", result.Identity.Scopes);
        Assert.True(store.MarkedUsed);
    }

    /// <summary>Verifies that VerifyAsync does not expose the raw secret in the result.</summary>
    /// <returns>A task that represents the asynchronous operation.</returns>
    [Fact]
    public async Task VerifyAsync_ValidKey_DoesNotExposeRawSecretInResult()
    {
        FakeApiKeyStore store = new(CreateRecord("pepper", revokedUtc: null));
        ApiKeyVerifier verifier = new(Options, store, new FakePepperProvider("pepper"));

        ApiKeyVerification result = await verifier.VerifyAsync(
            "Bearer mxgw_operator01_correct-secret",
            CancellationToken.None);

        string serialized = JsonSerializer.Serialize(result);

        Assert.DoesNotContain("correct-secret", serialized, StringComparison.Ordinal);
    }

    /// <summary>Verifies that VerifyAsync fails as missing/malformed for a malformed key.</summary>
    /// <param name="authorizationHeader">Authorization header value to test.</param>
    /// <returns>A task that represents the asynchronous operation.</returns>
    [Theory]
    [InlineData("")]
    [InlineData("Bearer mxgw_operator01")]
    [InlineData("Bearer wrong")]
    public async Task VerifyAsync_MalformedKey_FailsMissingOrMalformed(string authorizationHeader)
    {
        ApiKeyVerifier verifier = new(Options, new FakeApiKeyStore(storedKey: null), new FakePepperProvider("pepper"));

        ApiKeyVerification result = await verifier.VerifyAsync(
            authorizationHeader,
            CancellationToken.None);

        Assert.False(result.Succeeded);
        Assert.Equal(ApiKeyFailure.MissingOrMalformed, result.Failure);
    }

    /// <summary>Verifies that VerifyAsync fails for an unknown key.</summary>
    /// <returns>A task that represents the asynchronous operation.</returns>
    [Fact]
    public async Task VerifyAsync_UnknownKey_Fails()
    {
        ApiKeyVerifier verifier = new(Options, new FakeApiKeyStore(storedKey: null), new FakePepperProvider("pepper"));

        ApiKeyVerification result = await verifier.VerifyAsync(
            "Bearer mxgw_missing_secret",
            CancellationToken.None);

        Assert.False(result.Succeeded);
        Assert.Equal(ApiKeyFailure.KeyNotFound, result.Failure);
    }

    /// <summary>Verifies that VerifyAsync fails for a wrong secret (constant-time compare rejects it).</summary>
    /// <returns>A task that represents the asynchronous operation.</returns>
    [Fact]
    public async Task VerifyAsync_WrongSecret_Fails()
    {
        FakeApiKeyStore store = new(CreateRecord("pepper", revokedUtc: null));
        ApiKeyVerifier verifier = new(Options, store, new FakePepperProvider("pepper"));

        ApiKeyVerification result = await verifier.VerifyAsync(
            "Bearer mxgw_operator01_wrong-secret",
            CancellationToken.None);

        Assert.False(result.Succeeded);
        Assert.Equal(ApiKeyFailure.SecretMismatch, result.Failure);
        Assert.False(store.MarkedUsed);
    }

    /// <summary>Verifies that VerifyAsync fails for a revoked key.</summary>
    /// <returns>A task that represents the asynchronous operation.</returns>
    [Fact]
    public async Task VerifyAsync_RevokedKey_Fails()
    {
        FakeApiKeyStore store = new(CreateRecord("pepper", DateTimeOffset.UtcNow));
        ApiKeyVerifier verifier = new(Options, store, new FakePepperProvider("pepper"));

        ApiKeyVerification result = await verifier.VerifyAsync(
            "Bearer mxgw_operator01_correct-secret",
            CancellationToken.None);

        Assert.False(result.Succeeded);
        Assert.Equal(ApiKeyFailure.KeyRevoked, result.Failure);
        Assert.False(store.MarkedUsed);
    }

    /// <summary>Verifies that VerifyAsync fails closed when the pepper is missing.</summary>
    /// <returns>A task that represents the asynchronous operation.</returns>
    [Fact]
    public async Task VerifyAsync_MissingPepper_Fails()
    {
        FakeApiKeyStore store = new(CreateRecord("pepper", revokedUtc: null));
        ApiKeyVerifier verifier = new(Options, store, new FakePepperProvider(pepper: null));

        ApiKeyVerification result = await verifier.VerifyAsync(
            "Bearer mxgw_operator01_correct-secret",
            CancellationToken.None);

        Assert.False(result.Succeeded);
        Assert.Equal(ApiKeyFailure.PepperUnavailable, result.Failure);
    }

    /// <summary>Computes HMAC-SHA256(pepper, secret) — the documented peppered-hash format.</summary>
    private static byte[] PepperedHash(string secret, string pepper)
    {
        using HMACSHA256 hmac = new(Encoding.UTF8.GetBytes(pepper));
        return hmac.ComputeHash(Encoding.UTF8.GetBytes(secret));
    }

    private static ApiKeyRecord CreateRecord(string pepper, DateTimeOffset? revokedUtc)
    {
        return new ApiKeyRecord(
            KeyId: "operator01",
            KeyPrefix: "mxgw",
            SecretHash: PepperedHash("correct-secret", pepper),
            DisplayName: "Operator Key",
            Scopes: new HashSet<string>(StringComparer.Ordinal)
            {
                "session:open",
                "events:read"
            },
            ConstraintsJson: null,
            CreatedUtc: DateTimeOffset.UtcNow,
            LastUsedUtc: null,
            RevokedUtc: revokedUtc);
    }

    private sealed class FakePepperProvider(string? pepper) : IApiKeyPepperProvider
    {
        /// <summary>Returns the configured pepper (or null to simulate an unavailable pepper).</summary>
        /// <returns>The configured pepper, or null to simulate an unavailable pepper.</returns>
        public string? GetPepper() => pepper;
    }

    /// <summary>Fake in-memory API key store for testing.</summary>
    private sealed class FakeApiKeyStore(ApiKeyRecord? storedKey) : IApiKeyStore
    {
        /// <summary>Gets whether the key was marked as used.</summary>
        public bool MarkedUsed { get; private set; }

        /// <summary>Returns the fake's single stored key if its ID matches, regardless of revocation.</summary>
        /// <param name="keyId">Key ID to look up.</param>
        /// <param name="ct">Cancellation token.</param>
        /// <returns>The matching stored key record, or null if no key matches.</returns>
        public Task<ApiKeyRecord?> FindByKeyIdAsync(string keyId, CancellationToken ct)
        {
            return Task.FromResult(storedKey?.KeyId == keyId ? storedKey : null);
        }

        /// <summary>Returns the fake's single stored key if its ID matches and it has not been revoked.</summary>
        /// <param name="keyId">Key ID to look up.</param>
        /// <param name="ct">Cancellation token.</param>
        /// <returns>The matching, non-revoked stored key record, or null otherwise.</returns>
        public Task<ApiKeyRecord?> FindActiveByKeyIdAsync(string keyId, CancellationToken ct)
        {
            return Task.FromResult(
                storedKey?.KeyId == keyId && storedKey.RevokedUtc is null
                    ? storedKey
                    : null);
        }

        /// <summary>Records that the stored key was used, exposed via <see cref="MarkedUsed"/> for assertions.</summary>
        /// <param name="keyId">Key ID that was used.</param>
        /// <param name="whenUtc">Timestamp of use (unused by the fake).</param>
        /// <param name="ct">Cancellation token.</param>
        /// <returns>A task that represents the asynchronous operation.</returns>
        public Task MarkUsedAsync(string keyId, DateTimeOffset whenUtc, CancellationToken ct)
        {
            MarkedUsed = storedKey?.KeyId == keyId;

            return Task.CompletedTask;
        }
    }
}
