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

namespace Grains.RazorDesigner.Projection.Tests;

public static class CSharpGoldenRunner
{
    private const string LogPrefix = "[Grains.RazorDesigner]";
    private const int SupportedFormatVersion = 1;

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

        public CSharpGoldenResult( 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( "CSharpGoldenRunner: Project.Current is null — cannot locate Goldens directory." );
        return Path.Combine( root, "Editor", "Projection", "Tests", "Goldens" );
    }

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

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

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

        var (exPass, exMsg) = CSharpOpExhaustivenessTest.Run();
        results.Add( new CSharpGoldenResult( "CSharpOpExhaustivenessTest", exPass, exMsg ) );
        if ( exPass )
            Log.Info( $"{LogPrefix} PASS [CSharpOpExhaustivenessTest]: {exMsg}" );
        else
            Log.Warning( $"{LogPrefix} FAIL [CSharpOpExhaustivenessTest]: {exMsg}" );

        return results;
    }

    public static void WriteAllGoldens( bool overwrite = false )
    {
        var dir = GoldensDir();
        if ( !Directory.Exists( dir ) )
        {
            Log.Warning( $"{LogPrefix} CSharpGoldenRunner.WriteAllGoldens: directory missing at '{dir}'." );
            return;
        }

        foreach ( var path in Directory.GetFiles( dir, "*.csgolden.json" ) )
        {
            try
            {
                var json    = File.ReadAllText( path );
                var fixture = JsonSerializer.Deserialize<CSharpFixture>( json, DesignerIRJson.Options );
                if ( fixture is null )
                {
                    Log.Warning( $"{LogPrefix} CSharpGoldenRunner.WriteAllGoldens: failed to parse '{path}', skipping." );
                    continue;
                }

                if ( fixture.Expected != null && !overwrite )
                {
                    Log.Info( $"{LogPrefix} CSharpGoldenRunner.WriteAllGoldens: skipping '{fixture.Name}' (expected present, overwrite=false)." );
                    continue;
                }

                var actualOps = RunProjection( fixture );
                fixture.Expected = new CSharpExpectedDto { Ops = ToDtos( actualOps ) };

                // Re-serialize with the canonical options and normalise to LF (mirrors IRWriter).
                var updated = JsonSerializer.Serialize( fixture, DesignerIRJson.Options );
                if ( updated.Contains( '\r' ) )
                    updated = updated.Replace( "\r\n", "\n" ).Replace( "\r", "\n" );

                File.WriteAllText( path, updated );
                Log.Info( $"{LogPrefix} CSharpGoldenRunner.WriteAllGoldens: wrote '{fixture.Name}'." );
            }
            catch ( Exception ex )
            {
                Log.Error( $"{LogPrefix} CSharpGoldenRunner.WriteAllGoldens: '{path}' failed — {ex.GetType().Name}: {ex.Message}" );
            }
        }
    }

    // --- Private helpers ---

    private static CSharpGoldenResult RunFile( string path )
    {
        var name = "<unknown>";
        try
        {
            var json    = File.ReadAllText( path );
            var fixture = JsonSerializer.Deserialize<CSharpFixture>( json, DesignerIRJson.Options );
            if ( fixture is null )
                return new CSharpGoldenResult( name, false, $"Deserialization returned null for '{path}'" );

            name = fixture.Name ?? Path.GetFileNameWithoutExtension( path );

            if ( fixture.GoldenFormatVersion != SupportedFormatVersion )
                return new CSharpGoldenResult( name, false,
                    $"goldenFormatVersion={fixture.GoldenFormatVersion}, expected {SupportedFormatVersion}" );

            if ( fixture.Expected is null )
                return new CSharpGoldenResult( name, false, "expected block is null — run 'Update CSharp goldens (autoupdate)' to author it first" );

            var actualOps  = RunProjection( fixture );
            var expectedOps = ToOps( fixture.Expected.Ops ?? new List<CSharpOpDto>() );

            if ( expectedOps.Count != actualOps.Count )
                return new CSharpGoldenResult( name, false,
                    $"ops count: expected {expectedOps.Count}, got {actualOps.Count}" );

            for ( int i = 0; i < expectedOps.Count; i++ )
            {
                if ( !Equals( expectedOps[i], actualOps[i] ) )
                    return new CSharpGoldenResult( name, false,
                        $"ops[{i}]: expected {expectedOps[i]}, got {actualOps[i]}" );
            }

            return new CSharpGoldenResult( name, true, "PASS" );
        }
        catch ( Exception ex )
        {
            return new CSharpGoldenResult( name, false, $"Exception: {ex.GetType().Name}: {ex.Message}" );
        }
    }

    private static IReadOnlyList<CSharpOp> RunProjection( CSharpFixture fixture )
    {
        var view = new WiringEnvelopeView(
            fixture.Wiring ?? Wiring.WiringEnvelope.Empty,
            fixture.NamespaceFallback ?? "Grains",
            fixture.ClassNameFallback ?? "Test" );

        var result = CSharpProjector.Project( view, fixture.DocumentHasAnyBindings );
        return result.Ops;
    }

    private static List<CSharpOpDto> ToDtos( IReadOnlyList<CSharpOp> ops )
    {
        var list = new List<CSharpOpDto>( ops.Count );
        foreach ( var op in ops )
            list.Add( CSharpOpMapping.FromOp( op ) );
        return list;
    }

    private static List<CSharpOp> ToOps( List<CSharpOpDto> dtos )
    {
        var list = new List<CSharpOp>( dtos.Count );
        foreach ( var dto in dtos )
            list.Add( CSharpOpMapping.ToOp( dto ) );
        return list;
    }
}