Editor/Projection/Tests/RoundTripRunner.cs
using System;
using System.Collections.Generic;
using System.IO;
using System.Text.Json;
using Grains.RazorDesigner.Serialization.IR;
using Sandbox;

namespace Grains.RazorDesigner.Projection.Tests;

public static class RoundTripRunner
{
    private const string LogPrefix = "[Grains.RazorDesigner]";

    public readonly struct RoundTripResult
    {
        public string Name    { get; }
        public bool   Pass    { get; }
        public string Message { get; }

        public RoundTripResult( string name, bool pass, string message )
        {
            Name    = name;
            Pass    = pass;
            Message = message;
        }
    }

    private static string GoldensDir()
    {
        var root = Project.Current?.RootDirectory?.FullName
            ?? throw new InvalidOperationException( "RoundTripRunner: Project.Current is null — cannot locate Goldens directory." );
        return Path.Combine( root, "Editor", "Projection", "Tests", "Goldens" );
    }

    public static List<RoundTripResult> RunAll()
    {
        var results = new List<RoundTripResult>();
        var dir = GoldensDir();

        if ( !Directory.Exists( dir ) )
        {
            Log.Warning( $"{LogPrefix} RoundTripRunner.RunAll: Goldens directory not found at '{dir}'." );
            results.Add( new RoundTripResult( "<dir-missing>", false, $"Goldens directory missing: {dir}" ) );
            return results;
        }

        foreach ( var path in Directory.GetFiles( dir, "*.roundtrip.json" ) )
        {
            var result = RunFile( path );
            results.Add( result );
            if ( result.Pass )
                Log.Info( $"{LogPrefix} PASS [round-trip:{result.Name}]" );
            else
                Log.Warning( $"{LogPrefix} FAIL [round-trip:{result.Name}]: {result.Message}" );
        }

        return results;
    }

    private static RoundTripResult RunFile( string path )
    {
        var name = Path.GetFileNameWithoutExtension( path );
        try
        {
            // 1. Read on-disk text. .gitattributes pins LF; if a Windows checkout has CRLF, normalize.
            var original = File.ReadAllText( path );
            if ( original.Contains( '\r' ) )
                original = original.Replace( "\r\n", "\n" ).Replace( "\r", "\n" );

            // 2. Deserialize through the canonical Options.
            var envelope = JsonSerializer.Deserialize<IRDocumentEnvelope>( original, DesignerIRJson.Options );
            if ( envelope is null )
                return new RoundTripResult( name, false, "Deserialize returned null." );

            // 3. Re-serialize and normalize line endings the same way IRWriter does.
            var reserialized = JsonSerializer.Serialize( envelope, DesignerIRJson.Options );
            if ( reserialized.Contains( '\r' ) )
                reserialized = reserialized.Replace( "\r\n", "\n" ).Replace( "\r", "\n" );

            // 4. Byte-compare.
            if ( string.Equals( original, reserialized, StringComparison.Ordinal ) )
                return new RoundTripResult( name, true, "byte-identical" );

            // On mismatch, return the first divergent position for easier diagnosis.
            int max = Math.Min( original.Length, reserialized.Length );
            int divergeAt = -1;
            for ( int i = 0; i < max; i++ )
            {
                if ( original[i] != reserialized[i] )
                {
                    divergeAt = i;
                    break;
                }
            }
            if ( divergeAt < 0 )
                divergeAt = max;

            int contextStart = Math.Max( 0, divergeAt - 40 );
            var origCtx      = original.Substring( contextStart, Math.Min( 80, original.Length - contextStart ) );
            var rsCtx        = reserialized.Substring( contextStart, Math.Min( 80, reserialized.Length - contextStart ) );

            return new RoundTripResult( name, false,
                $"diverges at offset {divergeAt}. original: \"{origCtx}\" vs reserialized: \"{rsCtx}\"" );
        }
        catch ( Exception ex )
        {
            return new RoundTripResult( name, false, $"Exception: {ex.GetType().Name}: {ex.Message}" );
        }
    }
}