using ZB.MOM.WW.MxGateway.Server.Diagnostics;

namespace ZB.MOM.WW.MxGateway.Tests.Diagnostics;

public sealed class GatewayLogRedactorTests
{
    /// <summary>Verifies that RedactApiKey preserves the key ID and removes the secret.</summary>
    [Fact]
    public void RedactApiKey_PreservesKeyIdAndRemovesSecret()
    {
        string? redacted = GatewayLogRedactor.RedactApiKey("Bearer mxgw_operator01_super-secret");

        Assert.Equal("Bearer mxgw_operator01_[redacted]", redacted);
        Assert.DoesNotContain("super-secret", redacted);
    }

    /// <summary>Verifies that RedactApiKey removes secrets containing underscores.</summary>
    [Fact]
    public void RedactApiKey_RemovesSecretContainingUnderscores()
    {
        string? redacted = GatewayLogRedactor.RedactApiKey("Bearer mxgw_operator01_super_secret_value");

        Assert.Equal("Bearer mxgw_operator01_[redacted]", redacted);
        Assert.DoesNotContain("super_secret_value", redacted);
    }

    /// <summary>
    /// Verifies that a bearer credential the gateway does not issue is redacted too. A client that
    /// pastes a JWT (or any other token) into the authorization header must not have it logged.
    /// </summary>
    [Fact]
    public void RedactClientIdentity_RedactsNonGatewayBearerCredential()
    {
        const string token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxIn0.c2lnbmF0dXJl";

        string? redacted = GatewayLogRedactor.RedactClientIdentity($"Bearer {token}");

        Assert.Equal("Bearer [redacted]", redacted);
        Assert.DoesNotContain(token, redacted, StringComparison.Ordinal);
        Assert.DoesNotContain("eyJ", redacted, StringComparison.Ordinal);
    }

    /// <summary>Verifies that a gateway API key keeps its key-id shape so an operator can still tell keys apart.</summary>
    [Fact]
    public void RedactClientIdentity_PreservesGatewayKeyIdShape()
    {
        string? redacted = GatewayLogRedactor.RedactClientIdentity("Bearer mxgw_operator01_super-secret");

        Assert.Equal("Bearer mxgw_operator01_[redacted]", redacted);
        Assert.DoesNotContain("super-secret", redacted, StringComparison.Ordinal);
    }

    /// <summary>
    /// Pins the 64-character key-id boundary in both directions. Nothing validates key-id length at
    /// creation time (neither <c>ApiKeyAdminCommandLineParser.IsValidKeyId</c> nor
    /// <c>DashboardApiKeyManagementService.ValidateKeyId</c> caps it), so the cap here is a redaction
    /// heuristic that a real key id can cross. This test fixes which way it fails when it does: at
    /// exactly 64 the id is still an identifier and survives; at 65 the whole run is treated as secret
    /// material and goes. Losing an identifier is the cheap failure; logging a secret is not.
    /// </summary>
    /// <param name="keyIdLength">Length of the key id presented before the secret separator.</param>
    /// <param name="expectsKeyIdPreserved">Whether the key id must survive redaction at that length.</param>
    [Theory]
    [InlineData(64, true)]
    [InlineData(65, false)]
    public void RedactClientIdentity_KeyIdLengthBoundary_FailsTowardRedaction(
        int keyIdLength,
        bool expectsKeyIdPreserved)
    {
        string keyId = new('a', keyIdLength);

        string? redacted = GatewayLogRedactor.RedactClientIdentity($"Bearer mxgw_{keyId}_super-secret");

        Assert.Equal(
            expectsKeyIdPreserved ? $"Bearer mxgw_{keyId}_[redacted]" : "Bearer mxgw_[redacted]",
            redacted);
        Assert.DoesNotContain("super-secret", redacted, StringComparison.Ordinal);
    }

    /// <summary>
    /// Verifies that anything not recognized as a gateway API key fails closed: the scheme word
    /// survives only when it looks like an auth scheme, and the credential never does.
    /// </summary>
    /// <param name="clientIdentity">The raw client identity value.</param>
    /// <param name="expected">The expected redacted value.</param>
    [Theory]
    [InlineData("Bearer", "[redacted]")]
    [InlineData("Bearer   ", "[redacted]")]
    [InlineData("Basic dXNlcjpwYXNzd29yZA==", "Basic [redacted]")]
    [InlineData("Negotiate YIIJvwYGKwYBBQUCoIIJ", "Negotiate [redacted]")]
    [InlineData("mxgw_operator01_super-secret", "[redacted]")]
    [InlineData("mxgw_operator01_super-secret trailing", "[redacted]")]
    [InlineData("Bearer mxgw_operator01", "Bearer mxgw_[redacted]")]
    [InlineData("Bearer mxgw_", "Bearer mxgw_[redacted]")]
    [InlineData("Bearer mxgw__super-secret", "Bearer mxgw_[redacted]")]
    [InlineData("bearer mxgw_operator01_super-secret", "bearer mxgw_operator01_[redacted]")]
    [InlineData("anonymous", "[redacted]")]
    [InlineData("some random junk", "[redacted]")]
    public void RedactClientIdentity_FailsClosedForUnrecognizedCredentials(string clientIdentity, string expected)
    {
        Assert.Equal(expected, GatewayLogRedactor.RedactClientIdentity(clientIdentity));
    }

    /// <summary>Verifies that a blank client identity is passed through — there is nothing to redact.</summary>
    /// <param name="clientIdentity">The raw client identity value.</param>
    [Theory]
    [InlineData(null)]
    [InlineData("")]
    [InlineData("   ")]
    public void RedactClientIdentity_PassesThroughBlankValues(string? clientIdentity)
    {
        Assert.Equal(clientIdentity, GatewayLogRedactor.RedactClientIdentity(clientIdentity));
    }

    /// <summary>Verifies that IsCredentialBearingCommand identifies credential-bearing MXAccess commands.</summary>
    /// <param name="commandMethod">Name of the MXAccess command method.</param>
    [Theory]
    [InlineData("AuthenticateUser")]
    [InlineData("WriteSecured")]
    [InlineData("WriteSecured2")]
    public void IsCredentialBearingCommand_IdentifiesSensitiveMxAccessCommands(string commandMethod)
    {
        Assert.True(GatewayLogRedactor.IsCredentialBearingCommand(commandMethod));
    }

    /// <summary>Verifies that RedactCommandValue does not log raw values by default.</summary>
    [Fact]
    public void RedactCommandValue_DoesNotLogRawValuesByDefault()
    {
        object? redacted = GatewayLogRedactor.RedactCommandValue("Write", "plaintext-tag-value");

        Assert.Equal("[redacted]", redacted);
    }

    /// <summary>Verifies that RedactCommandValue redacts secured writes even when value logging is enabled.</summary>
    [Fact]
    public void RedactCommandValue_RedactsSecuredWriteEvenWhenValueLoggingIsEnabled()
    {
        object? redacted = GatewayLogRedactor.RedactCommandValue(
            "WriteSecured",
            "credential-bearing-value",
            valueLoggingEnabled: true);

        Assert.Equal("[redacted]", redacted);
    }

    /// <summary>Verifies that RedactCommandValue allows non-sensitive values only when value logging is enabled.</summary>
    [Fact]
    public void RedactCommandValue_AllowsNonSensitiveValueOnlyWhenValueLoggingIsEnabled()
    {
        object? redacted = GatewayLogRedactor.RedactCommandValue(
            "Write",
            "diagnostic-value",
            valueLoggingEnabled: true);

        Assert.Equal("diagnostic-value", redacted);
    }

    /// <summary>Verifies that LogScope redacts client identity before scope state is created.</summary>
    [Fact]
    public void LogScope_RedactsClientIdentityBeforeScopeStateIsCreated()
    {
        GatewayLogScope scope = new(
            SessionId: "session-1",
            WorkerProcessId: 1234,
            CorrelationId: 99,
            CommandMethod: "AuthenticateUser",
            ClientIdentity: "Bearer mxgw_admin_secret");

        IReadOnlyDictionary<string, object?> values = scope.ToDictionary();

        Assert.Equal("session-1", values["SessionId"]);
        Assert.Equal(1234, values["WorkerProcessId"]);
        Assert.Equal((ulong)99, values["CorrelationId"]);
        Assert.Equal("AuthenticateUser", values["CommandMethod"]);
        Assert.Equal("Bearer mxgw_admin_[redacted]", values["ClientIdentity"]);
    }
}
