// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Runtime.CompilerServices;
using System.Threading.Tasks;
using Microsoft.Extensions.StackTrace.Sources;
using ThrowingLibrary;
using Xunit;

namespace Microsoft.Extensions.Internal
{
    public class StackTraceHelperTest
    {
        [Fact]
        public void StackTraceHelper_IncludesLineNumbersForFiles()
        {
            // Arrange
            Exception exception = null;
            try
            {
                // Throwing an exception in the current assembly always seems to populate the full stack
                // trace regardless of symbol type. Crossing assembly boundaries ensures PortablePdbReader gets used
                // on desktop.
                Thrower.Throw();
            }
            catch (Exception ex)
            {
                exception = ex;
            }

            // Act
            var stackFrames = StackTraceHelper.GetFrames(exception);

            // Assert
            Assert.Collection(stackFrames,
                frame =>
                {
                    Assert.Contains("Thrower.cs", frame.FilePath);
                    Assert.Equal(17, frame.LineNumber);
                },
                frame =>
                {
                    Assert.Contains("StackTraceHelperTest.cs", frame.FilePath);
                });
        }

        [Fact]
        public void StackTraceHelper_PrettyPrintsStackTraceForGenericMethods()
        {
            // Arrange
            var exception = Record.Exception(() => GenericMethod<string>(null));

            // Act
            var stackFrames = StackTraceHelper.GetFrames(exception);

            // Assert
            var methods = stackFrames.Select(frame => frame.MethodDisplayInfo.ToString()).ToArray();
            Assert.Equal("Microsoft.Extensions.Internal.StackTraceHelperTest.GenericMethod<T>(T val)", methods[0]);
        }

        [Fact]
        public void StackTraceHelper_PrettyPrintsStackTraceForMethodsWithOutParameters()
        {
            // Arrange
            var exception = Record.Exception(() => MethodWithOutParameter(out var value));

            // Act
            var stackFrames = StackTraceHelper.GetFrames(exception);

            // Assert
            var methods = stackFrames.Select(frame => frame.MethodDisplayInfo.ToString()).ToArray();
            Assert.Equal("Microsoft.Extensions.Internal.StackTraceHelperTest.MethodWithOutParameter(out int value)", methods[0]);
        }

        [Fact]
        public void StackTraceHelper_PrettyPrintsStackTraceForMethodsWithGenericOutParameters()
        {
            // Arrange
            var exception = Record.Exception(() => MethodWithGenericOutParameter("Test", out int value));

            // Act
            var stackFrames = StackTraceHelper.GetFrames(exception);

            // Assert
            var methods = stackFrames.Select(frame => frame.MethodDisplayInfo.ToString()).ToArray();
            Assert.Equal("Microsoft.Extensions.Internal.StackTraceHelperTest.MethodWithGenericOutParameter<TVal>(string a, out TVal value)", methods[0]);
        }

        [Fact]
        public void StackTraceHelper_PrettyPrintsStackTraceForMethodsWithRefParameters()
        {
            // Arrange
            var value = 0;
            var exception = Record.Exception(() => MethodWithRefParameter(ref value));

            // Act
            var stackFrames = StackTraceHelper.GetFrames(exception);

            // Assert
            var methods = stackFrames.Select(frame => frame.MethodDisplayInfo.ToString()).ToArray();
            Assert.Equal("Microsoft.Extensions.Internal.StackTraceHelperTest.MethodWithRefParameter(ref int value)", methods[0]);
        }

        [Fact]
        public void StackTraceHelper_PrettyPrintsStackTraceForMethodsWithGenericRefParameters()
        {
            // Arrange
            var value = 0;
            var exception = Record.Exception(() => MethodWithGenericRefParameter(ref value));

            // Act
            var stackFrames = StackTraceHelper.GetFrames(exception);

            // Assert
            var methods = stackFrames.Select(frame => frame.MethodDisplayInfo.ToString()).ToArray();
            Assert.Equal("Microsoft.Extensions.Internal.StackTraceHelperTest.MethodWithGenericRefParameter<TVal>(ref TVal value)", methods[0]);
        }

        [Fact]
        public void StackTraceHelper_PrettyPrintsStackTraceForMethodsWithNullableParameters()
        {
            // Arrange
            var value = 0;
            var exception = Record.Exception(() => MethodWithNullableParameter(value));

            // Act
            var stackFrames = StackTraceHelper.GetFrames(exception);

            // Assert
            var methods = stackFrames.Select(frame => frame.MethodDisplayInfo.ToString()).ToArray();
            Assert.Equal("Microsoft.Extensions.Internal.StackTraceHelperTest.MethodWithNullableParameter(Nullable<int> value)", methods[0]);
        }

        [Fact]
        public void StackTraceHelper_PrettyPrintsStackTraceForMethodsOnGenericTypes()
        {
            // Arrange
            var exception = Record.Exception(() => new GenericClass<int>().Throw(0));

            // Act
            var stackFrames = StackTraceHelper.GetFrames(exception);

            // Assert
            var methods = stackFrames.Select(frame => frame.MethodDisplayInfo.ToString()).ToArray();
            Assert.Equal("Microsoft.Extensions.Internal.StackTraceHelperTest+GenericClass<T>.Throw(T parameter)", methods[0]);
        }

        [Fact]
        public void StackTraceHelper_ProducesReadableOutput()
        {
            // Arrange
            var expectedCallStack = new List<string>()
            {
                "Microsoft.Extensions.Internal.StackTraceHelperTest.Iterator()+MoveNext()",
                "string.Join(string separator, IEnumerable<string> values)",
                "Microsoft.Extensions.Internal.StackTraceHelperTest+GenericClass<T>.GenericMethod<V>(ref V value)",
                "Microsoft.Extensions.Internal.StackTraceHelperTest.MethodAsync(int value)",
                "Microsoft.Extensions.Internal.StackTraceHelperTest.MethodAsync<TValue>(TValue value)",
                "Microsoft.Extensions.Internal.StackTraceHelperTest.Method(string value)",
                "Microsoft.Extensions.Internal.StackTraceHelperTest.StackTraceHelper_ProducesReadableOutput()",
            };

            Exception exception = null;
            try
            {
                Method("test");
            }
            catch (Exception ex)
            {
                exception = ex;
            }

            // Act
            var stackFrames = StackTraceHelper.GetFrames(exception);
            var methodNames = stackFrames.Select(stackFrame => stackFrame.MethodDisplayInfo.ToString()).ToArray();

            // Assert
            Assert.Equal(expectedCallStack, methodNames);
        }

        [Fact]
        public void StackTraceHelper_DoesNotIncludeInstanceMethodsOnTypesWithStackTraceHiddenAttribute()
        {
            // Arrange
            var exception = Record.Exception(() => InvokeMethodOnTypeWithStackTraceHiddenAttribute());

            // Act
            var stackFrames = StackTraceHelper.GetFrames(exception);

            // Assert
            var methods = stackFrames.Select(frame => frame.MethodDisplayInfo.ToString()).ToArray();
            Assert.Equal("Microsoft.Extensions.Internal.StackTraceHelperTest.ThrowCore()", methods[0]);
            Assert.Equal("Microsoft.Extensions.Internal.StackTraceHelperTest.InvokeMethodOnTypeWithStackTraceHiddenAttribute()", methods[1]);
        }

        [Fact]
        public void StackTraceHelper_DoesNotIncludeStaticMethodsOnTypesWithStackTraceHiddenAttribute()
        {
            // Arrange
            var exception = Record.Exception(() => InvokeStaticMethodOnTypeWithStackTraceHiddenAttribute());

            // Act
            var stackFrames = StackTraceHelper.GetFrames(exception);

            // Assert
            var methods = stackFrames.Select(frame => frame.MethodDisplayInfo.ToString()).ToArray();
            Assert.Equal("Microsoft.Extensions.Internal.StackTraceHelperTest.ThrowCore()", methods[0]);
            Assert.Equal("Microsoft.Extensions.Internal.StackTraceHelperTest.InvokeStaticMethodOnTypeWithStackTraceHiddenAttribute()", methods[1]);
        }

        [Fact]
        public void StackTraceHelper_DoesNotIncludeMethodsWithStackTraceHiddenAttribute()
        {
            // Arrange
            var exception = Record.Exception(() => new TypeWithMethodWithStackTraceHiddenAttribute().Throw());

            // Act
            var stackFrames = StackTraceHelper.GetFrames(exception);

            // Assert
            var methods = stackFrames.Select(frame => frame.MethodDisplayInfo.ToString()).ToArray();
            Assert.Equal("Microsoft.Extensions.Internal.StackTraceHelperTest.ThrowCore()", methods[0]);
            Assert.Equal("Microsoft.Extensions.Internal.StackTraceHelperTest+TypeWithMethodWithStackTraceHiddenAttribute.Throw()", methods[1]);
        }

        [Fact]
        public void GetFrames_DoesNotFailForDynamicallyGeneratedAssemblies()
        {
            // Arrange
            var action = (Action)Expression.Lambda(
                Expression.Throw(
                    Expression.New(typeof(Exception)))).Compile();
            var exception = Record.Exception(action);

            // Act
            var frames = StackTraceHelper.GetFrames(exception).ToArray();

            // Assert
            var frame = frames[0];
            Assert.Null(frame.FilePath);
            Assert.Equal($"lambda_method(Closure )", frame.MethodDisplayInfo.ToString());
        }

        [MethodImpl(MethodImplOptions.NoOptimization | MethodImplOptions.NoInlining)]
        async Task<string> MethodAsync(int value)
        {
            await Task.Delay(0);
            return GenericClass<byte>.GenericMethod(ref value);
        }

        [MethodImpl(MethodImplOptions.NoOptimization | MethodImplOptions.NoInlining)]
        async Task<string> MethodAsync<TValue>(TValue value)
        {
            return await MethodAsync(1);
        }

        [MethodImpl(MethodImplOptions.NoOptimization | MethodImplOptions.NoInlining)]
        string Method(string value)
        {
            return MethodAsync(value).GetAwaiter().GetResult();
        }

        [MethodImpl(MethodImplOptions.NoOptimization | MethodImplOptions.NoInlining)]
        static IEnumerable<string> Iterator()
        {
            yield return "Success";
            throw new Exception();
        }

        [MethodImpl(MethodImplOptions.NoOptimization | MethodImplOptions.NoInlining)]
        void MethodWithOutParameter(out int value) => throw new Exception();

        [MethodImpl(MethodImplOptions.NoOptimization | MethodImplOptions.NoInlining)]
        void MethodWithGenericOutParameter<TVal>(string a, out TVal value) => throw new Exception();

        [MethodImpl(MethodImplOptions.NoOptimization | MethodImplOptions.NoInlining)]
        void MethodWithRefParameter(ref int value) => throw new Exception();

        [MethodImpl(MethodImplOptions.NoOptimization | MethodImplOptions.NoInlining)]
        void MethodWithGenericRefParameter<TVal>(ref TVal value) => throw new Exception();

        [MethodImpl(MethodImplOptions.NoOptimization | MethodImplOptions.NoInlining)]
        void MethodWithNullableParameter(int? value) => throw new Exception();

        [MethodImpl(MethodImplOptions.NoOptimization | MethodImplOptions.NoInlining)]
        void InvokeMethodOnTypeWithStackTraceHiddenAttribute() => new TypeWithStackTraceHiddenAttribute().Throw();

        [MethodImpl(MethodImplOptions.NoOptimization | MethodImplOptions.NoInlining)]
        void InvokeStaticMethodOnTypeWithStackTraceHiddenAttribute() => TypeWithStackTraceHiddenAttribute.ThrowStatic();

        class GenericClass<T>
        {
            [MethodImpl(MethodImplOptions.NoOptimization | MethodImplOptions.NoInlining)]
            public static string GenericMethod<V>(ref V value)
            {
                var returnVal = "";
                for (var i = 0; i < 10; i++)
                {
                    returnVal += string.Join(", ", Iterator());
                }
                return returnVal;
            }

            [MethodImpl(MethodImplOptions.NoOptimization | MethodImplOptions.NoInlining)]
            public void Throw(T parameter) => throw new Exception();
        }

        [MethodImpl(MethodImplOptions.NoOptimization | MethodImplOptions.NoInlining)]
        private void GenericMethod<T>(T val) where T : class => throw new Exception();

        private class StackTraceHiddenAttribute : Attribute
        {
        }

        [StackTraceHidden]
        private class TypeWithStackTraceHiddenAttribute
        {
            [MethodImpl(MethodImplOptions.NoOptimization | MethodImplOptions.NoInlining)]
            public void Throw() => ThrowCore();

            [MethodImpl(MethodImplOptions.NoOptimization | MethodImplOptions.NoInlining)]
            public static void ThrowStatic() => ThrowCore();
        }

        private class TypeWithMethodWithStackTraceHiddenAttribute
        {
            [MethodImpl(MethodImplOptions.NoOptimization | MethodImplOptions.NoInlining)]
            [StackTraceHidden]
            public void MethodWithStackTraceHiddenAttribute()
            {
                ThrowCore();
            }

            [MethodImpl(MethodImplOptions.NoOptimization | MethodImplOptions.NoInlining)]
            public void Throw() => MethodWithStackTraceHiddenAttribute();
        }

        [MethodImpl(MethodImplOptions.NoOptimization | MethodImplOptions.NoInlining)]
        private static void ThrowCore() => throw new Exception();
    }
}
