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

namespace Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Helpers;

internal static class ManagedNameHelper
{
    public static void GetManagedNameAndHierarchy(MethodBase method, out string managedTypeName, out string managedMethodName, out string?[] hierarchyValues)
    {
        _ = method ?? throw new ArgumentNullException(nameof(method));

        if (method.MemberType != MemberTypes.Method)
        {
            throw ApplicationStateGuard.Unreachable();
        }

        Type semanticType = method.ReflectedType ?? throw ApplicationStateGuard.Unreachable();

        if (semanticType.IsGenericType)
        {
            // The type might have some of its generic parameters specified, so make
            // sure we are working with the open form of the generic type.
            semanticType = semanticType.GetGenericTypeDefinition();

            // The method might have some of its parameters specified by the original closed type
            // declaration. Here we use the method handle (basically metadata token) to create
            // a new method reference using the open form of the reflected type. The intent is
            // to strip all generic type parameters.
            RuntimeMethodHandle methodHandle = method.MethodHandle;
            method = MethodBase.GetMethodFromHandle(methodHandle, semanticType.TypeHandle)!;
        }

        managedTypeName = semanticType.FullName!;

        if (method.IsGenericMethod)
        {
            // If this method is generic, then convert to the generic method definition
            // so that we get the open generic type definitions for parameters.
            method = ((MethodInfo)method).GetGenericMethodDefinition();
        }

        var methodBuilder = new StringBuilder();

        // Method Name with method arity
        int arity = method.GetGenericArguments().Length;
        AppendMethodString(methodBuilder, method.Name, arity);
        if (arity > 0)
        {
            methodBuilder.Append('`');
            methodBuilder.Append(arity);
        }

        // Type Parameters
        ParameterInfo[] paramList = method.GetParameters();
        if (paramList.Length != 0)
        {
            methodBuilder.Append('(');
            foreach (ParameterInfo p in paramList)
            {
                // closedType is always true here by RFC
                AppendTypeString(methodBuilder, p.ParameterType, closedType: true);
                methodBuilder.Append(',');
            }

            // Replace the last ',' with ')'
            methodBuilder[methodBuilder.Length - 1] = ')';
        }

        managedMethodName = methodBuilder.ToString();

        hierarchyValues = new string[HierarchyConstants.Levels.TotalLevelCount];
        hierarchyValues[HierarchyConstants.Levels.TestGroupIndex] = managedMethodName;

        if (semanticType.Namespace is { } @semanticTypeNamespace)
        {
            int lastIndexOfDot = semanticType.FullName!.LastIndexOf('.');
            hierarchyValues[HierarchyConstants.Levels.ClassIndex] = semanticType.FullName!.Substring(lastIndexOfDot + 1);
            hierarchyValues[HierarchyConstants.Levels.NamespaceIndex] = semanticTypeNamespace;
        }
        else
        {
            hierarchyValues[HierarchyConstants.Levels.ClassIndex] = semanticType.FullName;
            hierarchyValues[HierarchyConstants.Levels.NamespaceIndex] = null;
        }

        hierarchyValues[HierarchyConstants.Levels.ContainerIndex] = null; // This one will be set by test windows to current test project name.
    }

    /// <summary>
    /// Gets the <see cref="MethodBase"/> object with the specified <paramref name="managedTypeName"/>
    /// and <paramref name="managedMethodName"/> in the <paramref name="assembly"/> instance.
    /// </summary>
    /// <param name="assembly">
    /// An <see cref="Assembly" /> instance to search in.
    /// </param>
    /// <param name="managedTypeName">
    /// The fully qualified managed name of the type.
    /// The format is defined in <see href="https://github.com/microsoft/vstest/blob/main/docs/RFCs/0017-Managed-TestCase-Properties.md#managedtype-property">the RFC</see>.
    /// </param>
    /// <param name="managedMethodName">
    /// The fully qualified managed name of the method.
    /// The format is defined in <see href="https://github.com/microsoft/vstest/blob/main/docs/RFCs/0017-Managed-TestCase-Properties.md#managedmethod-property">the RFC</see>.
    /// </param>
    /// <returns>
    /// A <see cref="MethodBase" /> object that represents specified parameters, throws if null.
    /// </returns>
    /// <exception cref="InvalidManagedNameException">
    /// Values specified with <paramref name="managedTypeName"/> and <paramref name="managedMethodName"/>
    /// does not correspond to a method in the <paramref name="assembly"/> instance, or malformed.
    /// </exception>
    /// <remarks>
    /// More information about <paramref name="managedTypeName"/> and <paramref name="managedMethodName"/> can be found in
    /// <see href="https://github.com/microsoft/vstest/blob/main/docs/RFCs/0017-Managed-TestCase-Properties.md">the RFC</see>.
    /// </remarks>
    [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2026:Members attributed with RequiresUnreferencedCode may break when trimming", Justification = "Reflection-based name resolution is used by the legacy VSTest bridge for managed-name lookup. Native AOT support relies on MSTest source-generated reflection metadata, not on this code path.")]
    public static MethodInfo GetMethod(Assembly assembly, string managedTypeName, string managedMethodName)
    {
        Type type = assembly.GetType(managedTypeName, throwOnError: false, ignoreCase: false)
            ?? throw new InvalidManagedNameException();

        MethodInfo? method = null;
        ManagedNameParser.ParseManagedMethodName(managedMethodName, out string methodName, out int methodArity, out string[]? parameterTypes);

        if (!string.IsNullOrWhiteSpace(methodName))
        {
            method = FindMethod(type, methodName, methodArity, parameterTypes);
        }

        return method ?? throw new InvalidManagedNameException();
    }

    [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2070:'this' argument does not satisfy 'DynamicallyAccessedMembersAttribute' in call to target method.", Justification = "Reflection-based name resolution is used by the legacy VSTest bridge for managed-name lookup. Native AOT support relies on MSTest source-generated reflection metadata, not on this code path.")]
    private static MethodInfo? FindMethod(Type type, string methodName, int methodArity, string[]? parameterTypes)
    {
        bool Filter(MemberInfo mbr, object? param)
        {
            if (mbr is not MethodInfo method || method.Name != methodName || method.GetGenericArguments().Length != methodArity)
            {
                return false;
            }

            ParameterInfo[] paramList = method.GetParameters();
            if (paramList.Length == 0 && parameterTypes == null)
            {
                return true;
            }
            else if (parameterTypes == null || paramList.Length != parameterTypes.Length)
            {
                return false;
            }

            for (int i = 0; i < paramList.Length; i++)
            {
                string parameterType = GetTypeString(paramList[i].ParameterType, closedType: true);
                if (parameterType != parameterTypes[i])
                {
                    return false;
                }
            }

            return true;
        }

        BindingFlags bindingFlags = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static;
        MemberInfo[] methods = type.FindMembers(MemberTypes.Method, bindingFlags, Filter, null);

        return (MethodInfo?)(methods.Length switch
        {
            1 => methods[0],
            > 1 => methods.SingleOrDefault(i => i.DeclaringType == type),
            _ => null,
        });
    }

    private static void AppendTypeString(StringBuilder b, Type? type, bool closedType)
    {
        if (type is null)
        {
            return;
        }

        if (type.IsArray)
        {
            AppendTypeString(b, type.GetElementType(), closedType);
            b.Append('[');
            for (int i = 0; i < type.GetArrayRank() - 1; i++)
            {
                b.Append(',');
            }

            b.Append(']');
        }
        else if (type.IsGenericParameter)
        {
            if (type.DeclaringMethod != null)
            {
                b.Append('!');
            }

            b.Append('!');
            b.Append(type.GenericParameterPosition);
        }
        else
        {
            if (type.Namespace != null)
            {
                AppendNamespace(b, type.Namespace);
                b.Append('.');
            }

            AppendNestedTypeName(b, type);
            if (closedType)
            {
                AppendGenericTypeParameters(b, type);
            }
        }
    }

    private static void AppendNamespace(StringBuilder b, string? namespaceString)
    {
        if (namespaceString is null)
        {
            return;
        }

        int start = 0;
        bool shouldEscape = false;

        for (int i = 0; i <= namespaceString.Length; i++)
        {
            if (i == namespaceString.Length || namespaceString[i] == '.')
            {
                if (start != 0)
                {
                    b.Append('.');
                }

                string part = namespaceString.Substring(start, i - start);
                if (shouldEscape)
                {
                    NormalizeAndAppendString(b, part);
                    shouldEscape = false;
                }
                else
                {
                    b.Append(part);
                }

                start = i + 1;
                continue;
            }

            shouldEscape = shouldEscape || NeedsEscaping(namespaceString[i], i - start);
        }
    }

    private static void AppendMethodString(StringBuilder methodBuilder, string name, int methodArity)
    {
        int arityStart = name.LastIndexOf('`');
        int arity = 0;
        if (arityStart > 0)
        {
            arityStart++;
            string arityString = name.Substring(arityStart);
            if (int.TryParse(arityString, out arity))
            {
                if (arity == methodArity)
                {
                    name = name.Substring(0, arityStart - 1);
                }
            }
        }

        if (IsNormalized(name))
        {
            methodBuilder.Append(name);
        }
        else
        {
            NormalizeAndAppendString(methodBuilder, name);
        }

        if (arity > 0 && methodArity == arity)
        {
#if NET6_0_OR_GREATER
            methodBuilder.Append(CultureInfo.InvariantCulture, $"`{arity}");
#else
            methodBuilder.Append($"`{arity}");
#endif
        }
    }

    private static void NormalizeAndAppendString(StringBuilder b, string name)
    {
        b.Append('\'');
        for (int i = 0; i < name.Length; i++)
        {
            char c = name[i];
            if (NeedsEscaping(c, i))
            {
                if (c is '\\' or '\'')
                {
                    b.Append('\\');
                    b.Append(c);
                    continue;
                }
            }

            b.Append(c);
        }

        b.Append('\'');
    }

    private static int AppendNestedTypeName(StringBuilder b, Type? type)
    {
        if (type is null)
        {
            return 0;
        }

        int outerArity = 0;
        if (type.IsNested)
        {
            outerArity = AppendNestedTypeName(b, type.DeclaringType);
            b.Append('+');
        }

        string typeName = type.Name;
        int stars = 0;
        if (type.IsPointer)
        {
            for (int i = typeName.Length - 1; i > 0; i--)
            {
                if (typeName[i] != '*')
                {
                    stars = typeName.Length - i - 1;
                    typeName = typeName.Substring(0, i + 1);
                    break;
                }
            }
        }

        TypeInfo info = type.GetTypeInfo();
        int arity = !info.IsGenericType
                  ? 0
                  : info.GenericTypeParameters.Length > 0
                    ? info.GenericTypeParameters.Length
                    : info.GenericTypeArguments.Length;

        AppendMethodString(b, typeName, arity - outerArity);
        b.Append('*', stars);
        return arity;
    }

    private static void AppendGenericTypeParameters(StringBuilder b, Type type)
    {
        Type[] genericArguments = type.GetGenericArguments();
        AppendGenericArguments(b, genericArguments);
    }

    private static void AppendGenericArguments(StringBuilder b, Type[] genericArguments)
    {
        if (genericArguments.Length != 0)
        {
            b.Append('<');
            foreach (Type argType in genericArguments)
            {
                AppendTypeString(b, argType, closedType: true);
                b.Append(',');
            }

            // Replace the last ',' with '>'
            b[b.Length - 1] = '>';
        }
    }

    private static bool IsNormalized(string s)
    {
        int brackets = 0;

        for (int i = 0; i < s.Length; i++)
        {
            char c = s[i];
            if (NeedsEscaping(c, i) && c != '.')
            {
                if (i != 0)
                {
                    if (c == '<')
                    {
                        brackets++;
                        continue;
                    }

                    if (c == '>' && s[i - 1] != '<' && brackets > 0)
                    {
                        brackets--;
                        continue;
                    }
                }

                return false;
            }
        }

        return brackets == 0;
    }

    private static bool NeedsEscaping(char c, int pos)
    {
        if (pos == 0 && char.IsDigit(c))
        {
            return true;
        }

        if (c == '_'
            // 'Digit' does not include letter numbers, which are valid identifiers as per docs https://learn.microsoft.com/dotnet/csharp/fundamentals/coding-style/identifier-names'.
            || char.IsLetterOrDigit(c)) // Lu, Ll, Lt, Lm, Lo, or Nd
        {
            return false;
        }

        UnicodeCategory category = CharUnicodeInfo.GetUnicodeCategory(c);
        return category
            is not UnicodeCategory.LetterNumber // Nl
            and not UnicodeCategory.NonSpacingMark // Mn
            and not UnicodeCategory.SpacingCombiningMark // Mc
            and not UnicodeCategory.ConnectorPunctuation // Pc
            and not UnicodeCategory.Format; // Cf
    }

    private static string GetTypeString(Type type, bool closedType)
    {
        var builder = new StringBuilder();
        AppendTypeString(builder, type, closedType);
        return builder.ToString();
    }

    internal static string ParseEscapedString(string escapedString)
    {
        var stringBuilder = new StringBuilder();
        int end = 0;
        for (int i = 0; i < escapedString.Length; i++)
        {
            if (escapedString[i] == '\'')
            {
                stringBuilder.Append(escapedString, end, i - end);
                end = i = ParseEscapedStringSegment(escapedString, i + 1, stringBuilder);
            }
        }

        if (stringBuilder.Length == 0)
        {
            return escapedString;
        }

        if (end != 0 && end < escapedString.Length)
        {
            stringBuilder.Append(escapedString, end, escapedString.Length - end);
        }

        return stringBuilder.ToString();
    }

    // Unescapes a C# style escaped string.
    private static int ParseEscapedStringSegment(string escapedStringSegment, int pos, StringBuilder stringBuilder)
    {
        for (int i = pos; i < escapedStringSegment.Length; i++)
        {
            switch (escapedStringSegment[i])
            {
                case '\\':
                    if (escapedStringSegment[i + 1] == 'u')
                    {
                        char c;

                        try
                        {
                            string code = escapedStringSegment.Substring(i + 2, 4);
                            c = (char)Convert.ToInt32(code, 16);
                        }
                        catch
                        {
                            throw new InvalidManagedNameException();
                        }

                        stringBuilder.Append(c);
                        i += 5;
                    }
                    else
                    {
                        stringBuilder.Append(escapedStringSegment[++i]);
                    }

                    break;

                case '\'':
                    return i + 1;

                default:
                    stringBuilder.Append(escapedStringSegment[i]);
                    break;
            }
        }

        throw new InvalidManagedNameException();
    }
}
