// 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.Build.Framework;
using Microsoft.Build.Utilities;
using Microsoft.Testing.Platform.Helpers;

namespace Microsoft.Testing.Platform.MSBuild;

/// <summary>
/// Task to generate the self-registered extensions source file.
/// </summary>
public sealed class TestingPlatformSelfRegisteredExtensions : Build.Utilities.Task
{
    private const string DisplayNameMetadataName = "DisplayName";
    private const string TypeFullNameMetadataName = "TypeFullName";
    private const string CSharpLanguageSymbol = "C#";
    private const string FSharpLanguageSymbol = "F#";
    private const string VBLanguageSymbol = "VB";

    private const string EmbeddedAttributeDefinition = """
        namespace Microsoft.CodeAnalysis
        {
            internal sealed partial class EmbeddedAttribute : global::System.Attribute
            {
            }
        }
        """;

    private readonly IFileSystem _fileSystem;

    /// <summary>
    /// Initializes a new instance of the <see cref="TestingPlatformSelfRegisteredExtensions"/> class.
    /// </summary>
    public TestingPlatformSelfRegisteredExtensions()
        : this(new FileSystem())
    {
    }

    internal TestingPlatformSelfRegisteredExtensions(IFileSystem fileSystem)
    {
        if (Environment.GetEnvironmentVariable("TESTINGPLATFORM_MSBUILD_LAUNCH_ATTACH_DEBUGGER") == "1")
        {
            Debugger.Launch();
        }

        _fileSystem = fileSystem;
    }

    /// <summary>
    /// Gets or sets the path to the source file.
    /// </summary>
    [Required]
    public required ITaskItem SelfRegisteredExtensionsSourcePath { get; set; }

    /// <summary>
    /// Gets or sets the language of the source file.
    /// </summary>
    [Required]
    public required ITaskItem Language { get; set; }

    /// <summary>
    /// Gets or sets the builder hooks.
    /// </summary>
    [Required]
    public required ITaskItem[] SelfRegisteredExtensionsBuilderHook { get; set; }

    /// <summary>
    /// Gets or sets the root namespace.
    /// </summary>
    public string? RootNamespace { get; set; }

    /// <summary>
    /// Gets or sets the path to the generated file. It stays <see langword="null"/> when the project language is
    /// not supported, in which case the task produces no output item.
    /// </summary>
    [Output]
    public ITaskItem? SelfRegisteredExtensionsGeneratedFilePath { get; set; }

    private readonly string _expectedItemSpec = """
Expected item spec:
<ItemGroup>
    <!-- Unique stable identifier for the builder hook. -->
    <TestingPlatformBuilderHook Include="8E680F4D-E423-415A-9566-855439363BC0" >
        <!-- Display name for the builder hook. -->
        <DisplayName>MyBuilderHook</DisplayName>
        <!-- Full type name for the builder hook. -->
        <TypeFullName>Contoso.BuilderHook</TypeFullName>
    </TestingPlatformBuilderHook>
</ItemGroup>
Expected method signature
static Contoso.BuilderHook.AddExtensions(Microsoft.Testing.Platform.Builder.TestApplicationBuilder builder, string[] args)
""";

    /// <inheritdoc />
    public override bool Execute()
    {
        Log.LogMessage(MessageImportance.Normal, $"SelfRegisteredExtensionsSourcePath: '{SelfRegisteredExtensionsSourcePath.ItemSpec}'");
        Log.LogMessage(MessageImportance.Normal, $"Language: '{Language.ItemSpec}'");

        ITaskItem[] selfRegisteredExtensionsBuilderHook = ValidateAndDeduplicateBuilderHooks();

        if (selfRegisteredExtensionsBuilderHook.Length > 0)
        {
            StringBuilder stringBuilder = new();
            stringBuilder.AppendLine("TestingPlatformExtensionFullTypeNames:");

            foreach (ITaskItem item in selfRegisteredExtensionsBuilderHook)
            {
                if (RoslynString.IsNullOrEmpty(item.GetMetadata(DisplayNameMetadataName)))
                {
                    Log.LogError("Missing 'DisplayName' metadata for item 'TestingPlatformBuilderHook'\n{0}", _expectedItemSpec);
                }

                if (RoslynString.IsNullOrEmpty(item.GetMetadata(TypeFullNameMetadataName)))
                {
                    Log.LogError("Missing 'TypeFullName' metadata for item 'TestingPlatformBuilderHook'\n{0}", _expectedItemSpec);
                }

                stringBuilder.AppendLine(CultureInfo.InvariantCulture, $" Hook UID: '{item.ItemSpec}' DisplayName: '{item.GetMetadata(DisplayNameMetadataName)}' TypeFullName: '{item.GetMetadata(TypeFullNameMetadataName)}'");
            }

            Log.LogMessage(MessageImportance.Normal, stringBuilder.ToString());
        }

        if (!Log.HasLoggedErrors)
        {
            if (!Language.ItemSpec.Equals(CSharpLanguageSymbol, StringComparison.OrdinalIgnoreCase) &&
                !Language.ItemSpec.Equals(VBLanguageSymbol, StringComparison.OrdinalIgnoreCase) &&
                !Language.ItemSpec.Equals(FSharpLanguageSymbol, StringComparison.OrdinalIgnoreCase))
            {
                SelfRegisteredExtensionsGeneratedFilePath = null;
                Log.LogError($"Language '{Language.ItemSpec}' is not supported.");
            }
            else
            {
                GenerateCode(Language.ItemSpec, RootNamespace, selfRegisteredExtensionsBuilderHook, SelfRegisteredExtensionsSourcePath, _fileSystem, Log);
                SelfRegisteredExtensionsGeneratedFilePath = SelfRegisteredExtensionsSourcePath;
            }
        }

        return !Log.HasLoggedErrors;
    }

    private ITaskItem[] ValidateAndDeduplicateBuilderHooks()
    {
        List<ITaskItem> distinctBuilderHooks = [];

        foreach (IGrouping<string, ITaskItem> group in SelfRegisteredExtensionsBuilderHook.GroupBy(x => x.ItemSpec))
        {
            ITaskItem firstItem = group.First();
            foreach (ITaskItem duplicateItem in group.Skip(1))
            {
                if (!string.Equals(firstItem.GetMetadata(DisplayNameMetadataName), duplicateItem.GetMetadata(DisplayNameMetadataName), StringComparison.Ordinal) ||
                    !string.Equals(firstItem.GetMetadata(TypeFullNameMetadataName), duplicateItem.GetMetadata(TypeFullNameMetadataName), StringComparison.Ordinal))
                {
                    Log.LogError(
                        "Duplicate 'TestingPlatformBuilderHook' item with Include '{0}' has conflicting metadata. Items with the same Include value must have identical '{1}' and '{2}' metadata.",
                        firstItem.ItemSpec,
                        DisplayNameMetadataName,
                        TypeFullNameMetadataName);
                    break;
                }
            }

            distinctBuilderHooks.Add(firstItem);
        }

        return distinctBuilderHooks.ToArray();
    }

    private static void GenerateCode(string language, string? rootNamespace, ITaskItem[] taskItems, ITaskItem testingPlatformEntryPointSourcePath, IFileSystem fileSystem, TaskLoggingHelper taskLoggingHelper)
    {
        StringBuilder builder = new();

        for (int i = 0; i < taskItems.Length; i++)
        {
            if (i != 0)
            {
                // Indent
                builder.Append("        ");
            }

            string global = language switch
            {
                CSharpLanguageSymbol => "global::",
                VBLanguageSymbol => "Global.",
                FSharpLanguageSymbol => "global.",
                _ => throw ApplicationStateGuard.Unreachable(),
            };

            builder.Append(CultureInfo.InvariantCulture, $"{global}{taskItems[i].GetMetadata(TypeFullNameMetadataName)}.AddExtensions(builder, args){(language == CSharpLanguageSymbol ? ";" : string.Empty)}");
            if (i < taskItems.Length - 1)
            {
                builder.AppendLine();
            }
        }

        string entryPointSource = GetSourceCode(language, rootNamespace, builder.ToString());
        taskLoggingHelper.LogMessage(MessageImportance.Normal, $"SelfRegisteredExtensions source:\n'{entryPointSource}'");
        fileSystem.WriteAllText(testingPlatformEntryPointSourcePath.ItemSpec, entryPointSource);
    }

    private static string GetSourceCode(string language, string? rootNamespace, string extensionsFragments)
    {
        if (language != VBLanguageSymbol && !RoslynString.IsNullOrEmpty(rootNamespace))
        {
            rootNamespace = NamespaceHelpers.ToSafeNamespace(rootNamespace);
        }

        if (language == CSharpLanguageSymbol)
        {
            return RoslynString.IsNullOrEmpty(rootNamespace)
                ? $$"""
//------------------------------------------------------------------------------
// <auto-generated>
//     This code was generated by Microsoft.Testing.Platform.MSBuild
// </auto-generated>
//------------------------------------------------------------------------------

[global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
[global::Microsoft.CodeAnalysis.Embedded]
internal static class SelfRegisteredExtensions
{
    public static void AddSelfRegisteredExtensions(this global::Microsoft.Testing.Platform.Builder.ITestApplicationBuilder builder, string[] args)
    {
        {{extensionsFragments}}
    }
}

{{EmbeddedAttributeDefinition}}
"""
                : $$"""
//------------------------------------------------------------------------------
// <auto-generated>
//     This code was generated by Microsoft.Testing.Platform.MSBuild
// </auto-generated>
//------------------------------------------------------------------------------

namespace {{rootNamespace}}
{
    [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
    [global::Microsoft.CodeAnalysis.Embedded]
    internal static class SelfRegisteredExtensions
    {
        public static void AddSelfRegisteredExtensions(this global::Microsoft.Testing.Platform.Builder.ITestApplicationBuilder builder, string[] args)
        {
            {{extensionsFragments}}
        }
    }
}

{{EmbeddedAttributeDefinition}}
""";
        }
        else if (language == VBLanguageSymbol)
        {
            // NOTE: We don't use the value of RootNamespace here.
            // The compiler *already* wraps types in RootNamespace for Visual Basic
            // This is not the case for C# or F#.
            return $$"""
'------------------------------------------------------------------------------
' <auto-generated>
'     This code was generated by Microsoft.Testing.Platform.MSBuild
' </auto-generated>
'------------------------------------------------------------------------------

<System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage>
Friend Module SelfRegisteredExtensions
    <System.Runtime.CompilerServices.Extension>
    Public Sub AddSelfRegisteredExtensions(ByVal builder As Global.Microsoft.Testing.Platform.Builder.ITestApplicationBuilder, ByVal args As Global.System.String())
        {{extensionsFragments}}
    End Sub
End Module
""";
        }
        else if (language == FSharpLanguageSymbol)
        {
            return RoslynString.IsNullOrEmpty(rootNamespace)
                ? $$"""
//------------------------------------------------------------------------------
// <auto-generated>
//     This code was generated by Microsoft.Testing.Platform.MSBuild
// </auto-generated>
//------------------------------------------------------------------------------

namespace Microsoft.TestingPlatform.Extensions

open System.Runtime.CompilerServices

[<System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage>]
[<Extension>]
type SelfRegisteredExtensions() =

    [<Extension>]
    static member AddSelfRegisteredExtensions (builder: Microsoft.Testing.Platform.Builder.ITestApplicationBuilder, args: string[]) =
        {{(extensionsFragments.Length > 0 ? extensionsFragments : "()")}}
"""
                : $$"""
//------------------------------------------------------------------------------
// <auto-generated>
//     This code was generated by Microsoft.Testing.Platform.MSBuild
// </auto-generated>
//------------------------------------------------------------------------------

namespace {{rootNamespace}}

open System.Runtime.CompilerServices

[<System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage>]
[<Extension>]
type SelfRegisteredExtensions() =

    [<Extension>]
    static member AddSelfRegisteredExtensions (builder: Microsoft.Testing.Platform.Builder.ITestApplicationBuilder, args: string[]) =
        {{(extensionsFragments.Length > 0 ? extensionsFragments : "()")}}
""";
        }

        throw new InvalidOperationException($"Language not supported '{language}'");
    }
}
