using ZB.MOM.WW.MxGateway.Contracts.Proto;

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

/// <summary>
/// Unit tests for <see cref="MxGatewaySecretRedaction"/> — the exact-substring scrub applied to
/// diagnostic text and rebuilt exceptions before they leave the client on a failure path.
/// </summary>
public sealed class MxGatewaySecretRedactionTests
{
    [Fact]
    public void Redact_ReplacesEveryOccurrenceOfSecret()
    {
        string result = MxGatewaySecretRedaction.Redact(
            "pw=hunter2 retry pw=hunter2 again hunter2",
            "hunter2");

        Assert.DoesNotContain("hunter2", result, StringComparison.Ordinal);
        Assert.Equal("pw=<redacted> retry pw=<redacted> again <redacted>", result);
    }

    [Fact]
    public void Redact_ScrubsBothSecretsWhenOneIsSubstringOfTheOther()
    {
        // "secret" is a substring of "secretPassword"; both must be fully scrubbed regardless of
        // supplied order — no residual leak of either verbatim value.
        string result = MxGatewaySecretRedaction.Redact(
            "a=secretPassword b=secret",
            "secret",
            "secretPassword");

        Assert.DoesNotContain("secretPassword", result, StringComparison.Ordinal);
        Assert.DoesNotContain("secret", result, StringComparison.Ordinal);
    }

    [Fact]
    public void Redact_WithNullSecretsArray_ReturnsMessageUnchanged()
    {
        const string message = "nothing to scrub here";

        string result = MxGatewaySecretRedaction.Redact(message, null!);

        Assert.Equal(message, result);
    }

    [Fact]
    public void Redact_WithEmptySecretsArray_ReturnsMessageUnchanged()
    {
        const string message = "nothing to scrub here";

        string result = MxGatewaySecretRedaction.Redact(message);

        Assert.Equal(message, result);
    }

    [Fact]
    public void Redact_IgnoresWhitespaceOnlySecret()
    {
        // A whitespace-only secret must not over-redact the internal spaces of the message.
        const string message = "user operator logged in";

        string result = MxGatewaySecretRedaction.Redact(message, " ");

        Assert.Equal(message, result);
    }

    [Fact]
    public void Redacted_PreservesConcreteSubtypeAndDoesNotChainSecretBearingOriginal()
    {
        const string secret = "hunter2";
        Exception transportCause = new InvalidOperationException("transport reset");
        MxGatewaySessionException original = new(
            $"session rejected credential '{secret}'",
            "session-1",
            "correlation-1",
            new ProtocolStatus { Code = ProtocolStatusCode.SessionNotReady, Message = $"echoed '{secret}'" },
            hResult: -1,
            statuses: [new MxStatusProxy { DiagnosticText = $"denied '{secret}'" }],
            innerException: transportCause);

        MxGatewayException redacted = MxGatewaySecretRedaction.Redacted(original, secret);

        // Concrete runtime type is preserved.
        Assert.IsType<MxGatewaySessionException>(redacted);
        // The secret is gone from the message and every structured accessor.
        Assert.DoesNotContain(secret, redacted.Message, StringComparison.Ordinal);
        Assert.DoesNotContain(secret, redacted.ToString(), StringComparison.Ordinal);
        Assert.DoesNotContain(secret, redacted.ProtocolStatus!.Message, StringComparison.Ordinal);
        Assert.All(redacted.Statuses, status =>
            Assert.DoesNotContain(secret, status.DiagnosticText, StringComparison.Ordinal));
        Assert.Contains("<redacted>", redacted.Message, StringComparison.Ordinal);
        // The secret-bearing original is NOT chained; the original's transport cause is carried.
        Assert.NotSame(original, redacted.InnerException);
        Assert.Same(transportCause, redacted.InnerException);
    }
}
