using System.Buffers.Binary;
using System.Globalization;
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
using System.Text.Json.Nodes;
using DodoSSH.Crypto;
using NSec.Cryptography;

namespace DodoSSH.Crypto.Tests;

/// <summary>
/// Produces the deterministic byte-level results of the DSH1 specification.
/// </summary>
/// <remarks>
/// Only deterministic operations belong here. <c>SealTo</c> and signing draw fresh randomness,
/// so they are covered by round-trip and negative tests in <see cref="DshCryptoTests"/> instead.
/// </remarks>
internal static class GoldenVectors
{
    private static readonly Guid ResourceA = Guid.Parse("0192f0c8-1a2b-7c3d-8e4f-5a6b7c8d9e0f");
    private static readonly Guid ResourceB = Guid.Parse("0192f0c8-1a2b-7c3d-8e4f-5a6b7c8d9e10");
    private static readonly Guid KeyIdA = Guid.Parse("0192f0c8-9999-7aaa-8bbb-cccccccccccc");

    internal static string Generate()
    {
        var root = new JsonObject
        {
            ["_comment"] = "Generated by DodoSSH.Crypto.Tests.GoldenVectors. Normative source: docs/crypto.md.",
            ["_warning"] = "A failing assertion here is a regression or an intentional versioned format change. Do not regenerate to make it pass.",
            ["specVersion"] = 1,
            ["aad"] = BuildAadVectors(),
            ["envelope"] = BuildEnvelopeVectors(),
            ["aead"] = BuildAeadVectors(),
            ["hkdf"] = BuildHkdfVectors(),
            ["localCacheKey"] = BuildLocalCacheKeyVectors(),
            ["argon2id"] = BuildArgon2Vectors(),
            ["fingerprint"] = BuildFingerprintVectors(),
            ["keyStatement"] = BuildKeyStatementVectors(),
            ["keyLog"] = BuildKeyLogVectors(),
        };

        return root.ToJsonString(new JsonSerializerOptions { WriteIndented = true }) + "\n";
    }

    private static JsonArray BuildAadVectors()
    {
        (string Name, AadDescriptor Descriptor)[] cases =
        [
            ("item-payload", AadDescriptor.Create(
                CryptoSpec.AadPurpose.ItemPayload,
                CryptoSpec.AadResourceType.Credential,
                ResourceA,
                KeyIdA,
                keyGeneration: 7,
                itemVersion: 3)),
            ("item-payload-other-resource", AadDescriptor.Create(
                CryptoSpec.AadPurpose.ItemPayload,
                CryptoSpec.AadResourceType.Credential,
                ResourceB,
                KeyIdA,
                keyGeneration: 7,
                itemVersion: 3)),
            ("item-metadata", AadDescriptor.Create(
                CryptoSpec.AadPurpose.ItemMetadata,
                CryptoSpec.AadResourceType.Host,
                ResourceA,
                keyGeneration: 1,
                itemVersion: 1)),
            ("vault-key-grant", AadDescriptor.Create(
                CryptoSpec.AadPurpose.VaultKeyGrant,
                CryptoSpec.AadResourceType.Vault,
                ResourceA,
                keyGeneration: 2)),
            ("user-secret-bundle", AadDescriptor.Create(
                CryptoSpec.AadPurpose.UserSecretBundle,
                CryptoSpec.AadResourceType.User,
                ResourceA)),
            ("all-zero-ids", AadDescriptor.Create(
                CryptoSpec.AadPurpose.LocalCache,
                CryptoSpec.AadResourceType.None,
                Guid.Empty,
                keyGeneration: 0)),
        ];

        var array = new JsonArray();
        foreach (var (name, descriptor) in cases)
        {
            array.Add(new JsonObject
            {
                ["name"] = name,
                ["purpose"] = (int)descriptor.Purpose,
                ["resourceType"] = (int)descriptor.ResourceType,
                ["resourceId"] = descriptor.ResourceId.ToString(),
                ["keyId"] = descriptor.KeyId.ToString(),
                ["keyGeneration"] = descriptor.KeyGeneration,
                ["itemVersion"] = descriptor.ItemVersion,
                ["aadVersion"] = descriptor.AadVersion,
                ["schemaVersion"] = descriptor.SchemaVersion,
                ["canonicalEncoding"] = Hex(descriptor.ToCanonicalEncoding()),
                ["aad"] = Hex(descriptor.ComputeAad()),
            });
        }

        return array;
    }

    private static JsonArray BuildEnvelopeVectors()
    {
        // Framing only, with fixed inputs, so the byte layout of the header is pinned
        // independently of any AEAD behaviour.
        var ciphertext = Enumerable.Range(0, 20).Select(i => (byte)i).ToArray();
        var xchachaNonce = Enumerable.Range(0, DshEnvelope.XChaChaNonceSize).Select(i => (byte)(0xA0 + i)).ToArray();
        var gcmNonce = Enumerable.Range(0, DshEnvelope.AesGcmNonceSize).Select(i => (byte)(0xB0 + i)).ToArray();
        var ephemeral = Enumerable.Range(0, CryptoSpec.PublicKeySize).Select(i => (byte)(0xC0 + i)).ToArray();

        return
        [
            new JsonObject
            {
                ["name"] = "xchacha20poly1305",
                ["algId"] = (int)CryptoSpec.AlgorithmId.XChaCha20Poly1305,
                ["nonce"] = Hex(xchachaNonce),
                ["ciphertext"] = Hex(ciphertext),
                ["prefixSize"] = DshEnvelope.PrefixSizeFor(CryptoSpec.AlgorithmId.XChaCha20Poly1305),
                ["envelope"] = Hex(DshEnvelope.Write(
                    CryptoSpec.AlgorithmId.XChaCha20Poly1305, xchachaNonce, ciphertext)),
            },
            new JsonObject
            {
                ["name"] = "aes256gcm",
                ["algId"] = (int)CryptoSpec.AlgorithmId.Aes256Gcm,
                ["nonce"] = Hex(gcmNonce),
                ["ciphertext"] = Hex(ciphertext),
                ["prefixSize"] = DshEnvelope.PrefixSizeFor(CryptoSpec.AlgorithmId.Aes256Gcm),
                ["envelope"] = Hex(DshEnvelope.Write(
                    CryptoSpec.AlgorithmId.Aes256Gcm, gcmNonce, ciphertext)),
            },
            new JsonObject
            {
                ["name"] = "sealto-x25519",
                ["algId"] = (int)CryptoSpec.AlgorithmId.SealToX25519,
                ["nonce"] = Hex(xchachaNonce),
                ["ephemeralPublicKey"] = Hex(ephemeral),
                ["ciphertext"] = Hex(ciphertext),
                ["prefixSize"] = DshEnvelope.PrefixSizeFor(CryptoSpec.AlgorithmId.SealToX25519),
                ["envelope"] = Hex(DshEnvelope.Write(
                    CryptoSpec.AlgorithmId.SealToX25519, xchachaNonce, ciphertext, ephemeral)),
            },
        ];
    }

    private static JsonArray BuildAeadVectors()
    {
        // Fixed key and nonce, so the AEAD itself is pinned. DshCrypto.Seal draws a random
        // nonce by design, so the primitive is exercised directly here.
        var key = Enumerable.Range(0, CryptoSpec.SymmetricKeySize).Select(i => (byte)i).ToArray();
        var nonce = Enumerable.Range(0, DshEnvelope.XChaChaNonceSize).Select(i => (byte)(0x10 + i)).ToArray();
        var plaintext = "correct horse battery staple"u8.ToArray();

        var descriptor = AadDescriptor.Create(
            CryptoSpec.AadPurpose.ItemPayload,
            CryptoSpec.AadResourceType.Credential,
            ResourceA,
            KeyIdA,
            keyGeneration: 7,
            itemVersion: 3);

        var aad = descriptor.ComputeAad();

        using var aeadKey = Key.Import(
            AeadAlgorithm.XChaCha20Poly1305, key, KeyBlobFormat.RawSymmetricKey);
        var ciphertext = AeadAlgorithm.XChaCha20Poly1305.Encrypt(aeadKey, nonce, aad, plaintext);

        return
        [
            new JsonObject
            {
                ["name"] = "xchacha20poly1305-with-canonical-aad",
                ["key"] = Hex(key),
                ["nonce"] = Hex(nonce),
                ["aad"] = Hex(aad),
                ["plaintext"] = Hex(plaintext),
                ["ciphertext"] = Hex(ciphertext),
                ["envelope"] = Hex(DshEnvelope.Write(
                    CryptoSpec.AlgorithmId.XChaCha20Poly1305, nonce, ciphertext)),
            },
        ];
    }

    /// <remarks>
    /// Only the labels that really are HKDF-Expand over a master key belong here. The local cache key used
    /// to be one of them and is not any more — it extracts and expands over the bundle's encoding
    /// instead — so it has its own section rather than an entry here that would describe a derivation this
    /// implementation no longer performs. See <see cref="BuildLocalCacheKeyVectors"/>.
    /// </remarks>
    private static JsonArray BuildHkdfVectors()
    {
        var prk = Enumerable.Range(0, 64).Select(i => (byte)i).ToArray();

        (string Name, byte[] Info)[] cases =
        [
            ("passphrase-kek", CryptoSpec.DerivationLabels.PassphraseKek.ToArray()),
        ];

        var array = new JsonArray();
        foreach (var (name, info) in cases)
        {
            array.Add(new JsonObject
            {
                ["name"] = name,
                ["algorithm"] = "HKDF-SHA512-Expand",
                ["prk"] = Hex(prk),
                ["info"] = Encoding.UTF8.GetString(info),
                ["outputLength"] = CryptoSpec.SymmetricKeySize,
                ["output"] = Hex(HKDF.Expand(
                    HashAlgorithmName.SHA512, prk, CryptoSpec.SymmetricKeySize, info)),
            });
        }

        return array;
    }

    /// <summary>
    /// The local cache key, over a bundle whose every byte is pinned. docs/crypto.md §3.2.
    /// </summary>
    /// <remarks>
    /// Built from a fixed encoding rather than from <c>UserSecretBundle.Create</c>, which draws fresh
    /// randomness and so could never produce a reproducible vector. This is the one that matters for a
    /// second implementation: it pins the extract-and-expand construction, the info label, and the fact
    /// that the input is the bundle's canonical encoding rather than any key inside it.
    /// </remarks>
    private static JsonArray BuildLocalCacheKeyVectors()
    {
        var encoded = FixedBundleEncoding();

        using var bundle = UserSecretBundle.TryDecode(encoded)
            ?? throw new InvalidOperationException("The fixed bundle encoding is not well-formed.");

        var cacheKey = new byte[CryptoSpec.SymmetricKeySize];
        bundle.DeriveLocalCacheKey(cacheKey);

        return
        [
            new JsonObject
            {
                ["name"] = "local-cache-key-from-bundle",
                ["algorithm"] = "HKDF-SHA512 extract-and-expand, no salt",
                ["bundle"] = Hex(encoded),
                ["info"] = Encoding.UTF8.GetString(CryptoSpec.DerivationLabels.LocalCache),
                ["outputLength"] = CryptoSpec.SymmetricKeySize,
                ["output"] = Hex(cacheKey),
            },
        ];
    }

    /// <remarks>
    /// The §3.1 layout with every field a constant: label, version 1, generation 1, a fixed timestamp, and
    /// two key scalars of recognisable byte patterns. Any 32 bytes is a valid X25519 scalar and a valid
    /// Ed25519 seed, so nothing here needs to be a real generated key.
    /// </remarks>
    private static byte[] FixedBundleEncoding()
    {
        var encoded = new byte[UserSecretBundle.EncodedLength];
        var span = encoded.AsSpan();

        UserSecretBundle.Label.CopyTo(span);
        BinaryPrimitives.WriteUInt16BigEndian(span[14..], UserSecretBundle.CurrentVersion);
        BinaryPrimitives.WriteUInt32BigEndian(span[16..], 1u);
        BinaryPrimitives.WriteInt64BigEndian(
            span[20..],
            new DateTimeOffset(2026, 1, 1, 0, 0, 0, TimeSpan.Zero).ToUnixTimeMilliseconds());

        for (var i = 0; i < CryptoSpec.SymmetricKeySize; i++)
        {
            span[28 + i] = (byte)(0x40 + i);
            span[60 + i] = (byte)(0x60 + i);
        }

        return encoded;
    }

    private static JsonArray BuildArgon2Vectors()
    {
        var salt = Enumerable.Range(0, CryptoSpec.SaltSize).Select(i => (byte)(0x20 + i)).ToArray();
        const string Passphrase = "correct horse battery staple";

        var array = new JsonArray();

        // Parameters of every profile are pinned cheaply. Only the smallest profile's output is
        // computed, to keep the suite fast; the KDF itself is libsodium's, not ours.
        (string Name, Argon2Profile Profile)[] profiles =
        [
            ("passphrase-default", Argon2Profile.PassphraseDefault),
            ("passphrase-reduced", Argon2Profile.PassphraseReduced),
            ("passphrase-high", Argon2Profile.PassphraseHigh),
            ("random-secret", Argon2Profile.RandomSecret),
        ];

        foreach (var (name, profile) in profiles)
        {
            array.Add(new JsonObject
            {
                ["name"] = name,
                ["memoryMebibytes"] = profile.MemoryMebibytes,
                ["memoryKibibytes"] = profile.MemoryKibibytes,
                ["passes"] = profile.Passes,
                ["parallelism"] = Argon2Profile.Parallelism,
            });
        }

        array.Add(new JsonObject
        {
            ["name"] = "random-secret-output",
            ["memoryMebibytes"] = Argon2Profile.RandomSecret.MemoryMebibytes,
            ["memoryKibibytes"] = Argon2Profile.RandomSecret.MemoryKibibytes,
            ["passes"] = Argon2Profile.RandomSecret.Passes,
            ["parallelism"] = Argon2Profile.Parallelism,
            ["passphrase"] = Passphrase,
            ["salt"] = Hex(salt),
            ["outputLength"] = CryptoSpec.SymmetricKeySize,
            ["output"] = Hex(Argon2Profile.RandomSecret
                .CreateAlgorithm()
                .DeriveBytes(Passphrase, salt, CryptoSpec.SymmetricKeySize)),
        });

        return array;
    }

    private static JsonArray BuildFingerprintVectors()
    {
        var x25519 = Enumerable.Range(0, CryptoSpec.PublicKeySize).Select(i => (byte)(0x40 + i)).ToArray();
        var ed25519 = Enumerable.Range(0, CryptoSpec.PublicKeySize).Select(i => (byte)(0x60 + i)).ToArray();

        return
        [
            new JsonObject
            {
                ["name"] = "identity-fingerprint",
                ["x25519PublicKey"] = Hex(x25519),
                ["ed25519PublicKey"] = Hex(ed25519),
                ["fingerprint"] = Hex(DshCrypto.ComputeFingerprint(x25519, ed25519)),
            },
        ];
    }

    /// <summary>
    /// Pins the key statement encoding and the nonce derived from it. See docs/crypto.md §7.1.
    /// </summary>
    /// <remarks>
    /// The highest-value vectors after the AAD ones. The nonce is what an identity provider signs
    /// over, so a client and a server that disagree by one byte here produce enrollments that can
    /// never be verified — and unlike a decryption failure, that only shows up against a real
    /// provider.
    /// <para>
    /// The absent-email case is present specifically to pin the presence byte, which is what stops
    /// an absent email and an empty one sharing a binding.
    /// </para>
    /// </remarks>
    private static JsonArray BuildKeyStatementVectors()
    {
        var array = new JsonArray();

        foreach (var (name, statement) in KeyStatementCases())
        {
            var encoding = KeyStatementCodec.Encode(statement);

            array.Add(new JsonObject
            {
                ["name"] = name,
                ["version"] = statement.Version,
                ["issuer"] = statement.Issuer,
                ["subject"] = statement.Subject,
                ["email"] = statement.Email,
                ["keyGeneration"] = statement.KeyGeneration,
                ["createdAtUnixMilliseconds"] = statement.CreatedAt.ToUnixTimeMilliseconds(),
                ["deviceName"] = statement.DeviceName,
                ["x25519PublicKey"] = Hex(statement.EncryptionPublicKey),
                ["ed25519PublicKey"] = Hex(statement.SigningPublicKey),
                ["canonicalEncoding"] = Hex(encoding),
                ["binding"] = Hex(KeyStatementCodec.ComputeBinding(encoding)),
                ["nonce"] = KeyStatementCodec.ComputeNonce(statement),
            });
        }

        return array;
    }

    private static (string Name, KeyStatementFields Statement)[] KeyStatementCases()
    {
        var x25519 = Enumerable.Range(0, CryptoSpec.PublicKeySize).Select(i => (byte)(0x40 + i)).ToArray();
        var ed25519 = Enumerable.Range(0, CryptoSpec.PublicKeySize).Select(i => (byte)(0x60 + i)).ToArray();

        // A fixed instant, so the vectors do not depend on when they were generated.
        var createdAt = DateTimeOffset.FromUnixTimeMilliseconds(1_750_000_000_123);

        return
        [
            ("with-email", new KeyStatementFields(
                1, "https://idp.example/realms/dodossh", "alice-subject", "alice@example.com",
                x25519, ed25519, 1, createdAt, "alice-laptop")),

            // Same statement with the email absent, not empty.
            ("without-email", new KeyStatementFields(
                1, "https://idp.example/realms/dodossh", "alice-subject", null,
                x25519, ed25519, 1, createdAt, "alice-laptop")),

            // ...and empty rather than absent. These three must all differ.
            ("empty-email", new KeyStatementFields(
                1, "https://idp.example/realms/dodossh", "alice-subject", string.Empty,
                x25519, ed25519, 1, createdAt, "alice-laptop")),

            // Multi-byte UTF-8, so a length prefix counting characters rather than bytes fails here.
            ("unicode-device-name", new KeyStatementFields(
                1, "https://idp.example/realms/dodossh", "alice-subject", "alice@example.com",
                x25519, ed25519, 1, createdAt, "alice's ThinkPad — büro")),

            // A sub-millisecond offset-bearing timestamp must encode identically to the UTC one,
            // because the encoding normalises and truncates.
            ("offset-and-sub-millisecond-timestamp", new KeyStatementFields(
                1, "https://idp.example/realms/dodossh", "alice-subject", "alice@example.com",
                x25519, ed25519, 1,
                createdAt.ToOffset(TimeSpan.FromHours(2)).AddTicks(7777),
                "alice-laptop")),

            ("later-generation", new KeyStatementFields(
                1, "https://idp.example/realms/dodossh", "alice-subject", "alice@example.com",
                x25519, ed25519, 4, createdAt, "alice-laptop")),
        ];
    }

    /// <summary>Pins the key log chain hash, including the genesis link. See docs/crypto.md §7.2.</summary>
    private static JsonArray BuildKeyLogVectors()
    {
        var x25519 = Enumerable.Range(0, CryptoSpec.PublicKeySize).Select(i => (byte)(0x40 + i)).ToArray();
        var ed25519 = Enumerable.Range(0, CryptoSpec.PublicKeySize).Select(i => (byte)(0x60 + i)).ToArray();
        var signature = Enumerable.Range(0, CryptoSpec.SignatureSize).Select(i => (byte)(0x80 + i)).ToArray();
        var createdAt = DateTimeOffset.FromUnixTimeMilliseconds(1_750_000_000_123);

        var genesisPrevious = KeyLogChain.CreateGenesisPreviousHash();
        var genesis = KeyLogChain.ComputeEntryHash(
            genesisPrevious, ResourceA, 1, x25519, ed25519, signature, createdAt);

        // The second entry links to the first, so a broken link shows up as a changed hash here
        // rather than only in a running deployment.
        var second = KeyLogChain.ComputeEntryHash(
            genesis, ResourceB, 1, x25519, ed25519, signature, createdAt);

        return
        [
            new JsonObject
            {
                ["name"] = "genesis",
                ["previousHash"] = Hex(genesisPrevious),
                ["userId"] = ResourceA.ToString(),
                ["generation"] = 1,
                ["x25519PublicKey"] = Hex(x25519),
                ["ed25519PublicKey"] = Hex(ed25519),
                ["statementSignature"] = Hex(signature),
                ["createdAtUnixMilliseconds"] = createdAt.ToUnixTimeMilliseconds(),
                ["hash"] = Hex(genesis),
            },
            new JsonObject
            {
                ["name"] = "second-entry",
                ["previousHash"] = Hex(genesis),
                ["userId"] = ResourceB.ToString(),
                ["generation"] = 1,
                ["x25519PublicKey"] = Hex(x25519),
                ["ed25519PublicKey"] = Hex(ed25519),
                ["statementSignature"] = Hex(signature),
                ["createdAtUnixMilliseconds"] = createdAt.ToUnixTimeMilliseconds(),
                ["hash"] = Hex(second),
            },
        ];
    }

    private static string Hex(ReadOnlySpan<byte> value) =>
        Convert.ToHexString(value).ToLower(CultureInfo.InvariantCulture);
}
