using DodoSSH.Crypto;

namespace DodoSSH.Contracts.Tests;

/// <summary>
/// Guards the two key statement types against drifting apart.
/// </summary>
/// <remarks>
/// <para>
/// <see cref="KeyStatement"/> is the wire DTO; <see cref="KeyStatementFields"/> is what the
/// canonical encoding in docs/crypto.md §7.1 is defined over. They are separate on purpose — one
/// may gain JSON fields freely, the other cannot change without invalidating every stored binding,
/// and <c>DodoSSH.Crypto</c> must not depend on the contract assembly.
/// </para>
/// <para>
/// The hazard that separation creates is a field added to the DTO and silently left out of the
/// hash. A client would then sign and bind a statement that omits it, and the field would be
/// unauthenticated data the server could change at will. This test makes that a build failure:
/// adding a field to the DTO forces a deliberate decision about whether it is covered, and if it
/// is, a spec version bump.
/// </para>
/// </remarks>
public sealed class KeyStatementDriftTests
{
    [Fact]
    public void BothTypes_DeclareTheSameFields()
    {
        var contract = PropertyNames<KeyStatement>();
        var canonical = PropertyNames<KeyStatementFields>();

        canonical.ShouldBe(
            contract,
            "KeyStatement and KeyStatementFields disagree. A field on the wire that the canonical "
            + "encoding does not cover is unauthenticated: the identity provider never signs over "
            + "it, so the server can change it undetected. Cover it and bump the statement version, "
            + "or document why it is deliberately outside the binding.");
    }

    [Fact]
    public void BothTypes_AgreeOnFieldTypes()
    {
        // Names alone would not catch a string becoming a Uri, or an int becoming a long — either
        // of which changes what gets encoded without changing what gets listed.
        var contract = PropertyTypes<KeyStatement>();
        var canonical = PropertyTypes<KeyStatementFields>();

        canonical.ShouldBe(contract);
    }

    private static IReadOnlyList<string> PropertyNames<T>() =>
        [.. typeof(T)
            .GetProperties()
            .Select(p => p.Name)
            .Where(name => !string.Equals(name, "EqualityContract", StringComparison.Ordinal))
            .Order(StringComparer.Ordinal)];

    private static IReadOnlyList<string> PropertyTypes<T>() =>
        [.. typeof(T)
            .GetProperties()
            .Where(p => !string.Equals(p.Name, "EqualityContract", StringComparison.Ordinal))
            .Select(p => $"{p.Name}:{p.PropertyType.Name}")
            .Order(StringComparer.Ordinal)];
}
