﻿// 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.

using System.Collections.Immutable;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.Editor.UnitTests.Utilities;
using Microsoft.CodeAnalysis.Editor.UnitTests.Workspaces;
using Microsoft.CodeAnalysis.FindUsages;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.CodeAnalysis.StackTraceExplorer;
using Microsoft.CodeAnalysis.Test.Utilities;
using Roslyn.Test.Utilities;
using Xunit;

namespace Microsoft.CodeAnalysis.UnitTests.StackTraceExplorer
{
    [UseExportProvider]
    public class StackTraceExplorerTests
    {
        private static async Task TestSymbolFoundAsync(string inputLine, string code)
        {
            using var workspace = TestWorkspace.CreateCSharp(code);
            var result = await StackTraceAnalyzer.AnalyzeAsync(inputLine, CancellationToken.None);
            Assert.Single(result.ParsedFrames);

            var stackFrame = result.ParsedFrames[0] as ParsedStackFrame;
            AssertEx.NotNull(stackFrame);

            // Test that ToString() and reparsing keeps the same outcome
            var reparsedResult = await StackTraceAnalyzer.AnalyzeAsync(stackFrame.ToString(), CancellationToken.None);
            Assert.Single(reparsedResult.ParsedFrames);

            var reparsedFrame = reparsedResult.ParsedFrames[0] as ParsedStackFrame;
            AssertEx.NotNull(reparsedFrame);
            StackFrameUtils.AssertEqual(stackFrame.Root, reparsedFrame.Root);

            // Get the definition for the parsed frame
            var service = workspace.Services.GetRequiredService<IStackTraceExplorerService>();
            var definition = await service.TryFindDefinitionAsync(workspace.CurrentSolution, stackFrame, StackFrameSymbolPart.Method, CancellationToken.None);
            AssertEx.NotNull(definition);

            // Get the symbol that was indicated in the source code by cursor position
            var cursorDoc = workspace.Documents.Single();
            var selectedSpan = cursorDoc.SelectedSpans.Single();
            var doc = workspace.CurrentSolution.GetRequiredDocument(cursorDoc.Id);
            var root = await doc.GetRequiredSyntaxRootAsync(CancellationToken.None);
            var node = root.FindNode(selectedSpan);
            var semanticModel = await doc.GetRequiredSemanticModelAsync(CancellationToken.None);

            var expectedSymbol = semanticModel.GetDeclaredSymbol(node);
            AssertEx.NotNull(expectedSymbol);

            // Compare the definition found to the definition for the test symbol
            var expectedDefinition = expectedSymbol.ToNonClassifiedDefinitionItem(workspace.CurrentSolution, includeHiddenLocations: true);

            Assert.Equal(expectedDefinition.IsExternal, definition.IsExternal);
            AssertEx.SetEqual(expectedDefinition.NameDisplayParts, definition.NameDisplayParts);
            AssertEx.SetEqual(expectedDefinition.OriginationParts, definition.OriginationParts);
            AssertEx.SetEqual(expectedDefinition.Properties, definition.Properties);
            AssertEx.SetEqual(expectedDefinition.SourceSpans, definition.SourceSpans);
            AssertEx.SetEqual(expectedDefinition.Tags, definition.Tags);
        }

        private static void AssertContents(ImmutableArray<ParsedFrame> frames, params string[] contents)
        {
            Assert.Equal(contents.Length, frames.Length);
            for (var i = 0; i < contents.Length; i++)
            {
                Assert.Equal(contents[i], frames[i].ToString());
            }
        }

        [Fact]
        public Task TestSymbolFound_DebuggerLine()
        {
            return TestSymbolFoundAsync(
                "ConsoleApp4.dll!ConsoleApp4.MyClass.M()",
                @"using System;

namespace ConsoleApp4
{
    class MyClass
    {
        void [|M|]() {}
    }
}");
        }

        [Theory]
        [InlineData("object", "Object")]
        [InlineData("bool", "Boolean")]
        [InlineData("sbyte", "SByte")]
        [InlineData("byte", "Byte")]
        [InlineData("decimal", "Decimal")]
        [InlineData("float", "Single")]
        [InlineData("double", "Double")]
        [InlineData("short", "Int16")]
        [InlineData("int", "Int32")]
        [InlineData("long", "Int64")]
        [InlineData("string", "String")]
        [InlineData("ushort", "UInt16")]
        [InlineData("uint", "UInt32")]
        [InlineData("ulong", "UInt64")]
        public Task TestSpecialTypes(string type, string typeName)
        {
            return TestSymbolFoundAsync(
                $"at ConsoleApp.MyClass.M({typeName} value)",
                @$"using System;

namespace ConsoleApp
{{
    class MyClass
    {{
        void [|M|]({type} value) {{}}
    }}
}}");
        }

        [Fact]
        public Task TestSymbolFound_DebuggerLine_SingleSimpleClassParam()
        {
            return TestSymbolFoundAsync(
                "ConsoleApp4.dll!ConsoleApp4.MyClass.M(String s)",
                @"using System;

namespace ConsoleApp4
{
    class MyClass
    {
        void [|M|](string s) {}
    }
}");
        }

        [Fact]
        public Task TestSymbolFound_ExceptionLine()
        {
            return TestSymbolFoundAsync(
                "at ConsoleApp4.MyClass.M()",
                @"using System;

namespace ConsoleApp4
{
    class MyClass
    {
        void [|M|]() {}
    }
}");
        }

        [Fact]
        public Task TestSymbolFound_ExceptionLine_SingleSimpleClassParam()
        {
            return TestSymbolFoundAsync(
                "at ConsoleApp4.MyClass.M(String s)",
                @"using System;

namespace ConsoleApp4
{
    class MyClass
    {
        void [|M|](string s) {}
    }
}");
        }

        [Fact]
        public Task TestSymbolFound_ExceptionLineWithFile()
        {
            return TestSymbolFoundAsync(
                @"at ConsoleApp4.MyClass.M() in C:\repos\ConsoleApp4\ConsoleApp4\Program.cs:line 26",
                @"using System;

namespace ConsoleApp4
{
    class MyClass
    {
        void [|M|]() {}
    }
}");
        }

        [Fact]
        public Task TestSymbolFound_GenericType()
        {
            return TestSymbolFoundAsync(
                @"at ConsoleApp.MyClass`1.M(String s)",
                @"using System;
namespace ConsoleApp
{
    class MyClass<T> 
    {
        void [|M|](string s) { }
    }
}");
        }

        [Fact]
        public Task TestSymbolFound_GenericType2()
        {
            return TestSymbolFoundAsync(
                @"at ConsoleApp.MyClass`2.M(String s)",
                @"using System;
namespace ConsoleApp
{
    class MyClass<T, U> 
    {
        void [|M|](string s) { }
    }
}");
        }

        [Fact]
        public Task TestSymbolFound_GenericType_GenericArg()
        {
            return TestSymbolFoundAsync(
                @"at ConsoleApp.MyClass`1.M(T s)",
                @"using System;
namespace ConsoleApp
{
    class MyClass<T>
    {
        void [|M|](T s) { }
    }
}");
        }

        [Fact]
        public Task TestSymbolFound_ExceptionLine_GenericMethod()
        {
            return TestSymbolFoundAsync(
                @"at ConsoleApp4.MyClass.M[T](T t) in C:\repos\Test\MyClass.cs:line 7",
                @"using System;

namespace ConsoleApp4
{
    class MyClass
    {
        void [|M|]<T>(T t) {}
    }
}");
        }

        [Fact]
        public Task TestSymbolFound_ExceptionLine_GenericMethod_FromActivityLog()
        {
            return TestSymbolFoundAsync(
                @"at ConsoleApp4.MyClass.M&lt;T&gt;(T t)",
                @"using System;

namespace ConsoleApp4
{
    class MyClass
    {
        void [|M|]<T>(T t) {}
    }
}");
        }

        [Fact]
        public Task TestSymbolFound_ExceptionLine_MultipleGenerics()
        {
            return TestSymbolFoundAsync(
                "at ConsoleApp4.MyClass.M<T>(T t)",
                @"using System;

namespace ConsoleApp4
{
    class MyClass
    {
        void [|M|]<T>(T t) {}
    }
}");
        }

        [Fact]
        public Task TestSymbolFound_ParameterSpacing()
        {
            return TestSymbolFoundAsync(
                "at ConsoleApp.MyClass.M( String   s    )",
                @"
namespace ConsoleApp
{
    class MyClass
    {
        void [|M|](string s)
        {
        }
    }
}");
        }

        [Fact]
        public Task TestSymbolFound_OverloadsWithSameName()
        {
            return TestSymbolFoundAsync(
                "at ConsoleApp.MyClass.M(String value)",
                @"
namespace ConsoleApp
{
    class MyClass
    {
        void [|M|](string value)
        {
        }

        void M(int value)
        {
        }
    }
}");
        }

        [Fact]
        public Task TestSymbolFound_ArrayParameter()
        {
            return TestSymbolFoundAsync(
                "at ConsoleApp.MyClass.M(String[] s)",
                @"
namespace ConsoleApp
{
    class MyClass
    {
        void [|M|](string[] s)
        {
        }
    }
}");
        }

        [Fact]
        public Task TestSymbolFound_MultidimensionArrayParameter()
        {
            return TestSymbolFoundAsync(
                "at ConsoleApp.MyClass.M(String[,] s)",
                @"
namespace ConsoleApp
{
    class MyClass
    {
        void [|M|](string[,] s)
        {
        }
    }
}");
        }

        [Fact]
        public Task TestSymbolFound_MultidimensionArrayParameter_WithSpaces()
        {
            return TestSymbolFoundAsync(
                "at ConsoleApp.MyClass.M(String[ , ] s)",
                @"
namespace ConsoleApp
{
    class MyClass
    {
        void [|M|](string[,] s)
        {
        }
    }
}");
        }

        [Fact]
        public Task TestSymbolFound_MultidimensionArrayParameter_WithSpaces2()
        {
            return TestSymbolFoundAsync(
                "at ConsoleApp.MyClass.M(String[,] s)",
                @"
namespace ConsoleApp
{
    class MyClass
    {
        void [|M|](string[ , ] s)
        {
        }
    }
}");
        }

        [Fact]
        public Task TestSymbolFound_MultidimensionArrayParameter2()
        {
            return TestSymbolFoundAsync(
                "at ConsoleApp.MyClass.M(String[,][] s)",
                @"
namespace ConsoleApp
{
    class MyClass
    {
        void [|M|](string[,][] s)
        {
        }
    }
}");
        }

        [Fact(Skip = "Symbol search for nested types does not work")]
        public Task TestSymbolFound_ExceptionLine_GenericsHierarchy()
        {
            return TestSymbolFoundAsync(
                "at ConsoleApp4.MyClass`1.MyInnerClass`1.M[T](T t)",
                @"using System;

namespace ConsoleApp4
{
    class MyClass<A>
    {
        public class MyInnerClass<B>
        {
            public void [|M|]<T>(T t) 
            {
                throw new Exception();
            }
        }
    }
}");
        }

        [Fact(Skip = "ref params do not work yet")]
        public Task TestSymbolFound_ExceptionLine_RefArg()
        {
            return TestSymbolFoundAsync(
                @"at ConsoleApp4.MyClass.M(String& s) in C:\repos\Test\MyClass.cs:line 8",
                @"using System;

namespace ConsoleApp4
{
    class MyClass
    {
        void [|M|](ref string s)
        {
            s = string.Empty;
        }
    }
}");
        }

        [Fact(Skip = "out params do not work yet")]
        public Task TestSymbolFound_ExceptionLine_OutArg()
        {
            return TestSymbolFoundAsync(
                @"at ConsoleApp4.MyClass.M(String& s) in C:\repos\Test\MyClass.cs:line 8",
                @"using System;

namespace ConsoleApp4
{
    class MyClass
    {
        void [|M|](out string s)
        {
            s = string.Empty;
        }
    }
}");
        }

        [Fact(Skip = "in params do not work yet")]
        public Task TestSymbolFound_ExceptionLine_InArg()
        {
            return TestSymbolFoundAsync(
                @"at ConsoleApp4.MyClass.M(Int32& i)",
                @"using System;

namespace ConsoleApp4
{
    class MyClass
    {
        void [|M|](in int i)
        {
            throw new Exception();
        }
    }
}");
        }

        [Fact(Skip = "Generated types/methods are not supported")]
        public Task TestSymbolFound_ExceptionLine_AsyncMethod()
        {
            return TestSymbolFoundAsync(
                @"at ConsoleApp4.MyClass.<>c.<DoThingAsync>b__1_0() in C:\repos\Test\MyClass.cs:line 15",
                @"namespace ConsoleApp4
{
    class MyClass
    {
        public async Task M()
        {
            await DoThingAsync();
        }

        async Task DoThingAsync()
        {
            var task = new Task(() => 
            {
                Console.WriteLine(""Doing async work"");
                throw new Exception();
            });

            task.Start();

            await task;
        }
    }
}");
        }

        [Fact]
        public Task TestSymbolFound_ExceptionLine_PropertySet()
        {
            return TestSymbolFoundAsync(
                @"at ConsoleApp4.MyClass.set_I(Int32 value)",
                @"using System;

namespace ConsoleApp4
{
    class MyClass
    {
        public int I
        {
            get => throw new Exception();
            [|set|] => throw new Exception();
        }
    }
}");
        }

        [Fact]
        public Task TestSymbolFound_ExceptionLine_PropertyGet()
        {
            return TestSymbolFoundAsync(
                @"at ConsoleApp4.MyClass.get_I()",
                @"using System;

namespace ConsoleApp4
{
    class MyClass
    {
        public int I
        {
            [|get|] => throw new Exception();
            set => throw new Exception();
        }
    }
}");
        }

        [Fact]
        public Task TestSymbolFound_ExceptionLine_IndexerSet()
        {
            return TestSymbolFoundAsync(
                @"at ConsoleApp4.MyClass.set_Item(Int32 i, Int32 value)",
                @"using System;

namespace ConsoleApp4
{
    class MyClass
    {
        public int this[int i]
        {
            get => throw new Exception();
            [|set|] => throw new Exception();
        }
    }
}");
        }

        [Fact]
        public Task TestSymbolFound_ExceptionLine_IndexerGet()
        {
            return TestSymbolFoundAsync(
                @"at ConsoleApp4.MyClass.get_Item(Int32 i)",
                @"using System;

namespace ConsoleApp4
{
    class MyClass
    {
        public int this[int i]
        {
            [|get|] => throw new Exception();
            set => throw new Exception();
        }
    }
}");
        }

        [Fact]
        public Task TestSymbolFound_ExceptionLine_LocalFunction()
        {
            return TestSymbolFoundAsync(
                @"at ConsoleApp4.MyClass.<M>g__LocalFunction|0_0()",
                @"using System;

namespace ConsoleApp4
{
    class MyClass
    {
        public void M()
        {
            LocalFunction();

            void [|LocalFunction|]()
            {
                throw new Exception();
            }
        }

        public void LocalFunction()
        {
        }
    }
}");
        }

        [Fact]
        public Task TestSymbolFound_ExceptionLine_MultipleLocalFunctions()
        {
            return TestSymbolFoundAsync(
                @"at ConsoleApp4.MyClass.<M>g__LocalFunction|0_0()",
                @"using System;

namespace ConsoleApp4
{
    class MyClass
    {
        public void M()
        {
            LocalFunction();

            void [|LocalFunction|]()
            {
                throw new Exception();
            }
        }

        public void M2()
        {
            LocalFunction();

            void LocalFunction()
            {
                throw new Exception();
            }
        }
    }
}");
        }

        [Fact]
        public Task TestSymbolFound_ExceptionLine_MultipleLocalFunctions2()
        {
            return TestSymbolFoundAsync(
                @"at ConsoleApp4.MyClass.<M2>g__LocalFunction|0_0()",
                @"using System;

namespace ConsoleApp4
{
    class MyClass
    {
        public void M()
        {
            LocalFunction();

            void LocalFunction()
            {
                throw new Exception();
            }
        }

        public void M2()
        {
            LocalFunction();

            void [|LocalFunction()|]
            {
                throw new Exception();
            }
        }
    }
}");
        }

        [Fact]
        public Task TestSymbolFound_ExceptionLine_MemberFunctionSameNameAsFunction()
        {
            return TestSymbolFoundAsync(
                @"at ConsoleApp4.MyClass.LocalFunction()",
                @"using System;

namespace ConsoleApp4
{
    class MyClass
    {
        public void M()
        {
            LocalFunction();

            void LocalFunction()
            {
                throw new Exception();
            }
        }

        public void [|LocalFunction|]()
        {
        }
    }
}");
        }

        /// <summary>
        /// Behavior for this test needs some explanation. Note that if there are multiple
        /// local functions within a container, they will be uniquely identified by the 
        /// suffix. In this case we have g__Local|0_0 and g__Local|0_1 as the two local functions.
        /// Resolution doesn't try to reverse engineer how these suffixes get produced, which means
        /// that the first applicable symbol with the name "Local" inside the method "M" will be found.
        /// Since local function resolution is done by searching the descendents of the method "M", the top
        /// most local function matching the name will be the first the resolver sees and considers applicable.
        /// This should get the user close to what they want, and hopefully is rare enough that it won't
        /// be frequently encountered. 
        /// </summary>
        [Fact]
        public Task TestSymbolFound_ExceptionLine_NestedLocalFunctions()
        {
            return TestSymbolFoundAsync(
                @"at C.<M>g__Local|0_1()",
                @"using System;

class C 
{
    public void M()
    {
        Local();
        
        void [|Local|]()
        {
            Local();
            
            void Local()
            {
                throw new Exception();
            }
        }
    }
}");
        }

        [Fact(Skip = "Top level local functions are not supported")]
        public Task TestSymbolFound_ExceptionLine_LocalInTopLevelStatement()
        {
            return TestSymbolFoundAsync(
                @"at ConsoleApp4.Program.<Main$>g__LocalInTopLevelStatement|0_0()",
                @"using System;

LocalInTopLevelStatement();

void [|LocalInTopLevelStatement|]()
{
    throw new Exception();
}");
        }

        [Fact(Skip = "The parser doesn't correctly handle ..ctor() methods yet")]
        public Task TestSymbolFound_ExceptionLine_Constructor()
        {
            return TestSymbolFoundAsync(
                @"at ConsoleApp4.MyClass..ctor()",
                @"namespace ConsoleApp4
{
    class MyClass
    {
        public MyClass()
        {
            throw new Exception();
        }

        ~MyClass()
        {
            throw new Exception();
        }
    }
}");
        }

        [Theory]
        [InlineData("alkjsdflkjasdlkfjasd")]
        [InlineData("at alksjdlfjasdlkfj")]
        [InlineData("line 26")]
        [InlineData("alksdjflkjsadf.cs:line 26")]
        [InlineData("This,that.A,,,,,,,,,b()")]
        [InlineData("ConsoleWriteLine()")]
        [InlineData("at <><>.<><>()")]
        [InlineData("at 897098.70987__ ()")]
        [InlineData("at jlksdjf . kljsldkjf () in aklsjdflkj")]
        public async Task TestFailureToParse(string line)
        {
            var result = await StackTraceAnalyzer.AnalyzeAsync(line, CancellationToken.None);
            Assert.Equal(1, result.ParsedFrames.Length);

            var ignoredFrames = result.ParsedFrames.OfType<IgnoredFrame>();
            AssertEx.SetEqual(result.ParsedFrames, ignoredFrames);
        }

        /// <summary>
        /// Tests cases where the text will technically parse and look like a symbol, but does not point to
        /// a symbol in the solution. 
        /// </summary>
        [Theory]
        [InlineData("at __.__._()")]
        [InlineData("abcd!__.__._()")]
        public async Task TestInvalidSymbol(string line)
        {
            using var workspace = TestWorkspace.CreateCSharp(@"
class C
{
}");

            var result = await StackTraceAnalyzer.AnalyzeAsync(line, CancellationToken.None);
            Assert.Equal(1, result.ParsedFrames.Length);

            var parsedFame = result.ParsedFrames.OfType<ParsedStackFrame>().Single();
            var service = workspace.Services.GetRequiredService<IStackTraceExplorerService>();
            var definition = await service.TryFindDefinitionAsync(workspace.CurrentSolution, parsedFame, StackFrameSymbolPart.Method, CancellationToken.None);
            Assert.Null(definition);
        }

        [Fact]
        public async Task TestActivityLogParsing()
        {
            var activityLogException = @"Exception occurred while loading solution options: System.Runtime.InteropServices.COMException (0x8000FFFF): Catastrophic failure (Exception from HRESULT: 0x8000FFFF (E_UNEXPECTED))&#x000D;&#x000A;   at System.Runtime.InteropServices.Marshal.ThrowExceptionForHRInternal(Int32 errorCode, IntPtr errorInfo)&#x000D;&#x000A;   at Microsoft.VisualStudio.Shell.Package.Initialize()&#x000D;&#x000A;--- End of stack trace from previous location where exception was thrown ---&#x000D;&#x000A;   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw&lt;string&gt;()&#x000D;&#x000A;   at Microsoft.VisualStudio.Telemetry.WindowsErrorReporting.WatsonReport.GetClrWatsonExceptionInfo(Exception exceptionObject)";

            var result = await StackTraceAnalyzer.AnalyzeAsync(activityLogException, CancellationToken.None);
            AssertContents(result.ParsedFrames,
                @"Exception occurred while loading solution options: System.Runtime.InteropServices.COMException (0x8000FFFF): Catastrophic failure (Exception from HRESULT: 0x8000FFFF (E_UNEXPECTED))",
                @"at System.Runtime.InteropServices.Marshal.ThrowExceptionForHRInternal(Int32 errorCode, IntPtr errorInfo)",
                @"at Microsoft.VisualStudio.Shell.Package.Initialize()",
                @"--- End of stack trace from previous location where exception was thrown ---",
                @"at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw<string>()",
                @"at Microsoft.VisualStudio.Telemetry.WindowsErrorReporting.WatsonReport.GetClrWatsonExceptionInfo(Exception exceptionObject)");
        }

        [Fact]
        public async Task TestMetadataSymbol()
        {
            var code = @"class C{}";
            using var workspace = TestWorkspace.CreateCSharp(code);

            var result = await StackTraceAnalyzer.AnalyzeAsync("at System.String.ToLower()", CancellationToken.None);
            Assert.Single(result.ParsedFrames);

            var frame = result.ParsedFrames[0] as ParsedStackFrame;
            AssertEx.NotNull(frame);

            var service = workspace.Services.GetRequiredService<IStackTraceExplorerService>();
            var definition = await service.TryFindDefinitionAsync(workspace.CurrentSolution, frame, StackFrameSymbolPart.Method, CancellationToken.None);

            AssertEx.NotNull(definition);
            Assert.Equal("String.ToLower", definition.NameDisplayParts.ToVisibleDisplayString(includeLeftToRightMarker: false));
        }
    }
}
