namespace DodoSSH.Crypto.Tests;

/// <summary>
/// Asserts the committed golden vectors still hold.
/// </summary>
/// <remarks>
/// <para>
/// This is the single most important test in the product. The server holds ciphertext and no
/// keys, so it can never re-encrypt anything: a change to the envelope layout or to AAD
/// derivation that reaches a release makes every existing vault undecryptable, with no
/// server-side remedy and no rollback.
/// </para>
/// <para>
/// <b>A failure here is never fixed by regenerating the fixture.</b> It means either a genuine
/// regression, or an intentional format change — which requires a new
/// <c>aadVersion</c>/<c>algId</c> and a client-side lazy re-encrypt-on-write path to exist
/// first. See docs/crypto.md §8.
/// </para>
/// <para>
/// To regenerate deliberately, set <c>DODOSSH_REGENERATE_VECTORS=1</c>. The test rewrites the
/// fixture in the source tree and then fails, so the diff has to be reviewed rather than
/// silently absorbed.
/// </para>
/// </remarks>
public sealed class GoldenVectorTests
{
    private const string RegenerateVariable = "DODOSSH_REGENERATE_VECTORS";
    private const string FixtureRelativePath = "fixtures/crypto/vectors.json";

    [Fact]
    public void CommittedVectors_MatchCurrentImplementation()
    {
        var actual = GoldenVectors.Generate();

        if (string.Equals(Environment.GetEnvironmentVariable(RegenerateVariable), "1", StringComparison.Ordinal))
        {
            var sourcePath = ResolveSourceTreeFixturePath();
            Directory.CreateDirectory(Path.GetDirectoryName(sourcePath)!);
            File.WriteAllText(sourcePath, actual);

            Assert.Fail(
                $"Regenerated {sourcePath}. Review the diff and unset {RegenerateVariable}. "
                + "If the envelope or AAD changed, a version bump and a client migration path are required first.");
        }

        var expected = File.ReadAllText(OutputFixturePath());

        Normalise(actual).ShouldBe(
            Normalise(expected),
            "The DSH1 format or AAD derivation changed. This would make every existing vault "
            + "undecryptable. Do not regenerate the fixture to silence this.");
    }

    [Fact]
    public void Fixture_IsCommittedAndNonTrivial()
    {
        var content = File.ReadAllText(OutputFixturePath());

        content.Length.ShouldBeGreaterThan(1000);
        content.ShouldContain("canonicalEncoding");
        content.ShouldContain("\"specVersion\": 1");
    }

    private static string Normalise(string json) => json.ReplaceLineEndings("\n").TrimEnd();

    /// <summary>
    /// The fixture as copied beside the test assembly. Robust under deterministic source paths.
    /// </summary>
    private static string OutputFixturePath()
    {
        var path = Path.Combine(AppContext.BaseDirectory, FixtureRelativePath);

        File.Exists(path).ShouldBeTrue(
            $"Golden vector fixture missing at {path}. It should be copied to the output "
            + $"directory by the project file. Set {RegenerateVariable}=1 to create it.");

        return path;
    }

    /// <summary>
    /// Locates the fixture in the source tree by walking up to the solution file.
    /// </summary>
    /// <remarks>
    /// Used only when regenerating, which is a developer-local action.
    /// </remarks>
    private static string ResolveSourceTreeFixturePath()
    {
        var directory = new DirectoryInfo(AppContext.BaseDirectory);

        while (directory is not null && !File.Exists(Path.Combine(directory.FullName, "DodoSSH.slnx")))
        {
            directory = directory.Parent;
        }

        if (directory is null)
        {
            throw new InvalidOperationException(
                "Could not locate the repository root (no DodoSSH.slnx found above "
                + $"{AppContext.BaseDirectory}). Regenerate from within the repository.");
        }

        return Path.Combine(directory.FullName, "tests", FixtureRelativePath.Replace('/', Path.DirectorySeparatorChar));
    }
}
