using NSec.Cryptography;

namespace DodoSSH.Crypto.Tests;

/// <summary>Key statement signing and verification. See docs/crypto.md §7.</summary>
public sealed class DshSignaturesTests
{
    [Fact]
    public void AFreshSignature_Verifies()
    {
        using var key = CreateSigningKey();
        var statement = Canonical(key);

        var signature = DshSignatures.SignKeyStatement(key, statement);

        signature.Length.ShouldBe(CryptoSpec.SignatureSize);
        DshSignatures.VerifyKeyStatement(PublicKeyBytes(key), statement, signature).ShouldBeTrue();
    }

    [Fact]
    public void ATamperedStatement_DoesNotVerify()
    {
        using var key = CreateSigningKey();
        var statement = Canonical(key);
        var signature = DshSignatures.SignKeyStatement(key, statement);

        var tampered = statement.ToArray();
        tampered[^1] ^= 0x01;

        DshSignatures.VerifyKeyStatement(PublicKeyBytes(key), tampered, signature).ShouldBeFalse();
    }

    [Fact]
    public void AnotherKeysSignature_DoesNotVerify()
    {
        using var key = CreateSigningKey();
        using var other = CreateSigningKey();
        var statement = Canonical(key);

        var signature = DshSignatures.SignKeyStatement(other, statement);

        DshSignatures.VerifyKeyStatement(PublicKeyBytes(key), statement, signature).ShouldBeFalse();
    }

    [Fact]
    public void ASignatureMissingTheSigningContext_DoesNotVerify()
    {
        // Domain separation. A signature over the bare canonical encoding must not be accepted as a
        // key statement signature, or the same bytes could be replayed into another role.
        using var key = CreateSigningKey();
        var statement = Canonical(key);

        var contextless = SignatureAlgorithm.Ed25519.Sign(key, statement);

        DshSignatures.VerifyKeyStatement(PublicKeyBytes(key), statement, contextless).ShouldBeFalse();
    }

    [Theory]
    [InlineData(0)]
    [InlineData(31)]
    [InlineData(33)]
    public void AMalformedPublicKey_ReturnsFalseRatherThanThrowing(int length)
    {
        // These values arrive from an untrusted server, so rejection has to be an ordinary outcome.
        using var key = CreateSigningKey();
        var statement = Canonical(key);
        var signature = DshSignatures.SignKeyStatement(key, statement);

        DshSignatures.VerifyKeyStatement(new byte[length], statement, signature).ShouldBeFalse();
    }

    [Theory]
    [InlineData(0)]
    [InlineData(63)]
    [InlineData(65)]
    public void AMalformedSignature_ReturnsFalseRatherThanThrowing(int length)
    {
        using var key = CreateSigningKey();

        DshSignatures.VerifyKeyStatement(PublicKeyBytes(key), Canonical(key), new byte[length])
            .ShouldBeFalse();
    }

    private static Key CreateSigningKey() =>
        Key.Create(
            SignatureAlgorithm.Ed25519,
            new KeyCreationParameters { ExportPolicy = KeyExportPolicies.AllowPlaintextExport });

    private static byte[] PublicKeyBytes(Key key) => key.PublicKey.Export(KeyBlobFormat.RawPublicKey);

    private static byte[] Canonical(Key signingKey) =>
        KeyStatementCodec.Encode(new KeyStatementFields(
            Version: 1,
            Issuer: "https://idp.example/realms/dodossh",
            Subject: "alice-subject",
            Email: "alice@example.com",
            EncryptionPublicKey: TestKeys.Encryption,
            SigningPublicKey: PublicKeyBytes(signingKey),
            KeyGeneration: 1,
            CreatedAt: DateTimeOffset.FromUnixTimeMilliseconds(1_750_000_000_123),
            DeviceName: "alice-laptop"));
}
