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

using System.CommandLine;

using Microsoft.Testing.TestInfrastructure;

using MSTest.Performance.Runner.Steps;

using DotnetMuxer = MSTest.Performance.Runner.Steps.DotnetMuxer;
using ExecutionScope = MSTest.Performance.Runner.Steps.ExecutionScope;

namespace MSTest.Performance.Runner;

internal class EntryPoint
{
    public static Task<int> Main(string[] args)
    {
        // Opt out telemetry for clean stacks, AppInsight is allocating strings and polluting the results.
        Environment.SetEnvironmentVariable("DOTNET_CLI_TELEMETRY_OPTOUT", "1");

        Console.WriteLine("Microsoft (R) MSTest Performance Profiler Command Line Tool");

        var rootCommand = new RootCommand("MSTest Performance Profiler Command Line Tool");
        var pipelineNameFilter = new Option<string>(name: "--pipelineNameFilter", description: "Globing filter for the pipeline name to execute.", getDefaultValue: () => string.Empty);
        var executeTests = new Command("execute", "Execute the performance scenarios.")
        {
            pipelineNameFilter,
        };
        executeTests.SetHandler(
            pipelineNameFilter => _ = Pipelines(pipelineNameFilter), pipelineNameFilter);

        rootCommand.AddCommand(executeTests);

        return rootCommand.InvokeAsync(args);
    }

    private static int Pipelines(string pipelineNameFilter)
    {
        var pipelineRunner = new PipelinesRunner();

        pipelineRunner.AddPipeline("Default", "Scenario1_PerfView", [OSPlatform.Windows], parametersBag =>
        Pipeline
            .FirstStep(() => new Scenario1(numberOfClass: 100, methodsPerClass: 100, tfm: "net9.0", executionScope: ExecutionScope.MethodLevel), parametersBag)
            .NextStep(() => new DotnetMuxer(BuildConfiguration.Debug))
            .NextStep(() => new PerfviewRunner(" /BufferSizeMB:1024 ", "Scenario1_PerfView.zip", includeScenario: true))
            .NextStep(() => new MoveFiles("*.zip", Path.Combine(Directory.GetCurrentDirectory(), "Results")))
            .NextStep(() => new CleanupDisposable()));

        // dotnet-trace is a cross-platform CLI tool; no Windows-only restriction needed.
        pipelineRunner.AddPipeline("Default", "Scenario1_DotnetTrace", [OSPlatform.Windows, OSPlatform.Linux, OSPlatform.OSX], parametersBag =>
        Pipeline
            .FirstStep(() => new Scenario1(numberOfClass: 100, methodsPerClass: 100, tfm: "net9.0", executionScope: ExecutionScope.MethodLevel), parametersBag)
            .NextStep(() => new DotnetMuxer(BuildConfiguration.Debug))
            .NextStep(() => new DotnetTrace("--profile cpu-sampling", "DotnetTrace_CPU_Sampling.zip"))
            .NextStep(() => new MoveFiles("*.zip", Path.Combine(Directory.GetCurrentDirectory(), "Results")))
            .NextStep(() => new CleanupDisposable()));

        // C:\Program Files\Microsoft Visual Studio\2022\Preview\Team Tools\DiagnosticsHub\Collector\AgentConfigs
        pipelineRunner.AddPipeline("Default", "Scenario1_DotNetObjectAllocBase", [OSPlatform.Windows], parametersBag =>
        Pipeline
            .FirstStep(() => new Scenario1(numberOfClass: 100, methodsPerClass: 100, tfm: "net9.0", executionScope: ExecutionScope.MethodLevel), parametersBag)
            .NextStep(() => new DotnetMuxer(BuildConfiguration.Debug))
            .NextStep(() => new VSDiagnostics("DotNetObjectAllocLow.json", "Scenario1_DotNetObjectAllocBase.zip"))
            .NextStep(() => new MoveFiles("*.zip", Path.Combine(Directory.GetCurrentDirectory(), "Results")))
            .NextStep(() => new CleanupDisposable()));
        pipelineRunner.AddPipeline("Default", "Scenario1_CpuUsageLow", [OSPlatform.Windows], parametersBag =>
        Pipeline
            .FirstStep(() => new Scenario1(numberOfClass: 100, methodsPerClass: 100, tfm: "net9.0", executionScope: ExecutionScope.MethodLevel), parametersBag)
            .NextStep(() => new DotnetMuxer(BuildConfiguration.Debug))
            .NextStep(() => new VSDiagnostics("CpuUsageHigh.json", "Scenario1_CpuUsageLow.zip"))
            .NextStep(() => new MoveFiles("*.zip", Path.Combine(Directory.GetCurrentDirectory(), "Results")))
            .NextStep(() => new CleanupDisposable()));

        pipelineRunner.AddPipeline("Default", "Scenario1_ConcurrencyVisualizer", [OSPlatform.Windows], parametersBag =>
        Pipeline
            .FirstStep(() => new Scenario1(numberOfClass: 100, methodsPerClass: 100, tfm: "net9.0", executionScope: ExecutionScope.MethodLevel), parametersBag)
            .NextStep(() => new DotnetMuxer(BuildConfiguration.Debug))
            .NextStep(() => new ConcurrencyVisualizer("Scenario1_ConcurrencyVisualizer.zip"))
            .NextStep(() => new MoveFiles("*.zip", Path.Combine(Directory.GetCurrentDirectory(), "Results")))
            .NextStep(() => new CleanupDisposable()));

        // PlainProcess records wall-clock time, CPU time, and processor count via the standard
        // Process API — all cross-platform. Register it for all major OS platforms so contributors
        // on Linux and macOS can capture baseline timing data without needing Windows tools.
        pipelineRunner.AddPipeline("Default", "Scenario1_PlainProcess", [OSPlatform.Windows, OSPlatform.Linux, OSPlatform.OSX], parametersBag =>
        Pipeline
            .FirstStep(() => new Scenario1(numberOfClass: 100, methodsPerClass: 100, tfm: "net9.0", executionScope: ExecutionScope.MethodLevel), parametersBag)
            .NextStep(() => new DotnetMuxer(BuildConfiguration.Debug))
            .NextStep(() => new PlainProcess("Scenario1_PlainProcess.zip"))
            .NextStep(() => new MoveFiles("*.zip", Path.Combine(Directory.GetCurrentDirectory(), "Results")))
            .NextStep(() => new CleanupDisposable()));

        // Class-level variant: tests run in parallel at the class level rather than per-method.
        // Useful for comparing parallelism-strategy overhead vs. MethodLevel on the same hardware.
        pipelineRunner.AddPipeline("Default", "Scenario1_ClassLevel_PlainProcess", [OSPlatform.Windows, OSPlatform.Linux, OSPlatform.OSX], parametersBag =>
        Pipeline
            .FirstStep(() => new Scenario1(numberOfClass: 100, methodsPerClass: 100, tfm: "net9.0", executionScope: ExecutionScope.ClassLevel), parametersBag)
            .NextStep(() => new DotnetMuxer(BuildConfiguration.Debug))
            .NextStep(() => new PlainProcess("Scenario1_ClassLevel_PlainProcess.zip"))
            .NextStep(() => new MoveFiles("*.zip", Path.Combine(Directory.GetCurrentDirectory(), "Results")))
            .NextStep(() => new CleanupDisposable()));

        // Server-mode variant: runs via `dotnet test --no-build` to exercise the MTP JSON-RPC
        // (named-pipe) path rather than the plain-process standalone path.  The "PlainProcess"
        // suffix keeps this scenario captured by the existing nightly workflow filter (*PlainProcess*).
        pipelineRunner.AddPipeline("Default", "Scenario1_DotnetTest_PlainProcess", [OSPlatform.Windows, OSPlatform.Linux, OSPlatform.OSX], parametersBag =>
        Pipeline
            .FirstStep(() => new Scenario1(numberOfClass: 100, methodsPerClass: 100, tfm: "net9.0", executionScope: ExecutionScope.MethodLevel), parametersBag)
            .NextStep(() => new DotnetMuxer(BuildConfiguration.Debug))
            .NextStep(() => new DotnetTestProcess("Scenario1_DotnetTest_PlainProcess.zip", BuildConfiguration.Debug))
            .NextStep(() => new MoveFiles("*.zip", Path.Combine(Directory.GetCurrentDirectory(), "Results")))
            .NextStep(() => new CleanupDisposable()));

        // Scenario2: data-driven tests (100 classes × 10 methods × 10 DataRow rows = 10 000 executions).
        // Matches Scenario1's total execution count for a direct overhead comparison between
        // the plain and data-driven execution paths.
        pipelineRunner.AddPipeline("Default", "Scenario2_DataDriven_PlainProcess", [OSPlatform.Windows, OSPlatform.Linux, OSPlatform.OSX], parametersBag =>
        Pipeline
            .FirstStep(() => new Scenario2(numberOfClass: 100, methodsPerClass: 10, dataRowsPerMethod: 10, tfm: "net9.0", executionScope: ExecutionScope.MethodLevel), parametersBag)
            .NextStep(() => new DotnetMuxer(BuildConfiguration.Debug))
            .NextStep(() => new PlainProcess("Scenario2_DataDriven_PlainProcess.zip"))
            .NextStep(() => new MoveFiles("*.zip", Path.Combine(Directory.GetCurrentDirectory(), "Results")))
            .NextStep(() => new CleanupDisposable()));

        // Class-level parallelism variant of the data-driven scenario.
        pipelineRunner.AddPipeline("Default", "Scenario2_DataDriven_ClassLevel_PlainProcess", [OSPlatform.Windows, OSPlatform.Linux, OSPlatform.OSX], parametersBag =>
        Pipeline
            .FirstStep(() => new Scenario2(numberOfClass: 100, methodsPerClass: 10, dataRowsPerMethod: 10, tfm: "net9.0", executionScope: ExecutionScope.ClassLevel), parametersBag)
            .NextStep(() => new DotnetMuxer(BuildConfiguration.Debug))
            .NextStep(() => new PlainProcess("Scenario2_DataDriven_ClassLevel_PlainProcess.zip"))
            .NextStep(() => new MoveFiles("*.zip", Path.Combine(Directory.GetCurrentDirectory(), "Results")))
            .NextStep(() => new CleanupDisposable()));

        // dotnet-test path for the data-driven scenario.
        pipelineRunner.AddPipeline("Default", "Scenario2_DataDriven_DotnetTest_PlainProcess", [OSPlatform.Windows, OSPlatform.Linux, OSPlatform.OSX], parametersBag =>
        Pipeline
            .FirstStep(() => new Scenario2(numberOfClass: 100, methodsPerClass: 10, dataRowsPerMethod: 10, tfm: "net9.0", executionScope: ExecutionScope.MethodLevel), parametersBag)
            .NextStep(() => new DotnetMuxer(BuildConfiguration.Debug))
            .NextStep(() => new DotnetTestProcess("Scenario2_DataDriven_DotnetTest_PlainProcess.zip", BuildConfiguration.Debug))
            .NextStep(() => new MoveFiles("*.zip", Path.Combine(Directory.GetCurrentDirectory(), "Results")))
            .NextStep(() => new CleanupDisposable()));

        // Scenario3: lifecycle overhead — same total execution count as Scenario1 (100 classes ×
        // 100 methods = 10 000 executions), but every class has a [TestInitialize] and a
        // [TestCleanup] method. Running Scenario1 and Scenario3 back-to-back on the same machine
        // isolates the per-test cost of the MSTest lifecycle machinery.
        pipelineRunner.AddPipeline("Default", "Scenario3_Lifecycle_PlainProcess", [OSPlatform.Windows, OSPlatform.Linux, OSPlatform.OSX], parametersBag =>
        Pipeline
            .FirstStep(() => new Scenario3(numberOfClass: 100, methodsPerClass: 100, tfm: "net9.0", executionScope: ExecutionScope.MethodLevel), parametersBag)
            .NextStep(() => new DotnetMuxer(BuildConfiguration.Debug))
            .NextStep(() => new PlainProcess("Scenario3_Lifecycle_PlainProcess.zip"))
            .NextStep(() => new MoveFiles("*.zip", Path.Combine(Directory.GetCurrentDirectory(), "Results")))
            .NextStep(() => new CleanupDisposable()));

        // Class-level parallelism variant of the lifecycle scenario.
        pipelineRunner.AddPipeline("Default", "Scenario3_Lifecycle_ClassLevel_PlainProcess", [OSPlatform.Windows, OSPlatform.Linux, OSPlatform.OSX], parametersBag =>
        Pipeline
            .FirstStep(() => new Scenario3(numberOfClass: 100, methodsPerClass: 100, tfm: "net9.0", executionScope: ExecutionScope.ClassLevel), parametersBag)
            .NextStep(() => new DotnetMuxer(BuildConfiguration.Debug))
            .NextStep(() => new PlainProcess("Scenario3_Lifecycle_ClassLevel_PlainProcess.zip"))
            .NextStep(() => new MoveFiles("*.zip", Path.Combine(Directory.GetCurrentDirectory(), "Results")))
            .NextStep(() => new CleanupDisposable()));

        // dotnet-test path for the lifecycle scenario.
        pipelineRunner.AddPipeline("Default", "Scenario3_Lifecycle_DotnetTest_PlainProcess", [OSPlatform.Windows, OSPlatform.Linux, OSPlatform.OSX], parametersBag =>
        Pipeline
            .FirstStep(() => new Scenario3(numberOfClass: 100, methodsPerClass: 100, tfm: "net9.0", executionScope: ExecutionScope.MethodLevel), parametersBag)
            .NextStep(() => new DotnetMuxer(BuildConfiguration.Debug))
            .NextStep(() => new DotnetTestProcess("Scenario3_Lifecycle_DotnetTest_PlainProcess.zip", BuildConfiguration.Debug))
            .NextStep(() => new MoveFiles("*.zip", Path.Combine(Directory.GetCurrentDirectory(), "Results")))
            .NextStep(() => new CleanupDisposable()));

        // Scenario4: class-initialization overhead (100 classes × 100 methods = 10 000 executions).
        // Each class has a [ClassInitialize] and [ClassCleanup]; one dedicated class carries
        // [AssemblyInitialize] and [AssemblyCleanup]. Running Scenario4 alongside Scenario1 and
        // Scenario3 isolates the per-class cost of class-lifecycle dispatch compared to the
        // per-test (TestInitialize/TestCleanup) cost measured by Scenario3.
        pipelineRunner.AddPipeline("Default", "Scenario4_ClassInit_PlainProcess", [OSPlatform.Windows, OSPlatform.Linux, OSPlatform.OSX], parametersBag =>
        Pipeline
            .FirstStep(() => new Scenario4(numberOfClass: 100, methodsPerClass: 100, tfm: "net9.0", executionScope: ExecutionScope.MethodLevel), parametersBag)
            .NextStep(() => new DotnetMuxer(BuildConfiguration.Debug))
            .NextStep(() => new PlainProcess("Scenario4_ClassInit_PlainProcess.zip"))
            .NextStep(() => new MoveFiles("*.zip", Path.Combine(Directory.GetCurrentDirectory(), "Results")))
            .NextStep(() => new CleanupDisposable()));

        // Class-level parallelism variant of the class-init scenario.
        pipelineRunner.AddPipeline("Default", "Scenario4_ClassInit_ClassLevel_PlainProcess", [OSPlatform.Windows, OSPlatform.Linux, OSPlatform.OSX], parametersBag =>
        Pipeline
            .FirstStep(() => new Scenario4(numberOfClass: 100, methodsPerClass: 100, tfm: "net9.0", executionScope: ExecutionScope.ClassLevel), parametersBag)
            .NextStep(() => new DotnetMuxer(BuildConfiguration.Debug))
            .NextStep(() => new PlainProcess("Scenario4_ClassInit_ClassLevel_PlainProcess.zip"))
            .NextStep(() => new MoveFiles("*.zip", Path.Combine(Directory.GetCurrentDirectory(), "Results")))
            .NextStep(() => new CleanupDisposable()));

        // dotnet-test path for the class-init scenario.
        pipelineRunner.AddPipeline("Default", "Scenario4_ClassInit_DotnetTest_PlainProcess", [OSPlatform.Windows, OSPlatform.Linux, OSPlatform.OSX], parametersBag =>
        Pipeline
            .FirstStep(() => new Scenario4(numberOfClass: 100, methodsPerClass: 100, tfm: "net9.0", executionScope: ExecutionScope.MethodLevel), parametersBag)
            .NextStep(() => new DotnetMuxer(BuildConfiguration.Debug))
            .NextStep(() => new DotnetTestProcess("Scenario4_ClassInit_DotnetTest_PlainProcess.zip", BuildConfiguration.Debug))
            .NextStep(() => new MoveFiles("*.zip", Path.Combine(Directory.GetCurrentDirectory(), "Results")))
            .NextStep(() => new CleanupDisposable()));

        return pipelineRunner.Run(pipelineNameFilter);
    }
}
