using System.Text.Json;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Options;
using ZB.MOM.WW.MxGateway.Server.Configuration;
using ZB.MOM.WW.MxGateway.Server.Security.Authentication;

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

public sealed class ApiKeyVerifierTests
{
    /// <summary>Verifies that VerifyAsync returns identity and scopes for a valid key.</summary>
    [Fact]
    public async Task VerifyAsync_ValidKey_ReturnsIdentityAndScopes()
    {
        ApiKeySecretHasher hasher = CreateHasher("pepper");
        FakeApiKeyStore store = new(CreateRecord(hasher, revokedUtc: null));
        ApiKeyVerifier verifier = new(new ApiKeyParser(), hasher, store);

        ApiKeyVerificationResult 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>
    [Fact]
    public async Task VerifyAsync_ValidKey_DoesNotExposeRawSecretInResult()
    {
        ApiKeySecretHasher hasher = CreateHasher("pepper");
        FakeApiKeyStore store = new(CreateRecord(hasher, revokedUtc: null));
        ApiKeyVerifier verifier = new(new ApiKeyParser(), hasher, store);

        ApiKeyVerificationResult 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 with unauthenticated status for a malformed key.</summary>
    /// <param name="authorizationHeader">Authorization header value to test.</param>
    [Theory]
    [InlineData(null)]
    [InlineData("Bearer mxgw_operator01")]
    [InlineData("Bearer wrong")]
    public async Task VerifyAsync_MalformedKey_FailsUnauthenticated(string? authorizationHeader)
    {
        ApiKeyVerifier verifier = new(
            new ApiKeyParser(),
            CreateHasher("pepper"),
            new FakeApiKeyStore(storedKey: null));

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

        Assert.False(result.Succeeded);
        Assert.Equal(ApiKeyVerificationFailure.MissingOrMalformedCredentials, result.Failure);
    }

    /// <summary>Verifies that VerifyAsync fails for an unknown key.</summary>
    [Fact]
    public async Task VerifyAsync_UnknownKey_Fails()
    {
        ApiKeyVerifier verifier = new(
            new ApiKeyParser(),
            CreateHasher("pepper"),
            new FakeApiKeyStore(storedKey: null));

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

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

    /// <summary>Verifies that VerifyAsync fails for a wrong secret.</summary>
    [Fact]
    public async Task VerifyAsync_WrongSecret_Fails()
    {
        ApiKeySecretHasher hasher = CreateHasher("pepper");
        FakeApiKeyStore store = new(CreateRecord(hasher, revokedUtc: null));
        ApiKeyVerifier verifier = new(new ApiKeyParser(), hasher, store);

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

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

    /// <summary>Verifies that VerifyAsync fails for a revoked key.</summary>
    [Fact]
    public async Task VerifyAsync_RevokedKey_Fails()
    {
        ApiKeySecretHasher hasher = CreateHasher("pepper");
        FakeApiKeyStore store = new(CreateRecord(hasher, DateTimeOffset.UtcNow));
        ApiKeyVerifier verifier = new(new ApiKeyParser(), hasher, store);

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

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

    /// <summary>Verifies that VerifyAsync fails when the pepper is missing.</summary>
    [Fact]
    public async Task VerifyAsync_MissingPepper_Fails()
    {
        FakeApiKeyStore store = new(CreateRecord(CreateHasher("pepper"), revokedUtc: null));
        ApiKeyVerifier verifier = new(new ApiKeyParser(), CreateHasher(pepper: null), store);

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

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

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

    private static ApiKeySecretHasher CreateHasher(string? pepper)
    {
        Dictionary<string, string?> values = [];

        if (pepper is not null)
        {
            values["TestPepper"] = pepper;
        }

        IConfigurationRoot configuration = new ConfigurationBuilder()
            .AddInMemoryCollection(values)
            .Build();

        GatewayOptions options = new()
        {
            Authentication = new AuthenticationOptions
            {
                PepperSecretName = "TestPepper"
            }
        };

        return new ApiKeySecretHasher(configuration, Options.Create(options));
    }

    /// <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>Finds an API key record by its ID.</summary>
        /// <param name="keyId">Identifier of the API key.</param>
        /// <param name="cancellationToken">Token to cancel the asynchronous operation.</param>
        public Task<ApiKeyRecord?> FindByKeyIdAsync(string keyId, CancellationToken cancellationToken)
        {
            return Task.FromResult(storedKey?.KeyId == keyId ? storedKey : null);
        }

        /// <summary>Finds an active (non-revoked) API key record by its ID.</summary>
        /// <param name="keyId">Identifier of the API key.</param>
        /// <param name="cancellationToken">Token to cancel the asynchronous operation.</param>
        public Task<ApiKeyRecord?> FindActiveByKeyIdAsync(string keyId, CancellationToken cancellationToken)
        {
            return Task.FromResult(
                storedKey?.KeyId == keyId && storedKey.RevokedUtc is null
                    ? storedKey
                    : null);
        }

        /// <summary>Marks an API key as used at the specified time.</summary>
        /// <param name="keyId">Identifier of the API key.</param>
        /// <param name="usedUtc">Timestamp when the key was used.</param>
        /// <param name="cancellationToken">Token to cancel the asynchronous operation.</param>
        public Task MarkKeyUsedAsync(string keyId, DateTimeOffset usedUtc, CancellationToken cancellationToken)
        {
            MarkedUsed = storedKey?.KeyId == keyId;

            return Task.CompletedTask;
        }
    }
}
