using ZB.MOM.WW.Secrets.Abstractions;
using ZB.MOM.WW.Secrets.Replicator.AkkaDotNet.Protocol;

namespace ZB.MOM.WW.Secrets.Replicator.AkkaDotNet.Tests;

/// <summary>
/// The DTO is the trust boundary for anything a peer sends. Every rejection here must surface as an
/// <see cref="ArgumentException"/>, because that is the exception type the receiving actor filters
/// on — anything else escapes its handler, restarts the actor, and (since the row is redelivered)
/// loops.
/// </summary>
public sealed class HostileWireInputTests
{
    private static SecretRowDto Valid() => new()
    {
        Name = "app/ok",
        ContentType = "Text",
        Ciphertext = [1],
        Nonce = [2],
        Tag = [3],
        WrappedDek = [4],
        WrapNonce = [5],
        WrapTag = [6],
        KekId = "sha256:x",
        Revision = 0,
        CreatedUtc = "2026-01-01T00:00:00.0000000+00:00",
        UpdatedUtc = "2026-01-01T00:00:00.0000000+00:00",
    };

    [Fact]
    public void A_valid_row_materializes()
    {
        StoredSecret row = Valid().ToStoredSecret();

        Assert.Equal("app/ok", row.Name.Value);
        Assert.Equal(SecretContentType.Text, row.ContentType);
    }

    [Theory]
    [InlineData("../../etc/passwd")]
    [InlineData("/rooted")]
    [InlineData("has space")]
    [InlineData("")]
    public void A_malformed_name_is_rejected(string name)
    {
        Assert.Throws<ArgumentException>(() => (Valid() with { Name = name }).ToStoredSecret());
    }

    [Theory]
    // An in-range number names no member — Enum.Parse would happily produce (SecretContentType)4096
    // and the store would persist it for later readers that assume a defined value.
    [InlineData("4096")]
    // Overflows the underlying type: Enum.Parse throws OverflowException, which is NOT an
    // ArgumentException, so it would escape the actor's filter and restart it.
    [InlineData("99999999999999999999")]
    [InlineData("NotAContentType")]
    [InlineData("")]
    public void An_unrecognized_content_type_is_rejected_as_an_ArgumentException(string contentType)
    {
        Assert.Throws<ArgumentException>(
            () => (Valid() with { ContentType = contentType }).ToStoredSecret());
    }

    [Fact]
    public void A_null_crypto_blob_is_rejected_at_the_boundary()
    {
        // `required byte[]` means "present in the payload", not "non-null" — an explicit JSON null
        // satisfies it. Caught here, or it blows up deep in the store's parameter binding instead.
        Assert.Throws<ArgumentException>(
            () => (Valid() with { Ciphertext = null! }).ToStoredSecret());
        Assert.Throws<ArgumentException>(
            () => (Valid() with { WrappedDek = null! }).ToStoredSecret());
    }

    [Fact]
    public void A_malformed_timestamp_is_rejected()
    {
        Assert.ThrowsAny<FormatException>(
            () => (Valid() with { UpdatedUtc = "not-a-date" }).ToStoredSecret());
    }
}
