// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

#nullable disable

using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.CSharp.Debugging;
using Microsoft.CodeAnalysis.Editor.UnitTests.Workspaces;
using Microsoft.CodeAnalysis.Test.Utilities;
using Xunit;

namespace Microsoft.CodeAnalysis.Editor.CSharp.UnitTests.Debugging
{
    [UseExportProvider]
    [Trait(Traits.Feature, Traits.Features.DebuggingNameResolver)]
    public class NameResolverTests
    {
        private static async Task TestAsync(string text, string searchText, params string[] expectedNames)
        {
            using var workspace = TestWorkspace.CreateCSharp(text);

            var nameResolver = new BreakpointResolver(workspace.CurrentSolution, searchText);
            var results = await nameResolver.DoAsync(CancellationToken.None);

            Assert.Equal(expectedNames, results.Select(r => r.LocationNameOpt));
        }

        [Fact]
        public async Task TestCSharpLanguageDebugInfoCreateNameResolver()
        {
            using var workspace = TestWorkspace.CreateCSharp(" ");

            var debugInfo = new CSharpBreakpointResolutionService();
            var results = await debugInfo.ResolveBreakpointsAsync(workspace.CurrentSolution, "goo", CancellationToken.None);
            Assert.Equal(0, results.Count());
        }

        [Fact]
        public async Task TestSimpleNameInClass()
        {
            var text =
                """
                class C
                {
                  void Goo()
                  {
                  }
                }
                """;
            await TestAsync(text, "Goo", "C.Goo()");
            await TestAsync(text, "goo");
            await TestAsync(text, "C.Goo", "C.Goo()");
            await TestAsync(text, "N.C.Goo");
            await TestAsync(text, "Goo<T>");
            await TestAsync(text, "C<T>.Goo");
            await TestAsync(text, "Goo()", "C.Goo()");
            await TestAsync(text, "Goo(int i)");
            await TestAsync(text, "Goo(int)");
        }

        [Fact]
        public async Task TestSimpleNameInNamespace()
        {
            var text =
                """
                namespace N
                {
                  class C
                  {
                    void Goo()
                    {
                    }
                  }
                }
                """;
            await TestAsync(text, "Goo", "N.C.Goo()");
            await TestAsync(text, "goo");
            await TestAsync(text, "C.Goo", "N.C.Goo()");
            await TestAsync(text, "N.C.Goo", "N.C.Goo()");
            await TestAsync(text, "Goo<T>");
            await TestAsync(text, "C<T>.Goo");
            await TestAsync(text, "Goo()", "N.C.Goo()");
            await TestAsync(text, "C.Goo()", "N.C.Goo()");
            await TestAsync(text, "N.C.Goo()", "N.C.Goo()");
            await TestAsync(text, "Goo(int i)");
            await TestAsync(text, "Goo(int)");
            await TestAsync(text, "Goo(a)");
        }

        [Fact]
        public async Task TestSimpleNameInGenericClassNamespace()
        {
            var text =
                """
                namespace N
                {
                  class C<T>
                  {
                    void Goo()
                    {
                    }
                  }
                }
                """;
            await TestAsync(text, "Goo", "N.C<T>.Goo()");
            await TestAsync(text, "goo");
            await TestAsync(text, "C.Goo", "N.C<T>.Goo()");
            await TestAsync(text, "N.C.Goo", "N.C<T>.Goo()");
            await TestAsync(text, "Goo<T>");
            await TestAsync(text, "C<T>.Goo", "N.C<T>.Goo()");
            await TestAsync(text, "C<T>.Goo()", "N.C<T>.Goo()");
            await TestAsync(text, "Goo()", "N.C<T>.Goo()");
            await TestAsync(text, "C.Goo()", "N.C<T>.Goo()");
            await TestAsync(text, "N.C.Goo()", "N.C<T>.Goo()");
            await TestAsync(text, "Goo(int i)");
            await TestAsync(text, "Goo(int)");
            await TestAsync(text, "Goo(a)");
        }

        [Fact]
        public async Task TestGenericNameInClassNamespace()
        {
            var text =
                """
                namespace N
                {
                  class C
                  {
                    void Goo<T>()
                    {
                    }
                  }
                }
                """;
            await TestAsync(text, "Goo", "N.C.Goo<T>()");
            await TestAsync(text, "goo");
            await TestAsync(text, "C.Goo", "N.C.Goo<T>()");
            await TestAsync(text, "N.C.Goo", "N.C.Goo<T>()");
            await TestAsync(text, "Goo<T>", "N.C.Goo<T>()");
            await TestAsync(text, "Goo<X>", "N.C.Goo<T>()");
            await TestAsync(text, "Goo<T,X>");
            await TestAsync(text, "C<T>.Goo");
            await TestAsync(text, "C<T>.Goo()");
            await TestAsync(text, "Goo()", "N.C.Goo<T>()");
            await TestAsync(text, "C.Goo()", "N.C.Goo<T>()");
            await TestAsync(text, "N.C.Goo()", "N.C.Goo<T>()");
            await TestAsync(text, "Goo(int i)");
            await TestAsync(text, "Goo(int)");
            await TestAsync(text, "Goo(a)");
            await TestAsync(text, "Goo<T>(int i)");
            await TestAsync(text, "Goo<T>(int)");
            await TestAsync(text, "Goo<T>(a)");
        }

        [Fact]
        public async Task TestOverloadsInSingleClass()
        {
            var text =
                """
                class C
                {
                  void Goo()
                  {
                  }

                  void Goo(int i)
                  {
                  }
                }
                """;
            await TestAsync(text, "Goo", "C.Goo()", "C.Goo(int)");
            await TestAsync(text, "goo");
            await TestAsync(text, "C.Goo", "C.Goo()", "C.Goo(int)");
            await TestAsync(text, "N.C.Goo");
            await TestAsync(text, "Goo<T>");
            await TestAsync(text, "C<T>.Goo");
            await TestAsync(text, "Goo()", "C.Goo()");
            await TestAsync(text, "Goo(int i)", "C.Goo(int)");
            await TestAsync(text, "Goo(int)", "C.Goo(int)");
            await TestAsync(text, "Goo(i)", "C.Goo(int)");
        }

        [Fact]
        public async Task TestMethodsInMultipleClasses()
        {
            var text =
                """
                namespace N
                {
                  class C
                  {
                    void Goo()
                    {
                    }
                  }
                }

                namespace N1
                {
                  class C
                  {
                    void Goo(int i)
                    {
                    }
                  }
                }
                """;
            await TestAsync(text, "Goo", "N1.C.Goo(int)", "N.C.Goo()");
            await TestAsync(text, "goo");
            await TestAsync(text, "C.Goo", "N1.C.Goo(int)", "N.C.Goo()");
            await TestAsync(text, "N.C.Goo", "N.C.Goo()");
            await TestAsync(text, "N1.C.Goo", "N1.C.Goo(int)");
            await TestAsync(text, "Goo<T>");
            await TestAsync(text, "C<T>.Goo");
            await TestAsync(text, "Goo()", "N.C.Goo()");
            await TestAsync(text, "Goo(int i)", "N1.C.Goo(int)");
            await TestAsync(text, "Goo(int)", "N1.C.Goo(int)");
            await TestAsync(text, "Goo(i)", "N1.C.Goo(int)");
        }

        [Fact]
        public async Task TestMethodsWithDifferentArityInMultipleClasses()
        {
            var text =
                """
                namespace N
                {
                  class C
                  {
                    void Goo()
                    {
                    }
                  }
                }

                namespace N1
                {
                  class C
                  {
                    void Goo<T>(int i)
                    {
                    }
                  }
                }
                """;
            await TestAsync(text, "Goo", "N1.C.Goo<T>(int)", "N.C.Goo()");
            await TestAsync(text, "goo");
            await TestAsync(text, "C.Goo", "N1.C.Goo<T>(int)", "N.C.Goo()");
            await TestAsync(text, "N.C.Goo", "N.C.Goo()");
            await TestAsync(text, "N1.C.Goo", "N1.C.Goo<T>(int)");
            await TestAsync(text, "Goo<T>", "N1.C.Goo<T>(int)");
            await TestAsync(text, "C<T>.Goo");
            await TestAsync(text, "Goo()", "N.C.Goo()");
            await TestAsync(text, "Goo<T>()");
            await TestAsync(text, "Goo(int i)", "N1.C.Goo<T>(int)");
            await TestAsync(text, "Goo(int)", "N1.C.Goo<T>(int)");
            await TestAsync(text, "Goo(i)", "N1.C.Goo<T>(int)");
            await TestAsync(text, "Goo<T>(int i)", "N1.C.Goo<T>(int)");
            await TestAsync(text, "Goo<T>(int)", "N1.C.Goo<T>(int)");
            await TestAsync(text, "Goo<T>(i)", "N1.C.Goo<T>(int)");
        }

        [Fact]
        public async Task TestOverloadsWithMultipleParametersInSingleClass()
        {
            var text =
                """
                class C
                {
                  void Goo(int a)
                  {
                  }

                  void Goo(int a, string b = "bb")
                  {
                  }

                  void Goo(__arglist)
                  {
                  }
                }
                """;
            await TestAsync(text, "Goo", "C.Goo(int)", "C.Goo(int, [string])", "C.Goo(__arglist)");
            await TestAsync(text, "goo");
            await TestAsync(text, "C.Goo", "C.Goo(int)", "C.Goo(int, [string])", "C.Goo(__arglist)");
            await TestAsync(text, "N.C.Goo");
            await TestAsync(text, "Goo<T>");
            await TestAsync(text, "C<T>.Goo");
            await TestAsync(text, "Goo()", "C.Goo(__arglist)");
            await TestAsync(text, "Goo(int i)", "C.Goo(int)");
            await TestAsync(text, "Goo(int)", "C.Goo(int)");
            await TestAsync(text, "Goo(int x = 42)", "C.Goo(int)");
            await TestAsync(text, "Goo(i)", "C.Goo(int)");
            await TestAsync(text, "Goo(int i, int b)", "C.Goo(int, [string])");
            await TestAsync(text, "Goo(int, bool)", "C.Goo(int, [string])");
            await TestAsync(text, "Goo(i, s)", "C.Goo(int, [string])");
            await TestAsync(text, "Goo(,)", "C.Goo(int, [string])");
            await TestAsync(text, "Goo(int x = 42,)", "C.Goo(int, [string])");
            await TestAsync(text, "Goo(int x = 42, y = 42)", "C.Goo(int, [string])");
            await TestAsync(text, "Goo([attr] x = 42, y = 42)", "C.Goo(int, [string])");
            await TestAsync(text, "Goo(int i, int b, char c)");
            await TestAsync(text, "Goo(int, bool, char)");
            await TestAsync(text, "Goo(i, s, c)");
            await TestAsync(text, "Goo(__arglist)", "C.Goo(int)");
        }

        [Fact]
        public async Task AccessorTests()
        {
            var text =
                """
                class C
                {
                  int Property1 { get { return 42; } }
                  int Property2 { set { } }
                  int Property3 { get; set;}
                }
                """;
            await TestAsync(text, "Property1", "C.Property1");
            await TestAsync(text, "Property2", "C.Property2");
            await TestAsync(text, "Property3", "C.Property3");
        }

        [Fact]
        public async Task NegativeTests()
        {
            var text =
                """
                using System.Runtime.CompilerServices;
                abstract class C
                {
                    public abstract void AbstractMethod(int a);
                    int Field;
                    delegate void Delegate();
                    event Delegate Event;
                    [IndexerName("ABCD")]
                    int this[int i] { get { return i; } }
                    void Goo() { }
                    void Goo(int x = 1, int y = 2) { }
                    ~C() { }
                }
                """;
            await TestAsync(text, "AbstractMethod");
            await TestAsync(text, "Field");
            await TestAsync(text, "Delegate");
            await TestAsync(text, "Event");
            await TestAsync(text, "this");
            await TestAsync(text, "C.this[int]");
            await TestAsync(text, "C.get_Item");
            await TestAsync(text, "C.get_Item(i)");
            await TestAsync(text, "C[i]");
            await TestAsync(text, "ABCD");
            await TestAsync(text, "C.ABCD(int)");
            await TestAsync(text, "42");
            await TestAsync(text, "Goo", "C.Goo()", "C.Goo([int], [int])"); // just making sure it would normally resolve before trying bad syntax
            await TestAsync(text, "Goo Goo");
            await TestAsync(text, "Goo()asdf");
            await TestAsync(text, "Goo(),");
            await TestAsync(text, "Goo(),f");
            await TestAsync(text, "Goo().Goo");
            await TestAsync(text, "Goo(");
            await TestAsync(text, "(Goo");
            await TestAsync(text, "Goo)");
            await TestAsync(text, "(Goo)");
            await TestAsync(text, "Goo(x = 42, y = 42)", "C.Goo([int], [int])"); // just making sure it would normally resolve before trying bad syntax
            await TestAsync(text, "int x = 42");
            await TestAsync(text, "Goo(int x = 42, y = 42");
            await TestAsync(text, "C");
            await TestAsync(text, "C.C");
            await TestAsync(text, "~");
            await TestAsync(text, "~C");
            await TestAsync(text, "C.~C()");
            await TestAsync(text, "");
        }

        [Fact]
        public async Task TestInstanceConstructors()
        {
            var text =
                """
                class C
                {
                  public C() { }
                }

                class G<T>
                {
                  public G() { }
                  ~G() { }
                }
                """;
            await TestAsync(text, "C", "C.C()");
            await TestAsync(text, "C.C", "C.C()");
            await TestAsync(text, "C.C()", "C.C()");
            await TestAsync(text, "C()", "C.C()");
            await TestAsync(text, "C<T>");
            await TestAsync(text, "C<T>()");
            await TestAsync(text, "C(int i)");
            await TestAsync(text, "C(int)");
            await TestAsync(text, "C(i)");
            await TestAsync(text, "G", "G<T>.G()");
            await TestAsync(text, "G()", "G<T>.G()");
            await TestAsync(text, "G.G", "G<T>.G()");
            await TestAsync(text, "G.G()", "G<T>.G()");
            await TestAsync(text, "G<T>.G", "G<T>.G()");
            await TestAsync(text, "G<t>.G()", "G<T>.G()");
            await TestAsync(text, "G<T>");
            await TestAsync(text, "G<T>()");
            await TestAsync(text, "G.G<T>");
            await TestAsync(text, ".ctor");
            await TestAsync(text, ".ctor()");
            await TestAsync(text, "C.ctor");
            await TestAsync(text, "C.ctor()");
            await TestAsync(text, "G.ctor");
            await TestAsync(text, "G<T>.ctor()");
            await TestAsync(text, "Finalize", "G<T>.~G()");
        }

        [Fact]
        public async Task TestStaticConstructors()
        {
            var text =
                """
                class C
                {
                  static C()
                  {
                  }
                }
                """;
            await TestAsync(text, "C", "C.C()");
            await TestAsync(text, "C.C", "C.C()");
            await TestAsync(text, "C.C()", "C.C()");
            await TestAsync(text, "C()", "C.C()");
            await TestAsync(text, "C<T>");
            await TestAsync(text, "C<T>()");
            await TestAsync(text, "C(int i)");
            await TestAsync(text, "C(int)");
            await TestAsync(text, "C(i)");
            await TestAsync(text, "C.cctor");
            await TestAsync(text, "C.cctor()");
        }

        [Fact]
        public async Task TestAllConstructors()
        {
            var text =
                """
                class C
                {
                  static C()
                  {
                  }

                  public C(int i)
                  {
                  }
                }
                """;
            await TestAsync(text, "C", "C.C(int)", "C.C()");
            await TestAsync(text, "C.C", "C.C(int)", "C.C()");
            await TestAsync(text, "C.C()", "C.C()");
            await TestAsync(text, "C()", "C.C()");
            await TestAsync(text, "C<T>");
            await TestAsync(text, "C<T>()");
            await TestAsync(text, "C(int i)", "C.C(int)");
            await TestAsync(text, "C(int)", "C.C(int)");
            await TestAsync(text, "C(i)", "C.C(int)");
        }

        [Fact]
        public async Task TestPartialMethods()
        {
            var text =
                """
                partial class C
                {
                  partial int M1();

                  partial void M2() { }

                  partial void M2();

                  partial int M3();

                  partial int M3(int x) { return 0; }

                  partial void M4() { }
                }
                """;
            await TestAsync(text, "M1");
            await TestAsync(text, "C.M1");
            await TestAsync(text, "M2", "C.M2()");
            await TestAsync(text, "M3", "C.M3(int)");
            await TestAsync(text, "M3()");
            await TestAsync(text, "M3(y)", "C.M3(int)");
            await TestAsync(text, "M4", "C.M4()");
        }

        [Fact]
        public async Task TestLeadingAndTrailingText()
        {
            var text =
                """
                class C
                {
                  void Goo() { };
                }
                """;
            await TestAsync(text, "Goo;", "C.Goo()");
            await TestAsync(text,
@"Goo();", "C.Goo()");
            await TestAsync(text, "  Goo;", "C.Goo()");
            await TestAsync(text, "  Goo;;");
            await TestAsync(text, "  Goo; ;");
            await TestAsync(text,
@"Goo();", "C.Goo()");
            await TestAsync(text,
@"Goo();", "C.Goo()");
            await TestAsync(text,
@"Goo(); // comment", "C.Goo()");
            await TestAsync(text,
                """
                /*comment*/
                           Goo(/* params */); /* comment
                """, "C.Goo()");
        }

        [Fact]
        public async Task TestEscapedKeywords()
        {
            var text =
                """
                struct @true { }
                class @foreach
                {
                    void where(@true @this) { }
                    void @false() { }
                }
                """;
            await TestAsync(text, "where", "@foreach.where(@true)");
            await TestAsync(text, "@where", "@foreach.where(@true)");
            await TestAsync(text, "@foreach.where", "@foreach.where(@true)");
            await TestAsync(text, "foreach.where");
            await TestAsync(text, "@foreach.where(true)");
            await TestAsync(text, "@foreach.where(@if)", "@foreach.where(@true)");
            await TestAsync(text, "false");
        }

        [Fact]
        public async Task TestAliasQualifiedNames()
        {
            var text =
                """
                extern alias A
                class C
                {
                    void Goo(D d) { }
                }
                """;
            await TestAsync(text, "A::Goo");
            await TestAsync(text, "A::Goo(A::B)");
            await TestAsync(text, "A::Goo(A::B)");
            await TestAsync(text, "A::C.Goo");
            await TestAsync(text, "C.Goo(A::Q)", "C.Goo(D)");
        }

        [Fact]
        public async Task TestNestedTypesAndNamespaces()
        {
            var text =
                """
                namespace N1
                {
                  class C
                  {
                    void Goo() { }
                  }
                  namespace N2
                  {
                    class C { }
                  }
                  namespace N3
                  {
                    class D { }
                  }
                  namespace N4
                  {
                    class C
                    {
                      void Goo(double x) { }

                      class D
                      {
                        void Goo() { }

                        class E
                        {
                          void Goo() { }
                        }
                      }
                    }
                  }
                  namespace N5 { }
                }
                """;

            await TestAsync(text, "Goo", "N1.N4.C.Goo(double)", "N1.N4.C.D.Goo()", "N1.N4.C.D.E.Goo()", "N1.C.Goo()");
            await TestAsync(text, "C.Goo", "N1.N4.C.Goo(double)", "N1.C.Goo()");
            await TestAsync(text, "D.Goo", "N1.N4.C.D.Goo()");
            await TestAsync(text, "N1.N4.C.D.Goo", "N1.N4.C.D.Goo()");
            await TestAsync(text, "N1.Goo");
            await TestAsync(text, "N3.C.Goo");
            await TestAsync(text, "N5.C.Goo");
        }

        [Fact]
        public async Task TestInterfaces()
        {
            var text =
                """
                interface I1
                {
                  void Goo();
                }
                class C1 : I1
                {
                  void I1.Goo() { }
                }
                """;

            await TestAsync(text, "Goo", "C1.Goo()");
            await TestAsync(text, "I1.Goo");
            await TestAsync(text, "C1.Goo", "C1.Goo()");
            await TestAsync(text, "C1.I1.Moo");
        }
    }
}
