using DodoSSH.Crypto;

namespace DodoSSH.Crypto.Tests;

/// <summary>
/// Pins the specification constants that are written into stored data.
/// </summary>
/// <remarks>
/// These are not busywork. The envelope magic, AAD version and every enum value are persisted
/// in ciphertext rows or in the AAD they are bound to, and only clients can re-encrypt: if one
/// changes without a deliberate migration path, existing vaults stop decrypting and the server
/// cannot help.
/// </remarks>
public sealed class CryptoSpecTests
{
    [Fact]
    public void EnvelopeMagic_IsStable()
    {
        CryptoSpec.EnvelopeMagic.ToArray().ShouldBe("DSH1"u8.ToArray());
    }

    [Fact]
    public void AadMagic_IsStable()
    {
        CryptoSpec.AadMagic.ToArray().ShouldBe("dsh1\n"u8.ToArray());
    }

    [Fact]
    public void CurrentAadVersion_IsStable()
    {
        // Bumping this requires a lazy re-encrypt-on-write path in the client first.
        CryptoSpec.CurrentAadVersion.ShouldBe((byte)1);
    }

    [Fact]
    public void CurrentSchemaVersion_IsStable()
    {
        CryptoSpec.CurrentSchemaVersion.ShouldBe((ushort)1);
    }

    [Fact]
    public void Sizes_MatchTheSpecification()
    {
        CryptoSpec.AadEncodedLength.ShouldBe(64);
        CryptoSpec.SymmetricKeySize.ShouldBe(32);
        CryptoSpec.PublicKeySize.ShouldBe(32);
        CryptoSpec.SignatureSize.ShouldBe(64);
        CryptoSpec.DigestSize.ShouldBe(32);
        CryptoSpec.TagSize.ShouldBe(16);
        CryptoSpec.SaltSize.ShouldBe(16);
    }

    [Theory]
    [InlineData(CryptoSpec.AlgorithmId.XChaCha20Poly1305, 1)]
    [InlineData(CryptoSpec.AlgorithmId.Aes256Gcm, 2)]
    [InlineData(CryptoSpec.AlgorithmId.SealToX25519, 3)]
    public void AlgorithmId_HasStableWireValue(CryptoSpec.AlgorithmId algorithm, int expected)
    {
        ((int)algorithm).ShouldBe(expected);
    }

    [Fact]
    public void AlgorithmId_4_IsReservedForHybridPostQuantumSeal()
    {
        // Reserved for X25519 + ML-KEM-768. Claimed now so the identifier cannot be reused:
        // store-now-decrypt-later is a real threat for long-lived SSH keys.
        Enum.IsDefined(typeof(CryptoSpec.AlgorithmId), (byte)4).ShouldBeFalse();
    }

    [Theory]
    [InlineData(CryptoSpec.AadPurpose.UserSecretBundle, 1)]
    [InlineData(CryptoSpec.AadPurpose.VaultKeyGrant, 2)]
    [InlineData(CryptoSpec.AadPurpose.ItemDataKey, 3)]
    [InlineData(CryptoSpec.AadPurpose.ItemPayload, 4)]
    [InlineData(CryptoSpec.AadPurpose.ItemMetadata, 5)]
    [InlineData(CryptoSpec.AadPurpose.LocalCache, 6)]
    public void AadPurpose_HasStableWireValue(CryptoSpec.AadPurpose purpose, int expected)
    {
        ((int)purpose).ShouldBe(expected);
    }

    [Theory]
    [InlineData(CryptoSpec.AadResourceType.User, 1)]
    [InlineData(CryptoSpec.AadResourceType.Device, 2)]
    [InlineData(CryptoSpec.AadResourceType.Vault, 3)]
    [InlineData(CryptoSpec.AadResourceType.Host, 4)]
    [InlineData(CryptoSpec.AadResourceType.Credential, 5)]
    [InlineData(CryptoSpec.AadResourceType.SshKey, 6)]
    [InlineData(CryptoSpec.AadResourceType.HostGroup, 7)]
    [InlineData(CryptoSpec.AadResourceType.Tag, 8)]
    [InlineData(CryptoSpec.AadResourceType.Snippet, 9)]
    [InlineData(CryptoSpec.AadResourceType.PortForward, 10)]
    [InlineData(CryptoSpec.AadResourceType.KnownHostKey, 11)]
    [InlineData(CryptoSpec.AadResourceType.HostTag, 12)]
    [InlineData(CryptoSpec.AadResourceType.HostCredential, 13)]
    [InlineData(CryptoSpec.AadResourceType.ConnectionLogEntry, 14)]
    [InlineData(CryptoSpec.AadResourceType.ActivityLogEntry, 15)]
    [InlineData(CryptoSpec.AadResourceType.ObjectStore, 16)]
    public void AadResourceType_HasStableWireValue(CryptoSpec.AadResourceType type, int expected)
    {
        ((int)type).ShouldBe(expected);
    }

    [Fact]
    public void DerivationLabels_AreStable()
    {
        // These are HKDF info strings; changing one silently derives a different key.
        CryptoSpec.DerivationLabels.PassphraseKek.ToArray()
            .ShouldBe("dsh1/kek/passphrase/v1"u8.ToArray());
        // v2 since 2026-07-30: the cache key derives from the bundle rather than the master key, so that a
        // device or recovery unlock reaches the same cache. docs/crypto.md §3.2. Bumping the label is what
        // makes a v1 cache fail to open rather than decrypt to nonsense.
        CryptoSpec.DerivationLabels.LocalCache.ToArray()
            .ShouldBe("dsh1/localcache/v2"u8.ToArray());
        CryptoSpec.DerivationLabels.SealTo.ToArray()
            .ShouldBe("dsh1/sealto/v1|"u8.ToArray());
        CryptoSpec.DerivationLabels.Fingerprint.ToArray()
            .ShouldBe("dsh1/fp/v1"u8.ToArray());
    }

    [Fact]
    public void SigningContexts_AreStable()
    {
        CryptoSpec.SigningContexts.KeyStatement.ToArray()
            .ShouldBe("dsh1/sig/keystatement/v1"u8.ToArray());
        CryptoSpec.SigningContexts.Grant.ToArray()
            .ShouldBe("dsh1/sig/grant/v1"u8.ToArray());
        CryptoSpec.SigningContexts.Attestation.ToArray()
            .ShouldBe("dsh1/sig/attestation/v1"u8.ToArray());
    }

    [Fact]
    public void SigningContexts_AreAllDistinct()
    {
        // A shared context would let a signature in one role be replayed in another.
        string[] contexts =
        [
            System.Text.Encoding.UTF8.GetString(CryptoSpec.SigningContexts.KeyStatement),
            System.Text.Encoding.UTF8.GetString(CryptoSpec.SigningContexts.Grant),
            System.Text.Encoding.UTF8.GetString(CryptoSpec.SigningContexts.Attestation),
        ];

        contexts.Distinct(StringComparer.Ordinal).Count().ShouldBe(contexts.Length);
    }
}
