// Copyright (c) 2025-2026 GeWuYou
// SPDX-License-Identifier: Apache-2.0

using GFramework.Godot.SourceGenerators.Tests.Core;

namespace GFramework.Godot.SourceGenerators.Tests.Project;

/// <summary>
///     验证基于 <c>project.godot</c> 的项目元数据生成行为。
/// </summary>
[TestFixture]
public class GodotProjectMetadataGeneratorTests
{
    private const string AutoLoadProjectFile = """
                                                [autoload]
                                                GameServices="*res://autoload/game_services.tscn"
                                                AudioBus="*res://autoload/audio_bus.gd"
                                                """;

    private const string InputActionsProjectFile = """
                                                   [input]
                                                   move_up={
                                                   "deadzone": 0.5
                                                   }
                                                   ui_cancel={
                                                   "deadzone": 0.5
                                                   }
                                                   """;

    private const string ExpectedAutoLoads = """
                                             // <auto-generated />
                                             #nullable enable

                                             namespace GFramework.Godot.Generated;

                                             /// <summary>
                                             ///     提供 project.godot 中 AutoLoad 单例的强类型访问入口。
                                             /// </summary>
                                             public static partial class AutoLoads
                                             {
                                                 /// <summary>
                                                 ///     获取 AutoLoad <c>GameServices</c>。
                                                 /// </summary>
                                                 public static global::TestApp.GameServices GameServices => GetRequiredNode<global::TestApp.GameServices>("GameServices");

                                                 /// <summary>
                                                 ///     尝试获取 AutoLoad <c>GameServices</c>。
                                                 /// </summary>
                                                 public static bool TryGetGameServices(out global::TestApp.GameServices? value)
                                                 {
                                                     return TryGetNode("GameServices", out value);
                                                 }

                                                 /// <summary>
                                                 ///     获取 AutoLoad <c>AudioBus</c>。
                                                 /// </summary>
                                                 public static global::Godot.Node AudioBus => GetRequiredNode<global::Godot.Node>("AudioBus");

                                                 /// <summary>
                                                 ///     尝试获取 AutoLoad <c>AudioBus</c>。
                                                 /// </summary>
                                                 public static bool TryGetAudioBus(out global::Godot.Node? value)
                                                 {
                                                     return TryGetNode("AudioBus", out value);
                                                 }

                                                 /// <summary>
                                                 ///     获取一个必填的 AutoLoad 节点；缺失时抛出异常。
                                                 /// </summary>
                                                 /// <typeparam name="TNode">节点类型。</typeparam>
                                                 /// <param name="autoLoadName">AutoLoad 名称。</param>
                                                 /// <returns>已解析的 AutoLoad 节点。</returns>
                                                 private static TNode GetRequiredNode<TNode>(string autoLoadName)
                                                     where TNode : global::Godot.Node
                                                 {
                                                     if (TryGetNode(autoLoadName, out TNode? value))
                                                     {
                                                         return value!;
                                                     }

                                                     throw new global::System.InvalidOperationException($"AutoLoad '{autoLoadName}' is not available on the active SceneTree root.");
                                                 }

                                                 /// <summary>
                                                 ///     尝试从当前 SceneTree 根节点解析 AutoLoad。
                                                 /// </summary>
                                                 /// <typeparam name="TNode">节点类型。</typeparam>
                                                 /// <param name="autoLoadName">AutoLoad 名称。</param>
                                                 /// <param name="value">解析到的节点实例。</param>
                                                 /// <returns>若当前进程存在 SceneTree 且根节点中能解析到该 AutoLoad，则返回 <c>true</c>。</returns>
                                                 private static bool TryGetNode<TNode>(string autoLoadName, out TNode? value)
                                                     where TNode : global::Godot.Node
                                                 {
                                                     value = default;

                                                     if (global::Godot.Engine.GetMainLoop() is not global::Godot.SceneTree sceneTree)
                                                     {
                                                         return false;
                                                     }

                                                     var root = sceneTree.Root;
                                                     if (root is null)
                                                     {
                                                         return false;
                                                     }

                                                     value = root.GetNodeOrNull<TNode>($"/root/{autoLoadName}");
                                                     return value is not null;
                                                 }
                                             }

                                             """;

    private const string ExpectedInputActions = """
                                                // <auto-generated />
                                                #nullable enable

                                                namespace GFramework.Godot.Generated;

                                                /// <summary>
                                                ///     提供 project.godot 中 Input Action 名称的强类型常量。
                                                /// </summary>
                                                public static partial class InputActions
                                                {
                                                    /// <summary>
                                                    ///     Input Action <c>move_up</c> 的稳定名称。
                                                    /// </summary>
                                                    public const string MoveUp = "move_up";

                                                    /// <summary>
                                                    ///     Input Action <c>ui_cancel</c> 的稳定名称。
                                                    /// </summary>
                                                    public const string UiCancel = "ui_cancel";

                                                }

                                                """;

    /// <summary>
    ///     验证会根据 AutoLoad 与 Input Action 生成稳定的强类型入口。
    /// </summary>
    [Test]
    public void Run_Should_Generate_AutoLoads_And_InputActions()
    {
        var source = CreateSource(
            """
            namespace TestApp
            {
                using GFramework.Godot.SourceGenerators.Abstractions;
                using Godot;

                [AutoLoad("GameServices")]
                public partial class GameServices : Node
                {
                }
            }
            """,
            includeAutoLoadAttribute: true);

        var result = AdditionalTextGeneratorTestDriver.Run<GodotProjectMetadataGenerator>(
            source,
            ("project.godot", $"{AutoLoadProjectFile}\n\n{InputActionsProjectFile}"));

        var generatedSources = AdditionalTextGeneratorTestDriver.ToGeneratedSourceMap(result);

        Assert.That(result.Results.Single().Diagnostics, Is.Empty);
        Assert.That(
            generatedSources["GFramework_Godot_Generated_AutoLoads.g.cs"],
            Is.EqualTo(AdditionalTextGeneratorTestDriver.NormalizeLineEndings(ExpectedAutoLoads)));
        Assert.That(
            generatedSources["GFramework_Godot_Generated_InputActions.g.cs"],
            Is.EqualTo(AdditionalTextGeneratorTestDriver.NormalizeLineEndings(ExpectedInputActions)));
    }

    /// <summary>
    ///     验证 <c>[AutoLoad]</c> 标记在非节点类型上时会产生诊断。
    /// </summary>
    [Test]
    public void Run_Should_Report_Diagnostic_When_AutoLoad_Type_Does_Not_Derive_From_Node()
    {
        const string source = """
                              using System;

                              namespace GFramework.Godot.SourceGenerators.Abstractions
                              {
                                  [AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)]
                                  public sealed class AutoLoadAttribute : Attribute
                                  {
                                      public AutoLoadAttribute(string name)
                                      {
                                          Name = name;
                                      }

                                      public string Name { get; }
                                  }
                              }

                              namespace Godot
                              {
                                  public class Node
                                  {
                                  }
                              }

                              namespace TestApp
                              {
                                  using GFramework.Godot.SourceGenerators.Abstractions;

                                  [AutoLoad("ConfigHub")]
                                  public partial class ConfigHub
                                  {
                                  }
                              }
                              """;

        const string projectFile = """
                                   [autoload]
                                   ConfigHub="*res://autoload/config_hub.tscn"
                                   """;

        var result = AdditionalTextGeneratorTestDriver.Run<GodotProjectMetadataGenerator>(
            source,
            ("project.godot", projectFile));

        var diagnostic = result.Results.Single().Diagnostics.Single();

        Assert.Multiple(() =>
        {
            Assert.That(diagnostic.Id, Is.EqualTo("GF_Godot_Project_001"));
            Assert.That(diagnostic.Severity, Is.EqualTo(DiagnosticSeverity.Error));
            Assert.That(diagnostic.GetMessage(), Does.Contain("ConfigHub"));
        });
    }

    /// <summary>
    ///     验证 Input Action 标识符冲突时会追加稳定后缀并给出警告。
    /// </summary>
    [Test]
    public void Run_Should_Report_Diagnostic_And_Append_Suffix_When_Input_Action_Identifiers_Collide()
    {
        const string source = """
                              namespace Godot
                              {
                                  public class MainLoop
                                  {
                                  }

                                  public class Node
                                  {
                                      public T? GetNodeOrNull<T>(string path) where T : Node => default;
                                  }

                                  public sealed class SceneTree : MainLoop
                                  {
                                      public Node? Root { get; set; }
                                  }

                                  public static class Engine
                                  {
                                      public static MainLoop? GetMainLoop() => default;
                                  }
                              }
                              """;

        const string projectFile = """
                                   [input]
                                   move_up={
                                   }
                                   move-up={
                                   }
                                   """;

        var result = AdditionalTextGeneratorTestDriver.Run<GodotProjectMetadataGenerator>(
            source,
            ("project.godot", projectFile));

        var diagnostics = result.Results.Single().Diagnostics;
        var generatedSources = AdditionalTextGeneratorTestDriver.ToGeneratedSourceMap(result);

        Assert.That(diagnostics.Select(static item => item.Id), Is.EqualTo(new[] { "GF_Godot_Project_004" }));
        Assert.That(
            generatedSources["GFramework_Godot_Generated_InputActions.g.cs"],
            Does.Contain("public const string MoveUp = \"move_up\";"));
        Assert.That(
            generatedSources["GFramework_Godot_Generated_InputActions.g.cs"],
            Does.Contain("public const string MoveUp_2 = \"move-up\";"));
    }

    /// <summary>
    ///     验证多个显式映射指向同一个 AutoLoad 时会报告重复映射，并退化为 <c>Godot.Node</c>。
    /// </summary>
    [Test]
    public void Run_Should_Report_Diagnostic_When_Explicit_AutoLoad_Mappings_Are_Duplicated()
    {
        var result = RunGenerator(
            CreateSource(
                """
                namespace TestApp
                {
                    using GFramework.Godot.SourceGenerators.Abstractions;
                    using Godot;

                    [AutoLoad("AudioBus")]
                    public partial class PrimaryAudioBus : Node
                    {
                    }

                    [AutoLoad("AudioBus")]
                    public partial class SecondaryAudioBus : Node
                    {
                    }
                }
                """,
                includeAutoLoadAttribute: true),
            """
            [autoload]
            AudioBus="*res://autoload/audio_bus.tscn"
            """);

        var diagnostics = result.Results.Single().Diagnostics;
        var generatedSources = AdditionalTextGeneratorTestDriver.ToGeneratedSourceMap(result);

        Assert.Multiple(() =>
        {
            Assert.That(diagnostics.Select(static item => item.Id), Is.EqualTo(new[] { "GF_Godot_Project_002" }));
            Assert.That(diagnostics.Single().GetMessage(), Does.Contain("PrimaryAudioBus"));
            Assert.That(diagnostics.Single().GetMessage(), Does.Contain("SecondaryAudioBus"));
            Assert.That(
                generatedSources["GFramework_Godot_Generated_AutoLoads.g.cs"],
                Does.Contain("public static global::Godot.Node AudioBus => GetRequiredNode<global::Godot.Node>(\"AudioBus\");"));
        });
    }

    /// <summary>
    ///     验证不同命名空间下的同名节点类型会触发隐式映射冲突诊断，并退化为 <c>Godot.Node</c>。
    /// </summary>
    [Test]
    public void Run_Should_Report_Diagnostic_When_Implicit_AutoLoad_Mappings_Are_Ambiguous()
    {
        var result = RunGenerator(
            CreateSource(
                """
                namespace TestApp.Audio
                {
                    using Godot;

                    public partial class AudioBus : Node
                    {
                    }
                }

                namespace TestApp.Debug
                {
                    using Godot;

                    public partial class AudioBus : Node
                    {
                    }
                }
                """),
            """
            [autoload]
            AudioBus="*res://autoload/audio_bus.tscn"
            """);

        var diagnostics = result.Results.Single().Diagnostics;
        var generatedSources = AdditionalTextGeneratorTestDriver.ToGeneratedSourceMap(result);

        Assert.Multiple(() =>
        {
            Assert.That(diagnostics.Select(static item => item.Id), Is.EqualTo(new[] { "GF_Godot_Project_002" }));
            Assert.That(diagnostics.Single().GetMessage(), Does.Contain("Audio.AudioBus"));
            Assert.That(diagnostics.Single().GetMessage(), Does.Contain("Debug.AudioBus"));
            Assert.That(
                generatedSources["GFramework_Godot_Generated_AutoLoads.g.cs"],
                Does.Contain("public static global::Godot.Node AudioBus => GetRequiredNode<global::Godot.Node>(\"AudioBus\");"));
        });
    }

    /// <summary>
    ///     验证 AutoLoad 标识符冲突时会追加稳定后缀并报告诊断。
    /// </summary>
    [Test]
    public void Run_Should_Report_Diagnostic_And_Append_Suffix_When_AutoLoad_Identifiers_Collide()
    {
        var result = RunGenerator(
            CreateSource("namespace TestApp { }"),
            """
            [autoload]
            audio_bus="*res://autoload/audio_bus.tscn"
            audio-bus="*res://autoload/audio_bus_debug.tscn"
            """);

        var diagnostics = result.Results.Single().Diagnostics;
        var generatedSources = AdditionalTextGeneratorTestDriver.ToGeneratedSourceMap(result);

        Assert.Multiple(() =>
        {
            Assert.That(diagnostics.Select(static item => item.Id), Is.EqualTo(new[] { "GF_Godot_Project_003" }));
            Assert.That(
                generatedSources["GFramework_Godot_Generated_AutoLoads.g.cs"],
                Does.Contain("public static global::Godot.Node AudioBus => GetRequiredNode<global::Godot.Node>(\"audio_bus\");"));
            Assert.That(
                generatedSources["GFramework_Godot_Generated_AutoLoads.g.cs"],
                Does.Contain("public static global::Godot.Node AudioBus_2 => GetRequiredNode<global::Godot.Node>(\"audio-bus\");"));
        });
    }

    /// <summary>
    ///     验证重复 AutoLoad 条目会报告诊断，并只保留第一条声明参与生成。
    /// </summary>
    [Test]
    public void Run_Should_Report_Diagnostic_When_Project_File_Contains_Duplicate_AutoLoads()
    {
        var result = RunGenerator(
            CreateSource("namespace TestApp { }"),
            """
            [autoload]
            GameServices="*res://autoload/game_services.tscn"
            GameServices="*res://autoload/game_services_debug.tscn"
            """);

        var diagnostics = result.Results.Single().Diagnostics;
        var generatedSources = AdditionalTextGeneratorTestDriver.ToGeneratedSourceMap(result);

        Assert.Multiple(() =>
        {
            Assert.That(diagnostics.Select(static item => item.Id), Is.EqualTo(new[] { "GF_Godot_Project_005" }));
            Assert.That(diagnostics.Single().GetMessage(), Does.Contain("GameServices"));
            Assert.That(
                generatedSources["GFramework_Godot_Generated_AutoLoads.g.cs"],
                Does.Contain("public static global::Godot.Node GameServices => GetRequiredNode<global::Godot.Node>(\"GameServices\");"));
            Assert.That(
                generatedSources["GFramework_Godot_Generated_AutoLoads.g.cs"],
                Does.Not.Contain("GameServices_2"));
        });
    }

    /// <summary>
    ///     验证重复 Input Action 条目会报告诊断，并只保留第一条声明参与生成。
    /// </summary>
    [Test]
    public void Run_Should_Report_Diagnostic_When_Project_File_Contains_Duplicate_Input_Actions()
    {
        var result = RunGenerator(
            CreateSource("namespace TestApp { }"),
            """
            [input]
            move_up={
            }
            move_up={
            }
            """);

        var diagnostics = result.Results.Single().Diagnostics;
        var generatedSources = AdditionalTextGeneratorTestDriver.ToGeneratedSourceMap(result);

        Assert.Multiple(() =>
        {
            Assert.That(diagnostics.Select(static item => item.Id), Is.EqualTo(new[] { "GF_Godot_Project_006" }));
            Assert.That(diagnostics.Single().GetMessage(), Does.Contain("move_up"));
            Assert.That(
                generatedSources["GFramework_Godot_Generated_InputActions.g.cs"],
                Does.Contain("public const string MoveUp = \"move_up\";"));
            Assert.That(
                generatedSources["GFramework_Godot_Generated_InputActions.g.cs"],
                Does.Not.Contain("MoveUp_2"));
        });
    }

    /// <summary>
    ///     验证缺少 <c>project.godot</c> AdditionalText 时不会生成任何源码或诊断。
    /// </summary>
    [Test]
    public void Run_Should_Not_Generate_Sources_When_Project_File_Is_Missing()
    {
        var result = AdditionalTextGeneratorTestDriver.Run<GodotProjectMetadataGenerator>(
            CreateSource("namespace TestApp { }"));

        var generatorResult = result.Results.Single();

        Assert.Multiple(() =>
        {
            Assert.That(generatorResult.Diagnostics, Is.Empty);
            Assert.That(generatorResult.GeneratedSources, Is.Empty);
        });
    }

    /// <summary>
    ///     验证空的 <c>project.godot</c> 内容不会生成任何源码或诊断。
    /// </summary>
    [Test]
    public void Run_Should_Not_Generate_Sources_When_Project_File_Is_Empty()
    {
        var result = RunGenerator(
            CreateSource("namespace TestApp { }"),
            string.Empty);

        var generatorResult = result.Results.Single();

        Assert.Multiple(() =>
        {
            Assert.That(generatorResult.Diagnostics, Is.Empty);
            Assert.That(generatorResult.GeneratedSources, Is.Empty);
        });
    }

    /// <summary>
    ///     验证只有空节的 <c>project.godot</c> 不会生成任何源码或诊断。
    /// </summary>
    [Test]
    public void Run_Should_Not_Generate_Sources_When_Project_File_Has_Empty_Sections()
    {
        var result = RunGenerator(
            CreateSource("namespace TestApp { }"),
            """
            [autoload]

            [input]
            """);

        var generatorResult = result.Results.Single();

        Assert.Multiple(() =>
        {
            Assert.That(generatorResult.Diagnostics, Is.Empty);
            Assert.That(generatorResult.GeneratedSources, Is.Empty);
        });
    }

    private static GeneratorDriverRunResult RunGenerator(
        string source,
        string projectFile)
    {
        return AdditionalTextGeneratorTestDriver.Run<GodotProjectMetadataGenerator>(
            source,
            ("project.godot", projectFile));
    }

    private static string CreateSource(
        string applicationSource,
        bool includeAutoLoadAttribute = false)
    {
        var autoLoadAttributeSource = includeAutoLoadAttribute
            ? """
              using System;

              namespace GFramework.Godot.SourceGenerators.Abstractions
              {
                  [AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)]
                  public sealed class AutoLoadAttribute : Attribute
                  {
                      public AutoLoadAttribute(string name)
                      {
                          Name = name;
                      }

                      public string Name { get; }
                  }
              }
              """
            : string.Empty;

        return autoLoadAttributeSource + """
                                         namespace Godot
                                         {
                                             public class MainLoop
                                             {
                                             }

                                             public class Node
                                             {
                                                 public T? GetNodeOrNull<T>(string path) where T : Node => default;
                                             }

                                             public sealed class SceneTree : MainLoop
                                             {
                                                 public Node? Root { get; set; }
                                             }

                                             public static class Engine
                                             {
                                                 public static MainLoop? GetMainLoop() => default;
                                             }
                                         }

                                         """ + applicationSource;
    }
}
