// 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;

namespace Microsoft.Testing.Platform.MSBuild;

/// <summary>
/// This task generates the entry point for the Testing Platform.
/// </summary>
public sealed class TestingPlatformEntryPointTask : Build.Utilities.Task
{
    private const string CSharpLanguageSymbol = "C#";
    private const string FSharpLanguageSymbol = "F#";
    private const string VBLanguageSymbol = "VB";

    private readonly IFileSystem _fileSystem;

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

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

        _fileSystem = fileSystem;
    }

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

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

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

    /// <summary>
    /// Gets or sets a value indicating whether the generated source should include a process entry point.
    /// The reusable testing-platform application helper is always generated.
    /// </summary>
    public bool GenerateEntryPoint { get; set; } = true;

    /// <summary>
    /// Gets or sets the path to the generated Testing Platform entry point 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? TestingPlatformEntryPointGeneratedFilePath { get; set; }

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

        if (!Language.ItemSpec.Equals(CSharpLanguageSymbol, StringComparison.OrdinalIgnoreCase) &&
            !Language.ItemSpec.Equals(VBLanguageSymbol, StringComparison.OrdinalIgnoreCase) &&
            !Language.ItemSpec.Equals(FSharpLanguageSymbol, StringComparison.OrdinalIgnoreCase))
        {
            TestingPlatformEntryPointGeneratedFilePath = null;
            Log.LogError($"Language '{Language.ItemSpec}' is not supported.");
        }
        else
        {
            GenerateSource(Language.ItemSpec, RootNamespace, GenerateEntryPoint, TestingPlatformEntryPointSourcePath, _fileSystem, Log);
            TestingPlatformEntryPointGeneratedFilePath = TestingPlatformEntryPointSourcePath;
        }

        return !Log.HasLoggedErrors;
    }

    private static void GenerateSource(string language, string? rootNamespace, bool generateEntryPoint, ITaskItem testingPlatformEntryPointSourcePath, IFileSystem fileSystem, TaskLoggingHelper taskLoggingHelper)
    {
        string entryPointSource = GetEntryPointSourceCode(language, rootNamespace, generateEntryPoint);
        taskLoggingHelper.LogMessage(MessageImportance.Normal, $"Entrypoint source:\n'{entryPointSource}'");
        fileSystem.WriteAllText(testingPlatformEntryPointSourcePath.ItemSpec, entryPointSource);
    }

    private static string GetEntryPointSourceCode(string language, string? rootNamespace, bool generateEntryPoint)
    {
        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]
internal static class MicrosoftTestingPlatformApplication
{
    public static async global::System.Threading.Tasks.Task<int> RunAsync(string[] args)
    {
        global::Microsoft.Testing.Platform.Builder.ITestApplicationBuilder builder = await global::Microsoft.Testing.Platform.Builder.TestApplication.CreateBuilderAsync(args);
        global::SelfRegisteredExtensions.AddSelfRegisteredExtensions(builder, args);
        using (global::Microsoft.Testing.Platform.Builder.ITestApplication app = await builder.BuildAsync())
        {
            return await app.RunAsync();
        }
    }
}
{{(generateEntryPoint ? """

[global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
internal sealed class MicrosoftTestingPlatformEntryPoint
{
    public static global::System.Threading.Tasks.Task<int> Main(string[] args)
        => MicrosoftTestingPlatformApplication.RunAsync(args);
}
""" : string.Empty)}}
"""
                : $$"""
//------------------------------------------------------------------------------
// <auto-generated>
//     This code was generated by Microsoft.Testing.Platform.MSBuild
// </auto-generated>
//------------------------------------------------------------------------------

namespace {{rootNamespace}}
{
    [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
    internal static class MicrosoftTestingPlatformApplication
    {
        public static async global::System.Threading.Tasks.Task<int> RunAsync(string[] args)
        {
            global::Microsoft.Testing.Platform.Builder.ITestApplicationBuilder builder = await global::Microsoft.Testing.Platform.Builder.TestApplication.CreateBuilderAsync(args);
            global::{{rootNamespace}}.SelfRegisteredExtensions.AddSelfRegisteredExtensions(builder, args);
            using (global::Microsoft.Testing.Platform.Builder.ITestApplication app = await builder.BuildAsync())
            {
                return await app.RunAsync();
            }
        }
    }
{{(generateEntryPoint ? """

    [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
    internal sealed class MicrosoftTestingPlatformEntryPoint
    {
        public static global::System.Threading.Tasks.Task<int> Main(string[] args)
            => MicrosoftTestingPlatformApplication.RunAsync(args);
    }
""" : string.Empty)}}
}
""";
        }
        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 MicrosoftTestingPlatformApplication
    Public Async Function RunAsync(args As String()) As Global.System.Threading.Tasks.Task(Of Integer)
        Dim builder = Await Global.Microsoft.Testing.Platform.Builder.TestApplication.CreateBuilderAsync(args)
        SelfRegisteredExtensions.AddSelfRegisteredExtensions(builder, args)
        Using testApplication = Await builder.BuildAsync()
            Return Await testApplication.RunAsync()
        End Using
    End Function

End Module
{{(generateEntryPoint ? """

<System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage>
Module MicrosoftTestingPlatformEntryPoint
    Function Main(args As String()) As Integer
        Return MicrosoftTestingPlatformApplication.RunAsync(args).GetAwaiter().GetResult()
    End Function

    Public Function MainAsync(args As String()) As Global.System.Threading.Tasks.Task(Of Integer)
        Return MicrosoftTestingPlatformApplication.RunAsync(args)
    End Function
End Module
""" : string.Empty)}}
""";
        }
        else if (language == FSharpLanguageSymbol)
        {
            return RoslynString.IsNullOrEmpty(rootNamespace)
                ? $$"""
//------------------------------------------------------------------------------
// <auto-generated>
//     This code was generated by Microsoft.Testing.Platform.MSBuild
// </auto-generated>
//------------------------------------------------------------------------------

{{(generateEntryPoint ? string.Empty : "namespace Microsoft.TestingPlatform")}}

module internal MicrosoftTestingPlatformApplication =

    let runAsync args =
        task {
            let! builder = Microsoft.Testing.Platform.Builder.TestApplication.CreateBuilderAsync args
            Microsoft.TestingPlatform.Extensions.SelfRegisteredExtensions.AddSelfRegisteredExtensions(builder, args)
            use! app = builder.BuildAsync()
            return! app.RunAsync()
        }
{{(generateEntryPoint ? """

module MicrosoftTestingPlatformEntryPoint =

    [<System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage>]
    [<EntryPoint>]
    let main args =
        MicrosoftTestingPlatformApplication.runAsync args
        |> Async.AwaitTask
        |> Async.RunSynchronously
""" : string.Empty)}}
"""
                : $$"""
//------------------------------------------------------------------------------
// <auto-generated>
//     This code was generated by Microsoft.Testing.Platform.MSBuild
// </auto-generated>
//------------------------------------------------------------------------------

namespace {{rootNamespace}}

module internal MicrosoftTestingPlatformApplication =

    let runAsync args =
        task {
            let! builder = Microsoft.Testing.Platform.Builder.TestApplication.CreateBuilderAsync args
            SelfRegisteredExtensions.AddSelfRegisteredExtensions(builder, args)
            use! app = builder.BuildAsync()
            return! app.RunAsync()
        }
{{(generateEntryPoint ? """

module MicrosoftTestingPlatformEntryPoint =

    [<System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage>]
    [<EntryPoint>]
    let main args =
        MicrosoftTestingPlatformApplication.runAsync args
        |> Async.AwaitTask
        |> Async.RunSynchronously
""" : string.Empty)}}
""";
        }

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