// 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
// Uncomment to enable the IOperation test hook on all test runs. Do not commit this uncommented.
//#define ROSLYN_TEST_IOPERATION

// Uncomment to enable the Used Assemblies test hook on all test runs. Do not commit this uncommented.
//#define ROSLYN_TEST_USEDASSEMBLIES

using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading;
using Microsoft.CodeAnalysis.CodeGen;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.Emit;
using Microsoft.CodeAnalysis.FlowAnalysis;
using Microsoft.CodeAnalysis.Operations;
using Microsoft.CodeAnalysis.PooledObjects;
using Microsoft.CodeAnalysis.Text;
using Roslyn.Test.Utilities;
using Roslyn.Utilities;
using Xunit;
using SeparatedWithManyChildren = Microsoft.CodeAnalysis.Syntax.SyntaxList.SeparatedWithManyChildren;

namespace Microsoft.CodeAnalysis.Test.Utilities
{
    public static class CompilationExtensions
    {
        internal static bool EnableVerifyIOperation { get; } =
#if ROSLYN_TEST_IOPERATION
                                    true;
#else
                        !string.IsNullOrEmpty(Environment.GetEnvironmentVariable("ROSLYN_TEST_IOPERATION"));
#endif

        internal static bool EnableVerifyUsedAssemblies { get; } =
#if ROSLYN_TEST_USEDASSEMBLIES
            true;
#else
            !string.IsNullOrEmpty(Environment.GetEnvironmentVariable("ROSLYN_TEST_USEDASSEMBLIES"));
#endif

        internal static ImmutableArray<byte> EmitToArray(
            this Compilation compilation,
            EmitOptions options = null,
            CompilationTestData testData = null,
            DiagnosticDescription[] expectedWarnings = null,
            Stream pdbStream = null,
            IMethodSymbol debugEntryPoint = null,
            Stream sourceLinkStream = null,
            IEnumerable<EmbeddedText> embeddedTexts = null,
            IEnumerable<ResourceDescription> manifestResources = null,
            Stream metadataPEStream = null)
        {
            var peStream = new MemoryStream();

            if (pdbStream == null && compilation.Options.OptimizationLevel == OptimizationLevel.Debug && options?.DebugInformationFormat != DebugInformationFormat.Embedded)
            {
                if (MonoHelpers.IsRunningOnMono() || PathUtilities.IsUnixLikePlatform)
                {
                    options = (options ?? EmitOptions.Default).WithDebugInformationFormat(DebugInformationFormat.PortablePdb);
                }

                var discretePdb = (object)options != null && options.DebugInformationFormat != DebugInformationFormat.Embedded;
                pdbStream = discretePdb ? new MemoryStream() : null;
            }

            var emitResult = compilation.Emit(
                peStream: peStream,
                metadataPEStream: metadataPEStream,
                pdbStream: pdbStream,
                xmlDocumentationStream: null,
                win32Resources: null,
                manifestResources: manifestResources,
                options: options,
                debugEntryPoint: debugEntryPoint,
                sourceLinkStream: sourceLinkStream,
                embeddedTexts: embeddedTexts,
                rebuildData: null,
                testData: testData,
                cancellationToken: default(CancellationToken));

            Assert.True(emitResult.Success, "Diagnostics:\r\n" + string.Join("\r\n", emitResult.Diagnostics.Select(d => d.ToString())));

            if (expectedWarnings != null)
            {
                emitResult.Diagnostics.Verify(expectedWarnings);
            }

            return peStream.ToImmutable();
        }

        public static MemoryStream EmitToStream(this Compilation compilation, EmitOptions options = null, DiagnosticDescription[] expectedWarnings = null)
        {
            var stream = new MemoryStream();
            var emitResult = compilation.Emit(stream, options: options);
            Assert.True(emitResult.Success, "Diagnostics: " + string.Join(", ", emitResult.Diagnostics.Select(d => d.ToString())));

            if (expectedWarnings != null)
            {
                emitResult.Diagnostics.Verify(expectedWarnings);
            }

            stream.Position = 0;
            return stream;
        }

        public static MetadataReference EmitToImageReference(
            this Compilation comp,
            EmitOptions options = null,
            bool embedInteropTypes = false,
            ImmutableArray<string> aliases = default,
            DiagnosticDescription[] expectedWarnings = null,
            DocumentationProvider documentation = null) => EmitToPortableExecutableReference(comp, options, embedInteropTypes, aliases, expectedWarnings, documentation);

        public static PortableExecutableReference EmitToPortableExecutableReference(
            this Compilation comp,
            EmitOptions options = null,
            bool embedInteropTypes = false,
            ImmutableArray<string> aliases = default,
            DiagnosticDescription[] expectedWarnings = null,
            DocumentationProvider documentation = null)
        {
            var image = comp.EmitToArray(options, expectedWarnings: expectedWarnings);
            if (comp.Options.OutputKind == OutputKind.NetModule)
            {
                return ModuleMetadata.CreateFromImage(image).GetReference(documentation, display: comp.MakeSourceModuleName());
            }
            else
            {
                return AssemblyMetadata.CreateFromImage(image).GetReference(documentation, aliases: aliases, embedInteropTypes: embedInteropTypes, display: comp.MakeSourceAssemblySimpleName());
            }
        }

        internal static CompilationDifference EmitDifference(
            this Compilation compilation,
            EmitBaseline baseline,
            ImmutableArray<SemanticEdit> edits,
            IEnumerable<ISymbol> allAddedSymbols = null,
            EmitDifferenceOptions? options = null,
            CompilationTestData testData = null)
        {
            testData ??= new CompilationTestData();
            var isAddedSymbol = new Func<ISymbol, bool>(s => allAddedSymbols?.Contains(s) ?? false);

            using var mdStream = new MemoryStream();
            using var ilStream = new MemoryStream();
            using var pdbStream = new MemoryStream();

            var result = compilation.EmitDifference(
                baseline,
                edits,
                isAddedSymbol,
                mdStream,
                ilStream,
                pdbStream,
                options ?? EmitDifferenceOptions.Default,
                testData,
                CancellationToken.None);

            return new CompilationDifference(
                mdStream.ToImmutable(),
                ilStream.ToImmutable(),
                pdbStream.ToImmutable(),
                testData,
                result);
        }

        internal static void VerifyAssemblyVersionsAndAliases(this Compilation compilation, params string[] expectedAssembliesAndAliases)
        {
            var actual = compilation.GetBoundReferenceManager().GetReferencedAssemblyAliases().
               Select(t => $"{t.Item1.Identity.Name}, Version={t.Item1.Identity.Version}{(t.Item2.IsEmpty ? "" : ": " + string.Join(",", t.Item2))}");

            AssertEx.Equal(expectedAssembliesAndAliases, actual, itemInspector: s => '"' + s + '"');
        }

        internal static void VerifyAssemblyAliases(this Compilation compilation, params string[] expectedAssembliesAndAliases)
        {
            var actual = compilation.GetBoundReferenceManager().GetReferencedAssemblyAliases().
               Select(t => $"{t.Item1.Identity.Name}{(t.Item2.IsEmpty ? "" : ": " + string.Join(",", t.Item2))}");

            AssertEx.Equal(expectedAssembliesAndAliases, actual, itemInspector: s => '"' + s + '"');
        }

        internal static void VerifyOperationTree(this Compilation compilation, SyntaxNode node, string expectedOperationTree)
        {
            SemanticModel model = compilation.GetSemanticModel(node.SyntaxTree);
            model.VerifyOperationTree(node, expectedOperationTree);
        }

        internal static void VerifyOperationTree(this Compilation compilation, string expectedOperationTree, bool skipImplicitlyDeclaredSymbols = false)
        {
            VerifyOperationTree(compilation, symbolToVerify: null, expectedOperationTree: expectedOperationTree, skipImplicitlyDeclaredSymbols: skipImplicitlyDeclaredSymbols);
        }

        internal static void VerifyOperationTree(this Compilation compilation, string symbolToVerify, string expectedOperationTree, bool skipImplicitlyDeclaredSymbols = false)
        {
            SyntaxTree tree = compilation.SyntaxTrees.First();
            SyntaxNode root = tree.GetRoot();
            SemanticModel model = compilation.GetSemanticModel(tree);
            var declarationsBuilder = ArrayBuilder<DeclarationInfo>.GetInstance();
            model.ComputeDeclarationsInNode(root, associatedSymbol: null, getSymbol: true, builder: declarationsBuilder, cancellationToken: CancellationToken.None);

            var actualTextBuilder = new StringBuilder();
            foreach (DeclarationInfo declaration in declarationsBuilder.ToArrayAndFree().Where(d => d.DeclaredSymbol != null).OrderBy(d => d.DeclaredSymbol.ToTestDisplayString()))
            {
                if (!CanHaveExecutableCodeBlock(declaration.DeclaredSymbol))
                {
                    continue;
                }

                if (skipImplicitlyDeclaredSymbols && declaration.DeclaredSymbol.IsImplicitlyDeclared)
                {
                    continue;
                }

                if (!string.IsNullOrEmpty(symbolToVerify) && !declaration.DeclaredSymbol.Name.Equals(symbolToVerify, StringComparison.Ordinal))
                {
                    continue;
                }

                actualTextBuilder.Append(declaration.DeclaredSymbol.ToTestDisplayString());

                if (declaration.ExecutableCodeBlocks.Length == 0)
                {
                    actualTextBuilder.Append($" ('0' executable code blocks)");
                }
                else
                {
                    // Workaround for https://github.com/dotnet/roslyn/issues/11903 - skip the IOperation for EndBlockStatement.
                    ImmutableArray<SyntaxNode> executableCodeBlocks = declaration.ExecutableCodeBlocks;
                    if (declaration.DeclaredSymbol.Kind == SymbolKind.Method && compilation.Language == LanguageNames.VisualBasic)
                    {
                        executableCodeBlocks = executableCodeBlocks.RemoveAt(executableCodeBlocks.Length - 1);
                    }

                    foreach (SyntaxNode executableCodeBlock in executableCodeBlocks)
                    {
                        actualTextBuilder.Append(Environment.NewLine);
                        model.AppendOperationTree(executableCodeBlock, actualTextBuilder, initialIndent: 2);
                    }
                }

                actualTextBuilder.Append(Environment.NewLine);
            }

            OperationTreeVerifier.Verify(expectedOperationTree, actualTextBuilder.ToString());
        }

        internal static bool CanHaveExecutableCodeBlock(ISymbol symbol)
        {
            switch (symbol.Kind)
            {
                case SymbolKind.Field:
                case SymbolKind.Event:
                case SymbolKind.Method:
                case SymbolKind.NamedType:
                case SymbolKind.Property:
                    return true;

                default:
                    return false;
            }
        }

        public static void ValidateIOperations(Func<Compilation> createCompilation)
        {
            if (!EnableVerifyIOperation)
            {
                return;
            }

            var compilation = createCompilation();
            var roots = ArrayBuilder<(IOperation operation, ISymbol associatedSymbol)>.GetInstance();
            var stopWatch = new Stopwatch();
            start(stopWatch);

            void checkTimeout()
            {
                const int timeout = 15000;
                Assert.False(stopWatch.ElapsedMilliseconds > timeout, $"ValidateIOperations took too long: {stopWatch.ElapsedMilliseconds} ms");
            }

            foreach (var tree in compilation.SyntaxTrees)
            {
                var semanticModel = compilation.GetSemanticModel(tree);
                var root = tree.GetRoot();

                foreach (var node in root.DescendantNodesAndSelf())
                {
                    checkTimeout();

                    var operation = semanticModel.GetOperation(node);
                    if (operation != null)
                    {
                        // Make sure IOperation returned by GetOperation(syntaxnode) will have same syntaxnode as the given syntaxnode(IOperation.Syntax == syntaxnode).
                        Assert.True(node == operation.Syntax, $"Expected : {node} - Actual : {operation.Syntax}");

                        Assert.True(operation.Type == null || !operation.MustHaveNullType(), $"Unexpected non-null type: {operation.Type}");

                        Assert.Same(semanticModel, operation.SemanticModel);
                        Assert.NotSame(semanticModel, ((Operation)operation).OwningSemanticModel);
                        Assert.NotNull(((Operation)operation).OwningSemanticModel);
                        Assert.Same(semanticModel, ((Operation)operation).OwningSemanticModel.ContainingPublicModelOrSelf);
                        Assert.Same(semanticModel, semanticModel.ContainingPublicModelOrSelf);

                        if (operation.Parent == null)
                        {
                            roots.Add((operation, semanticModel.GetDeclaredSymbol(operation.Syntax)));
                        }
                    }
                }

                tree.VerifyChildNodePositions();
            }

            var explicitNodeMap = new Dictionary<SyntaxNode, IOperation>();
            var visitor = TestOperationVisitor.Singleton;

            foreach (var (root, associatedSymbol) in roots)
            {
                foreach (var operation in root.DescendantsAndSelf())
                {
                    checkTimeout();

                    if (!operation.IsImplicit)
                    {
                        try
                        {
                            explicitNodeMap.Add(operation.Syntax, operation);
                        }
                        catch (ArgumentException)
                        {
                            Assert.Fail($"Duplicate explicit node for syntax ({operation.Syntax.RawKind}): {operation.Syntax.ToString()}");
                        }
                    }

                    visitor.Visit(operation);
                }

                stopWatch.Stop();
                checkControlFlowGraph(root, associatedSymbol);
                start(stopWatch);
            }

            roots.Free();
            stopWatch.Stop();
            return;

            static void start(Stopwatch stopWatch)
            {
                if (!System.Diagnostics.Debugger.IsAttached)
                {
                    stopWatch.Start();
                }
            }

            void checkControlFlowGraph(IOperation root, ISymbol associatedSymbol)
            {
                switch (root)
                {
                    case IBlockOperation blockOperation:
                        // https://github.com/dotnet/roslyn/issues/27593 tracks adding ControlFlowGraph support in script code.
                        if (blockOperation.Syntax.SyntaxTree.Options.Kind != SourceCodeKind.Script)
                        {
                            ControlFlowGraphVerifier.GetFlowGraph(compilation, ControlFlowGraphBuilder.Create(blockOperation), associatedSymbol);
                        }

                        break;

                    case IMethodBodyOperation methodBody:
                    case IConstructorBodyOperation constructorBody:
                    case IFieldInitializerOperation fieldInitializerOperation:
                    case IPropertyInitializerOperation propertyInitializerOperation:
                        ControlFlowGraphVerifier.GetFlowGraph(compilation, ControlFlowGraphBuilder.Create(root), associatedSymbol);
                        break;

                    case IParameterInitializerOperation parameterInitializerOperation:
                        // https://github.com/dotnet/roslyn/issues/27594 tracks adding support for getting ControlFlowGraph for parameter initializers for local and anonymous functions.
                        if ((parameterInitializerOperation.Parameter.ContainingSymbol as IMethodSymbol)?.MethodKind is not (MethodKind.LocalFunction or MethodKind.AnonymousFunction))
                        {
                            ControlFlowGraphVerifier.GetFlowGraph(compilation, ControlFlowGraphBuilder.Create(root), associatedSymbol);
                        }
                        break;
                }
            }
        }

        internal static void VerifyChildNodePositions(this SyntaxTree tree)
        {
            var nodes = tree.GetRoot().DescendantNodesAndSelf();
            foreach (var node in nodes)
            {
                var childNodesAndTokens = node.ChildNodesAndTokens();
                if (childNodesAndTokens.Node is { } container)
                {
                    for (int i = 0; i < childNodesAndTokens.Count; i++)
                    {
                        if (container.GetNodeSlot(i) is SeparatedWithManyChildren separatedList)
                        {
                            verifyPositions(separatedList);
                        }
                    }
                }
            }

            static void verifyPositions(SeparatedWithManyChildren separatedList)
            {
                var green = (Microsoft.CodeAnalysis.Syntax.InternalSyntax.SyntaxList)separatedList.Green;

                // Calculate positions from start, using existing cache.
                int[] positions = getPositionsFromStart(separatedList);

                // Calculate positions from end, using existing cache.
                AssertEx.Equal(positions, getPositionsFromEnd(separatedList));

                // Avoid testing without caches if the number of children is large.
                if (separatedList.SlotCount > 100)
                {
                    return;
                }

                // Calculate positions from start, with empty cache.
                AssertEx.Equal(positions, getPositionsFromStart(new SeparatedWithManyChildren(green, null, separatedList.Position)));

                // Calculate positions from end, with empty cache.
                AssertEx.Equal(positions, getPositionsFromEnd(new SeparatedWithManyChildren(green, null, separatedList.Position)));
            }

            // Calculate positions from start, using any existing cache of red nodes on separated list.
            static int[] getPositionsFromStart(SeparatedWithManyChildren separatedList)
            {
                int n = separatedList.SlotCount;
                var positions = new int[n];
                for (int i = 0; i < n; i++)
                {
                    positions[i] = separatedList.GetChildPosition(i);
                }
                return positions;
            }

            // Calculate positions from end, using any existing cache of red nodes on separated list.
            static int[] getPositionsFromEnd(SeparatedWithManyChildren separatedList)
            {
                int n = separatedList.SlotCount;
                var positions = new int[n];
                for (int i = n - 1; i >= 0; i--)
                {
                    positions[i] = separatedList.GetChildPositionFromEnd(i);
                }
                return positions;
            }
        }

        /// <summary>
        /// The reference assembly System.Runtime.InteropServices.WindowsRuntime was removed in net5.0. This builds
        /// up <see cref="CompilationReference"/> which contains all of the well known types that were used from that
        /// reference by the compiler.
        /// </summary>
        public static PortableExecutableReference CreateWindowsRuntimeMetadataReference(TargetFramework targetFramework = TargetFramework.NetCoreApp)
        {
            var source = @"
namespace System.Runtime.InteropServices.WindowsRuntime
{
    public struct EventRegistrationToken { }

    public sealed class EventRegistrationTokenTable<T> where T : class
    {
        public T InvocationList { get; set; }

        public static EventRegistrationTokenTable<T> GetOrCreateEventRegistrationTokenTable(ref EventRegistrationTokenTable<T> refEventTable)
        {
            throw null;
        }

        public void RemoveEventHandler(EventRegistrationToken token)
        {
        }

        public void RemoveEventHandler(T handler)
        {
        }
    }

    public static class WindowsRuntimeMarshal
    {
        public static void AddEventHandler<T>(Func<T, EventRegistrationToken> addMethod, Action<EventRegistrationToken> removeMethod, T handler)
        {
        }

        public static void RemoveAllEventHandlers(Action<EventRegistrationToken> removeMethod)
        {
        }

        public static void RemoveEventHandler<T>(Action<EventRegistrationToken> removeMethod, T handler)
        {
        }
    }
}
";

            // The actual System.Runtime.InteropServices.WindowsRuntime DLL has a public key of
            // b03f5f7f11d50a3a and version 4.0.4.0. The compiler just looks at these via 
            // WellKnownTypes and WellKnownMembers so it can be safely skipped here. 
            var compilation = CSharpCompilation.Create(
                "System.Runtime.InteropServices.WindowsRuntime",
                new[] { CSharpSyntaxTree.ParseText(SourceText.From(source, encoding: null, SourceHashAlgorithms.Default)) },
                references: TargetFrameworkUtil.GetReferences(targetFramework),
                options: new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary));
            compilation.VerifyEmitDiagnostics();
            return compilation.EmitToPortableExecutableReference();
        }
    }
}
