using DodoSSH.Crypto;

namespace DodoSSH.Crypto.Tests;

/// <summary>
/// Canonical AAD encoding, per docs/crypto.md §4.
/// </summary>
public sealed class AadDescriptorTests
{
    private static readonly Guid ResourceId = Guid.Parse("0192f0c8-1a2b-7c3d-8e4f-5a6b7c8d9e0f");
    private static readonly Guid KeyId = Guid.Parse("0192f0c8-9999-7aaa-8bbb-cccccccccccc");

    private static AadDescriptor Sample() => AadDescriptor.Create(
        CryptoSpec.AadPurpose.ItemPayload,
        CryptoSpec.AadResourceType.Credential,
        ResourceId,
        KeyId,
        keyGeneration: 7,
        itemVersion: 3);

    [Fact]
    public void Encoding_IsExactlySixtyFourBytes()
    {
        Sample().ToCanonicalEncoding().Length.ShouldBe(CryptoSpec.AadEncodedLength);
    }

    [Fact]
    public void Encoding_StartsWithMagicAndVersions()
    {
        var encoded = Sample().ToCanonicalEncoding();

        encoded[..5].ShouldBe("dsh1\n"u8.ToArray());
        encoded[5].ShouldBe(CryptoSpec.CurrentAadVersion);
        encoded[6].ShouldBe((byte)CryptoSpec.AadPurpose.ItemPayload);
        encoded[7].ShouldBe((byte)CryptoSpec.AadResourceType.Credential);
    }

    [Fact]
    public void Encoding_WritesUuidsInRfc4122ByteOrder()
    {
        // Guid.ToByteArray() emits the first three groups little-endian. Using it here would
        // make our ciphertext undecryptable by any other implementation of this spec, and the
        // bug would only surface at a cross-implementation boundary.
        var encoded = Sample().ToCanonicalEncoding();

        encoded[8..24].ShouldBe(ResourceId.ToByteArray(bigEndian: true));
        encoded[24..40].ShouldBe(KeyId.ToByteArray(bigEndian: true));

        // And prove the mixed-endian form differs, so this test cannot pass vacuously.
        ResourceId.ToByteArray(bigEndian: true).ShouldNotBe(ResourceId.ToByteArray());
    }

    [Fact]
    public void Encoding_WritesIntegersBigEndian()
    {
        var encoded = Sample().ToCanonicalEncoding();

        encoded[40..44].ShouldBe(new byte[] { 0, 0, 0, 7 });   // keyGeneration
        encoded[44..48].ShouldBe(new byte[] { 0, 0, 0, 3 });   // itemVersion
        encoded[48..50].ShouldBe(new byte[] { 0, 1 });          // schemaVersion
    }

    [Fact]
    public void Encoding_LeavesReservedBytesZero()
    {
        Sample().ToCanonicalEncoding()[50..64].ShouldAllBe(b => b == 0);
    }

    [Fact]
    public void Encoding_IsDeterministic()
    {
        Sample().ToCanonicalEncoding().ShouldBe(Sample().ToCanonicalEncoding());
    }

    [Fact]
    public void Aad_IsSha256OfTheCanonicalEncoding()
    {
        var descriptor = Sample();

        descriptor.ComputeAad().ShouldBe(
            System.Security.Cryptography.SHA256.HashData(descriptor.ToCanonicalEncoding()));
    }

    [Fact]
    public void UnspecifiedPurpose_IsRejected()
    {
        var descriptor = AadDescriptor.Create(
            CryptoSpec.AadPurpose.Unspecified,
            CryptoSpec.AadResourceType.Host,
            ResourceId);

        Should.Throw<InvalidOperationException>(() => descriptor.ToCanonicalEncoding());
    }

    [Fact]
    public void ShortDestination_IsRejected()
    {
        var descriptor = Sample();

        Should.Throw<ArgumentException>(() =>
        {
            var tooSmall = new byte[CryptoSpec.AadEncodedLength - 1];
            descriptor.WriteCanonicalEncoding(tooSmall);
        });
    }

    /// <summary>
    /// Each field must change the AAD. If one did not, the corresponding substitution attack
    /// in docs/crypto.md §4.4 would succeed.
    /// </summary>
    [Fact]
    public void EveryField_ChangesTheAad()
    {
        var baseline = Sample();
        var baselineAad = baseline.ComputeAad();

        var variants = new (string Field, AadDescriptor Descriptor)[]
        {
            ("purpose", baseline with { Purpose = CryptoSpec.AadPurpose.ItemMetadata }),
            ("resourceType", baseline with { ResourceType = CryptoSpec.AadResourceType.Host }),
            ("resourceId", baseline with { ResourceId = Guid.Parse("0192f0c8-dead-7bee-8fee-000000000001") }),
            ("keyId", baseline with { KeyId = Guid.Empty }),
            ("keyGeneration", baseline with { KeyGeneration = 8 }),
            ("itemVersion", baseline with { ItemVersion = 4 }),
            ("aadVersion", baseline with { AadVersion = 2 }),
            ("schemaVersion", baseline with { SchemaVersion = 2 }),
        };

        foreach (var (field, descriptor) in variants)
        {
            descriptor.ComputeAad().ShouldNotBe(baselineAad, $"changing {field} must change the AAD");
        }
    }
}
