extern alias SerializationGenerator;
using System.Linq;
using System.Reflection;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.Text;
using NUnit.Framework;
using SerializationGenerator::Robust.Roslyn.Shared;
using SerializationGenerator::Robust.Serialization.Generator;

namespace Robust.Analyzers.Tests;

[TestFixture]
[TestOf(typeof(ComponentPauseGenerator))]
[Parallelizable(ParallelScope.All)]
public sealed class ComponentPauseGeneratorTest
{
    private const string TypesCode = """
        global using System;
        global using Robust.Shared.Analyzers;
        global using Robust.Shared.GameObjects;

        namespace Robust.Shared.Analyzers
        {
            [AttributeUsage(AttributeTargets.Class, Inherited = false)]
            public sealed class AutoGenerateComponentPauseAttribute : Attribute
            {
                public bool Dirty = false;
            }

            [AttributeUsage(AttributeTargets.Field | AttributeTargets.Property)]
            public sealed class AutoPausedFieldAttribute : Attribute;

            [AttributeUsage(AttributeTargets.Field | AttributeTargets.Property)]
            public sealed class AutoNetworkedFieldAttribute : Attribute
            {
            }
        }

        namespace Robust.Shared.GameObjects
        {
            public interface IComponent;
        }
        """;

    [Test]
    public void TestBasic()
    {
        var result = RunGenerator("""
            [AutoGenerateComponentPause]
            public sealed partial class FooComponent : IComponent
            {
                [AutoPausedField]
                public TimeSpan Foo;
            }
            """);

        ExpectNoDiagnostics(result);
        ExpectSource(
            result,
            """
            // <auto-generated />

            using Robust.Shared.GameObjects;

            public partial class FooComponent
            {
            [RobustAutoGenerated]
            [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]
            public sealed class FooComponent_AutoPauseSystem : EntitySystem
            {
                public override void Initialize()
                {
                    SubscribeLocalEvent<FooComponent, EntityUnpausedEvent>(OnEntityUnpaused);
                }

                private void OnEntityUnpaused(EntityUid uid, FooComponent component, ref EntityUnpausedEvent args)
                {
                    component.Foo += args.PausedTime;
                }
            }
            }

            """);
    }

    [Test]
    public void TestNullable()
    {
        var result = RunGenerator("""
            [AutoGenerateComponentPause]
            public sealed partial class FooComponent : IComponent
            {
                [AutoPausedField]
                public TimeSpan? Foo;
            }
            """);

        ExpectNoDiagnostics(result);
        ExpectSource(
            result,
            """
            // <auto-generated />

            using Robust.Shared.GameObjects;

            public partial class FooComponent
            {
            [RobustAutoGenerated]
            [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]
            public sealed class FooComponent_AutoPauseSystem : EntitySystem
            {
                public override void Initialize()
                {
                    SubscribeLocalEvent<FooComponent, EntityUnpausedEvent>(OnEntityUnpaused);
                }

                private void OnEntityUnpaused(EntityUid uid, FooComponent component, ref EntityUnpausedEvent args)
                {
                    if (component.Foo.HasValue)
                        component.Foo = component.Foo.Value + args.PausedTime;
                }
            }
            }

            """);
    }

    [Test]
    public void TestDictionary()
    {
        var result = RunGenerator("""
            [AutoGenerateComponentPause]
            public sealed partial class FooComponent : IComponent
            {
                [AutoPausedField]
                public Dictionary<string, TimeSpan> Foo;
            }
            """);

        ExpectNoDiagnostics(result);
        ExpectSource(
            result,
            """
            // <auto-generated />

            using Robust.Shared.GameObjects;

            public partial class FooComponent
            {
            [RobustAutoGenerated]
            [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]
            public sealed class FooComponent_AutoPauseSystem : EntitySystem
            {
                public override void Initialize()
                {
                    SubscribeLocalEvent<FooComponent, EntityUnpausedEvent>(OnEntityUnpaused);
                }

                private void OnEntityUnpaused(EntityUid uid, FooComponent component, ref EntityUnpausedEvent args)
                {
                    foreach (var key in component.Foo.Keys)
                        component.Foo[key] += args.PausedTime;
                }
            }
            }

            """);
    }

    [Test]
    public void TestAutoState()
    {
        var result = RunGenerator("""
            [AutoGenerateComponentPause]
            public sealed partial class FooComponent : IComponent
            {
                [AutoPausedField, AutoNetworkedField]
                public TimeSpan Foo;
            }
            """);

        ExpectNoDiagnostics(result);
        ExpectSource(
            result,
            """
            // <auto-generated />

            using Robust.Shared.GameObjects;

            public partial class FooComponent
            {
            [RobustAutoGenerated]
            [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]
            public sealed class FooComponent_AutoPauseSystem : EntitySystem
            {
                public override void Initialize()
                {
                    SubscribeLocalEvent<FooComponent, EntityUnpausedEvent>(OnEntityUnpaused);
                }

                private void OnEntityUnpaused(EntityUid uid, FooComponent component, ref EntityUnpausedEvent args)
                {
                    component.Foo += args.PausedTime;
                    Dirty(uid, component);
                }
            }
            }

            """);
    }

    [Test]
    public void TestExplicitDirty()
    {
        var result = RunGenerator("""
            [AutoGenerateComponentPause(Dirty = true)]
            public sealed partial class FooComponent : IComponent
            {
                [AutoPausedField]
                public TimeSpan Foo;
            }
            """);

        ExpectNoDiagnostics(result);
        ExpectSource(
            result,
            """
            // <auto-generated />

            using Robust.Shared.GameObjects;

            public partial class FooComponent
            {
            [RobustAutoGenerated]
            [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]
            public sealed class FooComponent_AutoPauseSystem : EntitySystem
            {
                public override void Initialize()
                {
                    SubscribeLocalEvent<FooComponent, EntityUnpausedEvent>(OnEntityUnpaused);
                }

                private void OnEntityUnpaused(EntityUid uid, FooComponent component, ref EntityUnpausedEvent args)
                {
                    component.Foo += args.PausedTime;
                    Dirty(uid, component);
                }
            }
            }

            """);
    }

    [Test]
    public void TestDiagnosticNotIComponent()
    {
        var result = RunGenerator("""
            [AutoGenerateComponentPause]
            public sealed partial class FooComponent
            {
                [AutoPausedField]
                public TimeSpan Foo;
            }
            """);

        ExpectNoSource(result);
        ExpectDiagnostics(result, [
            (Diagnostics.IdComponentPauseNotComponent, new LinePositionSpan(new LinePosition(1, 28), new LinePosition(1, 40)))
        ]);
    }

    [Test]
    public void TestDiagnosticNoFields()
    {
        var result = RunGenerator("""
            [AutoGenerateComponentPause]
            public sealed partial class FooComponent : IComponent
            {
                public TimeSpan Foo;
            }
            """);

        ExpectNoSource(result);
        ExpectDiagnostics(result, [
            (Diagnostics.IdComponentPauseNoFields, new LinePositionSpan(new LinePosition(1, 28), new LinePosition(1, 40)))
        ]);
    }

    [Test]
    public void TestDiagnosticNoParentAttribute()
    {
        var result = RunGenerator("""
            public sealed partial class FooComponent : IComponent
            {
                [AutoPausedField]
                public TimeSpan Foo, Fooz;

                [AutoPausedField]
                public TimeSpan Bar { get; set; }
            }
            """);

        ExpectNoSource(result);
        ExpectDiagnostics(result, [
            (Diagnostics.IdComponentPauseNoParentAttribute, new LinePositionSpan(new LinePosition(3, 20), new LinePosition(3, 23))),
            (Diagnostics.IdComponentPauseNoParentAttribute, new LinePositionSpan(new LinePosition(3, 25), new LinePosition(3, 29))),
            (Diagnostics.IdComponentPauseNoParentAttribute, new LinePositionSpan(new LinePosition(6, 20), new LinePosition(6, 23)))
        ]);
    }

    [Test]
    public void TestDiagnosticWrongType()
    {
        var result = RunGenerator("""
            [AutoGenerateComponentPause]
            public sealed partial class FooComponent : IComponent
            {
                [AutoPausedField]
                public int Foo, Fooz;

                [AutoPausedField]
                public int Bar { get; set; }
            }
            """);

        ExpectNoSource(result);
        ExpectDiagnostics(result, [
            (Diagnostics.IdComponentPauseWrongTypeAttribute, new LinePositionSpan(new LinePosition(4, 15), new LinePosition(4, 18))),
            (Diagnostics.IdComponentPauseWrongTypeAttribute, new LinePositionSpan(new LinePosition(4, 20), new LinePosition(4, 24))),
            (Diagnostics.IdComponentPauseWrongTypeAttribute, new LinePositionSpan(new LinePosition(7, 15), new LinePosition(7, 18)))
        ]);
    }

    private static void ExpectSource(GeneratorRunResult result, string expected)
    {
        Assert.That(result.GeneratedSources, Has.Length.EqualTo(1));

        var source = result.GeneratedSources[0];

        Assert.That(source.SourceText.ToString(), Is.EqualTo(expected));
    }

    private static void ExpectNoSource(GeneratorRunResult result)
    {
        Assert.That(result.GeneratedSources, Is.Empty);
    }

    private static void ExpectNoDiagnostics(GeneratorRunResult result)
    {
        Assert.That(result.Diagnostics, Is.Empty);
    }

    private static void ExpectDiagnostics(GeneratorRunResult result, (string code, LinePositionSpan span)[] diagnostics)
    {
        Assert.Multiple(() =>
        {
            Assert.That(result.Diagnostics, Has.Length.EqualTo(diagnostics.Length));
            foreach (var (code, span) in diagnostics)
            {
                Assert.That(
                    result.Diagnostics.Any(x => x.Id == code && x.Location.GetLineSpan().Span == span),
                    $"Expected diagnostic with code {code} and location {span}");
            }
        });
    }

    private static GeneratorRunResult RunGenerator(string source)
    {
        var compilation = (Compilation)CSharpCompilation.Create("compilation",
            new[]
            {
                CSharpSyntaxTree.ParseText(source, path: "Source.cs"),
                CSharpSyntaxTree.ParseText(TypesCode, path: "Types.cs")
            },
            new[] { MetadataReference.CreateFromFile(typeof(Binder).GetTypeInfo().Assembly.Location) },
            new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary));

        var generator = new ComponentPauseGenerator();
        GeneratorDriver driver = CSharpGeneratorDriver.Create(generator);
        driver = driver.RunGeneratorsAndUpdateCompilation(compilation, out var newCompilation, out _);
        var result = driver.GetRunResult();

        return result.Results[0];
    }
}
