// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.SourceGeneration.Helpers;
using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.SourceGeneration.Models;

namespace Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.SourceGeneration.Emitters;

/// <summary>
/// Produces the C# source that wires the test assembly into MSTest's source-generator runtime.
/// </summary>
internal static class ReflectionMetadataEmitter
{
    public const string GeneratedTypeName = "MSTestSourceGeneratedReflectionMetadata";

    public static string Emit(TestAssemblyMetadata metadata)
    {
        var sb = new StringBuilder();

        Append(sb, "// <auto-generated />");
        Append(sb, "// This file was generated by the MSTest source generator.");
        Append(sb, "// Do not edit it manually; changes will be lost on the next build.");
        Append(sb, "#nullable enable");
        Append(sb, string.Empty);
        Append(sb, "using System;");
        Append(sb, "using System.Collections.Generic;");
        Append(sb, "using System.Diagnostics.CodeAnalysis;");
        Append(sb, "using System.Reflection;");
        Append(sb, "using System.Runtime.CompilerServices;");
        Append(sb, string.Empty);
        Append(sb, "namespace Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.SourceGeneration.Generated");
        Append(sb, "{");
        Append(sb, "    /// <summary>Source-generated MSTest reflection metadata hook for this test assembly.</summary>");
        Append(sb, $"    internal static class {GeneratedTypeName}");
        Append(sb, "    {");
        Append(sb, "        [ModuleInitializer]");
        var emittedBaseTypeDependencies = new HashSet<string>(StringComparer.Ordinal);
        foreach (TestClassMetadata cls in metadata.Classes)
        {
            // Preserve the test class members at runtime even when the assembly is published with
            // Native AOT / IL trimming. MSTest's adapter still needs to call GetConstructors,
            // GetProperties and other reflection APIs on the class (the source generator only
            // populates TypeMethods; constructors, properties, etc. fall back to runtime reflection).
            // Without this hint the trimmer removes those members and discovery fails with
            // "Cannot find a valid constructor for test class".
            Append(sb, $"        [DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof({cls.FullyQualifiedName}))]");
        }

        // Emit a [DynamicDependency] for every accessible non-generic base type of every
        // discovered [TestClass]. This roots members declared on the abstract base — most
        // importantly [ClassInitialize] / [ClassCleanup] / [AssemblyInitialize] / [AssemblyCleanup]
        // and any [TestContext] property — so the trimmer / Native AOT does not remove them.
        // Dedupe across classes so a base shared by many derived classes is emitted once.
        foreach (TestClassMetadata cls in metadata.Classes)
        {
            foreach (string baseTypeName in cls.BaseTypeFullyQualifiedNames)
            {
                if (emittedBaseTypeDependencies.Add(baseTypeName))
                {
                    Append(sb, $"        [DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof({baseTypeName}))]");
                }
            }
        }

        Append(sb, "        internal static void Initialize()");
        Append(sb, "        {");
        Append(sb, $"            var assembly = typeof({GeneratedTypeName}).Assembly;");
        Append(sb, $"            var types = {EmitTypesExpression(metadata)};");
        EmitTestMethodsLocal(sb, metadata);
        Append(sb, $"            {Constants.ReflectionMetadataHookFullName}.Register(assembly, types, testMethods);");
        Append(sb, "        }");
        Append(sb, string.Empty);
        Append(sb, "        private static MethodInfo ResolveMethod([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods | DynamicallyAccessedMemberTypes.NonPublicMethods)] Type type, string name, Type[] parameterTypes)");
        Append(sb, "        {");
        // Include inherited methods (no DeclaredOnly) so test methods defined on an abstract base
        // class are resolvable through the concrete test class's typeof().
        Append(sb, "            const BindingFlags flags = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static;");
        Append(sb, "            foreach (MethodInfo candidate in type.GetMethods(flags))");
        Append(sb, "            {");
        Append(sb, "                if (candidate.Name != name)");
        Append(sb, "                {");
        Append(sb, "                    continue;");
        Append(sb, "                }");
        Append(sb, string.Empty);
        Append(sb, "                ParameterInfo[] candidateParameters = candidate.GetParameters();");
        Append(sb, "                if (candidateParameters.Length != parameterTypes.Length)");
        Append(sb, "                {");
        Append(sb, "                    continue;");
        Append(sb, "                }");
        Append(sb, string.Empty);
        Append(sb, "                bool match = true;");
        Append(sb, "                for (int i = 0; i < candidateParameters.Length; i++)");
        Append(sb, "                {");
        Append(sb, "                    if (candidateParameters[i].ParameterType != parameterTypes[i])");
        Append(sb, "                    {");
        Append(sb, "                        match = false;");
        Append(sb, "                        break;");
        Append(sb, "                    }");
        Append(sb, "                }");
        Append(sb, string.Empty);
        Append(sb, "                if (match)");
        Append(sb, "                {");
        Append(sb, "                    return candidate;");
        Append(sb, "                }");
        Append(sb, "            }");
        Append(sb, string.Empty);
        Append(sb, "            throw new MissingMethodException(type.FullName, name);");
        Append(sb, "        }");
        Append(sb, "    }");
        Append(sb, "}");

        return sb.ToString();
    }

    private static string EmitTypesExpression(TestAssemblyMetadata metadata)
    {
        if (metadata.Classes.Count == 0)
        {
            return "Array.Empty<Type>()";
        }

        var sb = new StringBuilder();
        sb.Append("new Type[]").Append(Constants.NewLine);
        sb.Append("            {").Append(Constants.NewLine);
        foreach (TestClassMetadata cls in metadata.Classes)
        {
            sb.Append($"                typeof({cls.FullyQualifiedName}),").Append(Constants.NewLine);
        }

        sb.Append("            }");
        return sb.ToString();
    }

    private static void EmitTestMethodsLocal(StringBuilder sb, TestAssemblyMetadata metadata)
    {
        if (metadata.Classes.Count == 0)
        {
            Append(sb, "            var testMethods = new Dictionary<Type, MethodInfo[]>();");
            return;
        }

        Append(sb, "            var testMethods = new Dictionary<Type, MethodInfo[]>");
        Append(sb, "            {");
        foreach (TestClassMetadata cls in metadata.Classes)
        {
            Append(sb, $"                [typeof({cls.FullyQualifiedName})] = new MethodInfo[]");
            Append(sb, "                {");
            foreach (TestMethodMetadata method in cls.Methods)
            {
                string parameterTypesArray;
                if (method.ParameterTypes.Count == 0)
                {
                    parameterTypesArray = "Type.EmptyTypes";
                }
                else
                {
                    var parts = new List<string>(method.ParameterTypes.Count);
                    foreach (string parameterType in method.ParameterTypes)
                    {
                        parts.Add($"typeof({parameterType})");
                    }

                    parameterTypesArray = "new Type[] { " + string.Join(", ", parts) + " }";
                }

                Append(sb, $"                    ResolveMethod(typeof({cls.FullyQualifiedName}), {ToCSharpLiteral(method.Name)}, {parameterTypesArray}),");
            }

            Append(sb, "                },");
        }

        Append(sb, "            };");
    }

    private static void Append(StringBuilder sb, string line)
    {
        sb.Append(line);
        sb.Append(Constants.NewLine);
    }

    private static string ToCSharpLiteral(string value)
        => "\"" + value.Replace("\\", "\\\\").Replace("\"", "\\\"") + "\"";
}
