// 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.Testing.Platform.Acceptance.IntegrationTests;

[TestClass]
public class MSBuildTests_Test : AcceptanceTestBase<NopAssetFixture>
{
    private const string AssetName = "MSBuildTests";

    public static string? FormatBuildMatrixEntry(MethodInfo method, object?[]? data)
        => $"{data![0]},{(Equals(TargetFrameworks.All.ToMSBuildTargetFrameworks(), data[1]) ? "multitfm" : data[1])},{data[2]},{((bool)data[3]! ? "Succeeded" : "Failed")}";

    internal static IEnumerable<(string BuildCommand, string TargetFramework, BuildConfiguration BuildConfiguration, bool TestSucceeded)> GetBuildMatrix()
    {
        foreach (string tfm in TargetFrameworks.All)
        {
            foreach ((string buildCommand, _, BuildConfiguration buildConfiguration, bool testSucceeded) in GetBuildMatrixMultiTfm())
            {
                yield return (buildCommand, tfm, buildConfiguration, testSucceeded);
            }
        }
    }

    internal static IEnumerable<(string BuildCommand, string TargetFramework, BuildConfiguration BuildConfiguration, bool TestSucceeded)> GetBuildMatrixMultiTfm()
    {
        foreach (BuildConfiguration compilationMode in Enum.GetValues<BuildConfiguration>())
        {
            foreach (bool testSucceeded in new bool[] { true, false })
            {
                yield return ("build -t:Test", TargetFrameworks.All.ToMSBuildTargetFrameworks(), compilationMode, testSucceeded);
            }
        }
    }

    [DynamicData(nameof(GetBuildMatrix), DynamicDataDisplayName = nameof(FormatBuildMatrixEntry))]
    [TestMethod]
    public async Task InvokeTestingPlatform_Target_Should_Execute_Tests_Without_Showing_Error_Detail_SingleTfm(string testCommand, string tfm, BuildConfiguration compilationMode, bool testSucceeded)
        => await InvokeTestingPlatform_Target_Should_Execute_Tests_Without_Showing_Error_Detail(testCommand, tfm, false, [tfm], compilationMode, testSucceeded);

    [DynamicData(nameof(GetBuildMatrixMultiTfm), DynamicDataDisplayName = nameof(FormatBuildMatrixEntry))]
    [TestMethod]
    public async Task InvokeTestingPlatform_Target_Should_Execute_Tests_Without_Showing_Error_Detail_MultiTfm(string testCommand, string multiTfm, BuildConfiguration compilationMode, bool testSucceeded)
        => await InvokeTestingPlatform_Target_Should_Execute_Tests_Without_Showing_Error_Detail(testCommand, multiTfm, true, TargetFrameworks.All, compilationMode, testSucceeded);

    private async Task InvokeTestingPlatform_Target_Should_Execute_Tests_Without_Showing_Error_Detail(string testCommand, string tfm, bool isMultiTfm, string[] tfmsToAssert, BuildConfiguration compilationMode, bool testSucceeded)
    {
        using TestAsset testAsset = await TestAsset.GenerateAssetAsync(
            AssetName,
            SourceCode
            .PatchCodeWithReplace("$PlatformTarget$", "<PlatformTarget>x64</PlatformTarget>")
            .PatchCodeWithReplace("$TargetFrameworks$", isMultiTfm ? $"<TargetFrameworks>{tfm}</TargetFrameworks>" : $"<targetFramework>{tfm}</targetFramework>")
            .PatchCodeWithReplace("$AssertValue$", testSucceeded.ToString().ToLowerInvariant())
            .PatchCodeWithReplace("$MicrosoftTestingPlatformVersion$", MicrosoftTestingPlatformVersion));
        string testResultFolder = Path.Combine(testAsset.TargetAssetPath, Guid.NewGuid().ToString("N"));
        DotnetMuxerResult compilationResult = await DotnetCli.RunAsync($"{testCommand} -p:TestingPlatformCommandLineArguments=\"--results-directory %22{testResultFolder}%22\" -p:Configuration={compilationMode} \"{testAsset.TargetAssetPath}\"", workingDirectory: testAsset.TargetAssetPath, failIfReturnValueIsNotZero: false, cancellationToken: TestContext.CancellationToken);

        foreach (string tfmToAssert in tfmsToAssert)
        {
            CommonAssert(compilationResult, tfmToAssert, testSucceeded, testResultFolder);
        }
    }

    [DynamicData(nameof(GetBuildMatrix), DynamicDataDisplayName = nameof(FormatBuildMatrixEntry))]
    [TestMethod]
    public async Task InvokeTestingPlatform_Target_Should_Build_Without_Warnings_And_Execute_Passing_Test_And_Pass_TheRun_SingleTfm(string testCommand, string tfm, BuildConfiguration compilationMode, bool testSucceeded)
        => await InvokeTestingPlatform_Target_Should_Build_Without_Warnings_And_Execute_Passing_Test_And_Pass_TheRun_Detail(testCommand, tfm, false, [tfm], compilationMode, testSucceeded);

    [DynamicData(nameof(GetBuildMatrixMultiTfm), DynamicDataDisplayName = nameof(FormatBuildMatrixEntry))]
    [TestMethod]
    public async Task InvokeTestingPlatform_Target_Should_Build_Without_Warnings_And_Execute_Passing_Test_And_Pass_TheRun_MultiTfm(string testCommand, string multiTfm, BuildConfiguration compilationMode, bool testSucceeded)
        => await InvokeTestingPlatform_Target_Should_Build_Without_Warnings_And_Execute_Passing_Test_And_Pass_TheRun_Detail(testCommand, multiTfm, true, TargetFrameworks.All, compilationMode, testSucceeded);

    private async Task InvokeTestingPlatform_Target_Should_Build_Without_Warnings_And_Execute_Passing_Test_And_Pass_TheRun_Detail(string testCommand, string tfm, bool isMultiTfm, string[] tfmsToAssert, BuildConfiguration compilationMode, bool testSucceeded)
    {
        using TestAsset testAsset = await TestAsset.GenerateAssetAsync(
            AssetName,
            SourceCode
            .PatchCodeWithReplace("$PlatformTarget$", "<PlatformTarget>x64</PlatformTarget>")
            .PatchCodeWithReplace("$TargetFrameworks$", isMultiTfm ? $"<TargetFrameworks>{tfm}</TargetFrameworks>" : $"<targetFramework>{tfm}</targetFramework>")
            .PatchCodeWithReplace("$AssertValue$", testSucceeded.ToString().ToLowerInvariant())
            .PatchCodeWithReplace("$MicrosoftTestingPlatformVersion$", MicrosoftTestingPlatformVersion));
        string testResultFolder = Path.Combine(testAsset.TargetAssetPath, Guid.NewGuid().ToString("N"));

        DotnetMuxerResult compilationResult = testCommand.StartsWith("test", StringComparison.OrdinalIgnoreCase)
            ? await DotnetCli.RunAsync(
                $"{testCommand} -p:Configuration={compilationMode} \"{testAsset.TargetAssetPath}\" -- --treenode-filter <whatever> --results-directory \"{testResultFolder}\"",
                workingDirectory: testAsset.TargetAssetPath, cancellationToken: TestContext.CancellationToken)
            : await DotnetCli.RunAsync(
                $"{testCommand} -p:TestingPlatformCommandLineArguments=\"--treenode-filter <whatever> --results-directory \"{testResultFolder}\"\" -p:Configuration={compilationMode} \"{testAsset.TargetAssetPath}\"",
                workingDirectory: testAsset.TargetAssetPath, cancellationToken: TestContext.CancellationToken);

        foreach (string tfmToAssert in tfmsToAssert)
        {
            CommonAssert(compilationResult, tfmToAssert, testSucceeded, testResultFolder);
        }
    }

    [TestMethod]
    [OSCondition(OperatingSystems.Windows)]
    public async Task RunUsingTestTargetWithNetfxMSBuild()
    {
        using TestAsset testAsset = await TestAsset.GenerateAssetAsync(
            AssetName,
            SourceCode
            .PatchCodeWithReplace("$PlatformTarget$", string.Empty)
            .PatchCodeWithReplace("$TargetFrameworks$", $"<targetFramework>{TargetFrameworks.NetCurrent}</targetFramework>")
            .PatchCodeWithReplace("$AssertValue$", bool.TrueString.ToLowerInvariant())
            .PatchCodeWithReplace("$MicrosoftTestingPlatformVersion$", MicrosoftTestingPlatformVersion));

        string msbuildExe = await FindMsbuildWithVsWhereAsync(TestContext.CancellationToken);
        var commandLine = new TestInfrastructure.CommandLine();
        string binlogFile = Path.Combine(TempDirectory.TestSuiteDirectory, $"{nameof(RunUsingTestTargetWithNetfxMSBuild)}.binlog");
        await commandLine.RunAsync($"\"{msbuildExe}\" {testAsset.TargetAssetPath} /t:Restore", cancellationToken: TestContext.CancellationToken);
        await commandLine.RunAsync(
            $"\"{msbuildExe}\" {testAsset.TargetAssetPath} /t:\"Build;Test\" /bl:\"{binlogFile}\"",
            environmentVariables: new Dictionary<string, string?>
            {
                ["DOTNET_ROOT"] = Path.Combine(RootFinder.Find(), ".dotnet"),
            },
            cancellationToken: TestContext.CancellationToken);
        Assert.Contains("Tests succeeded", commandLine.StandardOutput);
    }

    [TestMethod]
    [OSCondition(OperatingSystems.Windows)]
    public async Task Invoke_TestTarget_With_Arch_Switch_x86_Should_Work()
    {
        string root = RootFinder.Find();
        string x86Muxer = Path.Combine(root, ".dotnet", "x86");
        var dotnetRootX86 = new Dictionary<string, string?>
        {
            { "DOTNET_ROOT_X86", x86Muxer },
        };

        using TestAsset testAsset = await TestAsset.GenerateAssetAsync(
            AssetName,
            SourceCode
            .PatchCodeWithReplace("$PlatformTarget$", string.Empty)
            .PatchCodeWithReplace("$TargetFrameworks$", $"<TargetFrameworks>{TargetFrameworks.NetCurrent}</TargetFrameworks>")
            .PatchCodeWithReplace("$AssertValue$", bool.TrueString.ToLowerInvariant())
            .PatchCodeWithReplace("$MicrosoftTestingPlatformVersion$", MicrosoftTestingPlatformVersion));
        await DotnetCli.RunAsync(
            $"build -t:Test --arch x86 -p:Configuration=Release \"{testAsset.TargetAssetPath}\"",
            workingDirectory: testAsset.TargetAssetPath,
            environmentVariables: dotnetRootX86,
            failIfReturnValueIsNotZero: false,
            cancellationToken: TestContext.CancellationToken);

        string[] outputLogFiles = Directory.GetFiles(testAsset.TargetAssetPath, $"MSBuild Tests_{TargetFrameworks.NetCurrent}_x86.log", SearchOption.AllDirectories);
        Assert.ContainsSingle(outputLogFiles, $"Was expecting to find a single log file but found {outputLogFiles.Length}");
        string logFileContent = File.ReadAllText(outputLogFiles[0]);
        Assert.IsTrue(Regex.IsMatch(logFileContent, ".*win-x86.*"), logFileContent);

        // This is the architecture part that's written by TerminalOutputDevice when there is no banner specified.
        Assert.Contains($"[win-x86 - {TargetFrameworks.NetCurrent}]", logFileContent);
    }

    [TestMethod]
    public async Task Invoke_TestTarget_With_Incompatible_Arch()
    {
        // TODO: Test with both old and new dotnet test experience.
        Architecture currentArchitecture = RuntimeInformation.ProcessArchitecture;
        string incompatibleArchitecture = currentArchitecture switch
        {
            Architecture.X86 or Architecture.X64 => "arm64",
            _ => "x64",
        };

        using TestAsset testAsset = await TestAsset.GenerateAssetAsync(
            AssetName,
            SourceCode
            .PatchCodeWithReplace("$PlatformTarget$", string.Empty)
            .PatchCodeWithReplace("$TargetFrameworks$", $"<targetFramework>{TargetFrameworks.NetCurrent}</targetFramework>")
            .PatchCodeWithReplace("$AssertValue$", bool.TrueString.ToLowerInvariant())
            .PatchCodeWithReplace("$MicrosoftTestingPlatformVersion$", MicrosoftTestingPlatformVersion));
        DotnetMuxerResult result = await DotnetCli.RunAsync(
            $"build -t:Test --arch {incompatibleArchitecture} \"{testAsset.TargetAssetPath}\"",
            workingDirectory: testAsset.TargetAssetPath,
            failIfReturnValueIsNotZero: false,
            cancellationToken: TestContext.CancellationToken);

        // The build should fail and the failure should mention the incompatible target architecture
        // so we know the build/launch reached the apphost rather than failing for an unrelated reason.
        result.AssertExitCodeIsNot(0);
        result.AssertOutputContains($"[{TargetFrameworks.NetCurrent}|{incompatibleArchitecture}]");

        if (OperatingSystem.IsLinux())
        {
            // On Linux, the apphost launch failure can surface in two different ways depending on
            // whether the kernel/runtime path rejects the binary up-front or falls back to a shell:
            //   1) ToolTask catches a Win32Exception ("Exec format error") and emits MSB6003.
            //   2) The apphost falls back to /bin/sh which then fails to parse the binary, and
            //      InvokeTestingPlatformTask.HandleTaskExecutionErrors reports the captured tool
            //      output via "error run failed: Tests failed:" (often a shell-parse error like
            //      "Syntax error: word unexpected").
            // Either shape is acceptable — both prove we attempted to launch the incompatible-arch
            // apphost and failed.
            bool hasMsb6003 = result.StandardOutput.Contains("error MSB6003:", StringComparison.Ordinal)
                && result.StandardOutput.Contains("Exec format error", StringComparison.Ordinal);
            bool hasRunFailed = result.StandardOutput.Contains("error run failed: Tests failed:", StringComparison.Ordinal);
            Assert.IsTrue(
                hasMsb6003 || hasRunFailed,
                $"Expected output to contain either MSB6003+\"Exec format error\" or \"error run failed: Tests failed:\". Actual output: {result.StandardOutput}");
        }
        else
        {
            // On Windows we run the exe directly; on macOS we run the apphost.
            string executableName = OperatingSystem.IsWindows() ? "MSBuild Tests.exe" : "MSBuild Tests";

            result.AssertOutputContains($"error MSB6003: The specified task executable \"{executableName}\" could not be run. System.ComponentModel.Win32Exception");
            result.AssertOutputContains("An error occurred trying to start process");

            if (OperatingSystem.IsWindows())
            {
                // Launching an incompatible-architecture apphost can surface with different native
                // error text depending on the exact Windows build/runtime path:
                //   1) CreateProcess fails with ERROR_BAD_EXE_FORMAT / ERROR_EXE_MACHINE_TYPE_MISMATCH
                //      and .NET wraps it with the friendly "not a valid application for this OS platform".
                //   2) CreateProcess fails but GetLastError() reports 0, so the raw Win32Exception message
                //      is "The operation completed successfully." (HResult 0x80004005).
                // Both prove we attempted to launch the incompatible-arch apphost and failed. The MSB6003 +
                // "An error occurred trying to start process" assertions above already establish that.
                bool hasInvalidApplication = result.StandardOutput.Contains("The specified executable is not a valid application for this OS platform.", StringComparison.Ordinal);
                bool hasOperationCompleted = result.StandardOutput.Contains("The operation completed successfully.", StringComparison.Ordinal);
                Assert.IsTrue(
                    hasInvalidApplication || hasOperationCompleted,
                    $"Expected output to contain either \"The specified executable is not a valid application for this OS platform.\" or \"The operation completed successfully.\". Actual output: {result.StandardOutput}");
            }
            else if (OperatingSystem.IsMacOS())
            {
                result.AssertOutputContains("Bad CPU type in executable");
            }
            else
            {
                // Unexpected OS.
                throw ApplicationStateGuard.Unreachable();
            }
        }
    }

    [TestMethod]
    [OSCondition(OperatingSystems.Windows)]
    public async Task Invoke_TestTarget_With_DOTNET_HOST_PATH_Should_Work()
    {
        string root = RootFinder.Find();
        string dotnetHostPath = Path.Combine(root, ".dotnet", "dotnet.exe");
        var dotnetHostPathEnvVar = new Dictionary<string, string?>
        {
            { "DOTNET_HOST_PATH", dotnetHostPath },
        };

        using TestAsset testAsset = await TestAsset.GenerateAssetAsync(
            AssetName,
            SourceCode
            .PatchCodeWithReplace("$PlatformTarget$", string.Empty)
            .PatchCodeWithReplace("$TargetFrameworks$", $"<TargetFrameworks>{TargetFrameworks.NetCurrent}</TargetFrameworks>")
            .PatchCodeWithReplace("$AssertValue$", bool.TrueString.ToLowerInvariant())
            .PatchCodeWithReplace("$MicrosoftTestingPlatformVersion$", MicrosoftTestingPlatformVersion));
        await DotnetCli.RunAsync(
            $"build -t:Test -p:Configuration=Release \"{testAsset.TargetAssetPath}\"",
            workingDirectory: testAsset.TargetAssetPath,
            environmentVariables: dotnetHostPathEnvVar,
            failIfReturnValueIsNotZero: false,
            cancellationToken: TestContext.CancellationToken);

        string[] outputLogFiles = Directory.GetFiles(testAsset.TargetAssetPath, $"MSBuild Tests_{TargetFrameworks.NetCurrent}_x64.log", SearchOption.AllDirectories);
        Assert.ContainsSingle(outputLogFiles, $"Was expecting to find a single log file but found {outputLogFiles.Length}");
        string logFileContent = File.ReadAllText(outputLogFiles[0]);
        // This is the architecture part that's written by TerminalOutputDevice when there is no banner specified.
        Assert.Contains($"[win-x64 - {TargetFrameworks.NetCurrent}]", logFileContent);
    }

    private static void CommonAssert(DotnetMuxerResult compilationResult, string tfm, bool testSucceeded, string testResultFolder)
    {
        Assert.IsTrue(Regex.IsMatch(compilationResult.StandardOutput, $".*Run tests:.* \\[{tfm}|x64\\]"), compilationResult.StandardOutput);
        if (testSucceeded)
        {
            Assert.IsTrue(Regex.IsMatch(compilationResult.StandardOutput, $"Tests succeeded:.* \\[{tfm}|x64\\]"), compilationResult.StandardOutput);
        }
        else
        {
            Assert.IsTrue(Regex.IsMatch(compilationResult.StandardOutput, $".*error : Tests failed:.* \\[{tfm}|x64\\]"), compilationResult.StandardOutput);
        }

        // Regression test for https://github.com/microsoft/testfx/issues/8437: the MSBuild
        // extension must keep reporting a non-empty Duration in its run summary even after
        // dropping its dependency on the internal TestRequestExecutionTimeInfo message.
        Assert.IsTrue(
            Regex.IsMatch(compilationResult.StandardOutput, @"Failed: \d+, Passed: \d+, Skipped: \d+, Total: \d+, Duration: \S+"),
            compilationResult.StandardOutput);

        string outputFileLog = Path.Combine(testResultFolder, $"MSBuild Tests_{tfm}_x64.log");
        Assert.IsTrue(File.Exists(outputFileLog), $"Expected file '{outputFileLog}'");
        Assert.IsFalse(string.IsNullOrEmpty(File.ReadAllText(outputFileLog)), $"Content of file '{File.ReadAllText(outputFileLog)}'");
    }

    // We avoid to test the multi-tfm because it's already tested with the above tests and we don't want to have too heavy testing,
    // msbuild is pretty heavy (a lot of processes started due to the no 'nodereuse') and makes tests flaky.
    // We test two functionality for the same reason, we don't want to load too much the CI only for UX reasons.
    [DynamicData(nameof(GetBuildMatrix), DynamicDataDisplayName = nameof(FormatBuildMatrixEntry))]
    [TestMethod]
    public async Task InvokeTestingPlatform_Target_Showing_Error_And_Do_Not_Capture_The_Output_SingleTfm(string testCommand, string tfm, BuildConfiguration compilationMode, bool testSucceeded)
    {
        // We test only failed but we don't want to have too much argument provider overload.
        if (testSucceeded)
        {
            return;
        }

        using TestAsset testAsset = await TestAsset.GenerateAssetAsync(
            AssetName,
            SourceCode
            .PatchCodeWithReplace("$PlatformTarget$", "<PlatformTarget>x64</PlatformTarget>")
            .PatchCodeWithReplace("$TargetFrameworks$", $"<targetFramework>{tfm}</targetFramework>")
            .PatchCodeWithReplace("$AssertValue$", testSucceeded.ToString().ToLowerInvariant())
            .PatchCodeWithReplace("$MicrosoftTestingPlatformVersion$", MicrosoftTestingPlatformVersion));
        DotnetMuxerResult compilationResult = await DotnetCli.RunAsync($"{testCommand} -p:TestingPlatformShowTestsFailure=True -p:TestingPlatformCaptureOutput=False -p:Configuration={compilationMode} {testAsset.TargetAssetPath}", workingDirectory: testAsset.TargetAssetPath, failIfReturnValueIsNotZero: false, cancellationToken: TestContext.CancellationToken);

        compilationResult.AssertOutputContains("error test failed: Test2 (");
        compilationResult.AssertOutputContains("FAILED: Expected 'true', but got 'false'.");
        compilationResult.AssertOutputContains("Microsoft.Testing.Platform");
    }

    [TestMethod]
    public async Task TestingPlatformDisableCustomTestTarget_Should_Cause_UserDefined_Target_To_Run()
    {
        using TestAsset testAsset = await TestAsset.GenerateAssetAsync(
            AssetName,
            SourceCode
            .PatchCodeWithReplace("$PlatformTarget$", "<PlatformTarget>x64</PlatformTarget>")
            .PatchCodeWithReplace("$TargetFrameworks$", $"<targetFramework>{TargetFrameworks.NetCurrent}</targetFramework>")
            .PatchCodeWithReplace("$AssertValue$", "true")
            .PatchCodeWithReplace("$MicrosoftTestingPlatformVersion$", MicrosoftTestingPlatformVersion));
        DotnetMuxerResult compilationResult = await DotnetCli.RunAsync($"build {testAsset.TargetAssetPath} -p:TestingPlatformDisableCustomTestTarget=true -p:ImportUserDefinedTestTarget=true -t:\"Build;Test\"", failIfReturnValueIsNotZero: false, cancellationToken: TestContext.CancellationToken);

        compilationResult.AssertOutputContains("Error from UserDefinedTestTarget.targets");
    }

    [TestMethod]
    public async Task InvokeTestingPlatform_Target_Should_Pass_EnvironmentVariables_To_TestProcess()
    {
        using TestAsset testAsset = await TestAsset.GenerateAssetAsync(
            AssetName,
            SourceCode
            .PatchCodeWithReplace("$PlatformTarget$", "<PlatformTarget>x64</PlatformTarget>")
            .PatchCodeWithReplace("$TargetFrameworks$", $"<targetFramework>{TargetFrameworks.NetCurrent}</targetFramework>")
            .PatchCodeWithReplace("$AssertValue$", "true")
            .PatchCodeWithReplace("$MicrosoftTestingPlatformVersion$", MicrosoftTestingPlatformVersion));

        // TestingPlatformCaptureOutput=False routes the test process stdout through the MSBuild log so the values the
        // test process actually observed can be asserted on.
        DotnetMuxerResult compilationResult = await DotnetCli.RunAsync(
            $"build -t:Test -p:TestingPlatformCaptureOutput=False -p:AddTestingPlatformEnvironmentVariableItem=true \"{testAsset.TargetAssetPath}\"",
            workingDirectory: testAsset.TargetAssetPath,
            environmentVariables: new Dictionary<string, string?>
            {
                // Deliberately NOT declared as a TestingPlatformEnvironmentVariable item: it only reaches the test
                // process by inheritance from the MSBuild process.
                ["MTP_ENV_INHERITED"] = "inherited-value",
            },
            cancellationToken: TestContext.CancellationToken);

        compilationResult.AssertOutputContains("[env] MTP_ENV_SIMPLE=simple-value");

        // The value is carried by item metadata, which MSBuild does not split on ';', so it must arrive intact.
        compilationResult.AssertOutputContains("[env] MTP_ENV_WITH_SEMICOLON=first;second");

        // ToolTask layers EnvironmentVariables on top of the inherited environment rather than replacing it. This is
        // the load-bearing half of the feature: if it ever became replace semantics, declaring a single variable would
        // strip PATH / DOTNET_ROOT / ... from every test process, so pin it here alongside the declared values.
        compilationResult.AssertOutputContains("[env] MTP_ENV_INHERITED=inherited-value");
    }

    [TestMethod]
    public async Task InvokeTestingPlatform_Target_Should_Set_DotnetRoot_For_AppHost()
    {
        string architecture = RuntimeInformation.ProcessArchitecture.ToString();
        string dotnetRootVariableName = $"DOTNET_ROOT_{architecture.ToUpperInvariant()}";
        string dotnetRoot = Path.Combine(RootFinder.Find(), ".dotnet");

        using TestAsset testAsset = await TestAsset.GenerateAssetAsync(
            AssetName,
            SourceCode
            .PatchCodeWithReplace("$PlatformTarget$", $"<PlatformTarget>{architecture.ToLowerInvariant()}</PlatformTarget>")
            .PatchCodeWithReplace("$TargetFrameworks$", $"<targetFramework>{TargetFrameworks.NetCurrent}</targetFramework>")
            .PatchCodeWithReplace("$AssertValue$", "true")
            .PatchCodeWithReplace("$MicrosoftTestingPlatformVersion$", MicrosoftTestingPlatformVersion));

        Dictionary<string, string?> CreateEnvironmentVariables()
            => new()
            {
                [dotnetRootVariableName] = null,
            };

        DotnetMuxerResult derivedResult = await DotnetCli.RunAsync(
            $"build -t:Test -p:TestingPlatformCaptureOutput=False \"{testAsset.TargetAssetPath}\"",
            workingDirectory: testAsset.TargetAssetPath,
            environmentVariables: CreateEnvironmentVariables(),
            cancellationToken: TestContext.CancellationToken);

        derivedResult.AssertOutputContains($"[env] {dotnetRootVariableName}={dotnetRoot}");

        DotnetMuxerResult explicitResult = await DotnetCli.RunAsync(
            $"build -t:Test -p:TestingPlatformCaptureOutput=False -p:ExplicitDotnetRoot=\"{dotnetRoot}\" \"{testAsset.TargetAssetPath}\"",
            workingDirectory: testAsset.TargetAssetPath,
            environmentVariables: new Dictionary<string, string?>
            {
                // An inherited architecture-specific root has higher apphost precedence than DOTNET_ROOT. Use an
                // existing directory without a dotnet installation to prove the explicit item clears this override.
                [dotnetRootVariableName] = Path.GetTempPath(),
            },
            cancellationToken: TestContext.CancellationToken);

        explicitResult.AssertOutputContains($"[env] DOTNET_ROOT={dotnetRoot}");
        explicitResult.AssertOutputContains($"[env] {dotnetRootVariableName}=");

        DotnetMuxerResult disabledResult = await DotnetCli.RunAsync(
            $"build -t:Test -p:TestingPlatformCaptureOutput=False -p:TestingPlatformDisableAppHostDotnetRoot=true \"{testAsset.TargetAssetPath}\"",
            workingDirectory: testAsset.TargetAssetPath,
            environmentVariables: CreateEnvironmentVariables(),
            cancellationToken: TestContext.CancellationToken);

        disabledResult.AssertOutputContains($"[env] {dotnetRootVariableName}=");
    }

    private const string SourceCode = """
#file MSBuild Tests.csproj
<Project Sdk="Microsoft.NET.Sdk">
    <PropertyGroup>
        $TargetFrameworks$
        $PlatformTarget$
        <ImplicitUsings>enable</ImplicitUsings>
        <Nullable>enable</Nullable>
        <OutputType>Exe</OutputType>
        <UseAppHost>true</UseAppHost>
        <LangVersion>preview</LangVersion>
        <GenerateTestingPlatformEntryPoint>false</GenerateTestingPlatformEntryPoint>
        <RootNamespace>MSBuildTests</RootNamespace>
    </PropertyGroup>
    <ItemGroup>
        <PackageReference Include="Microsoft.Testing.Platform.MSBuild" Version="$MicrosoftTestingPlatformVersion$" />
        <PackageReference Include="Microsoft.Testing.Platform" Version="$MicrosoftTestingPlatformVersion$" />
    </ItemGroup>

    <ItemGroup Condition="'$(AddTestingPlatformEnvironmentVariableItem)' == 'true'">
        <TestingPlatformEnvironmentVariable Include="MTP_ENV_SIMPLE" Value="simple-value" />
        <!-- The value intentionally contains a ';' to prove item metadata is not subject to MSBuild's ';' splitting. -->
        <TestingPlatformEnvironmentVariable Include="MTP_ENV_WITH_SEMICOLON" Value="first;second" />
    </ItemGroup>

    <ItemGroup>
        <TestingPlatformEnvironmentVariable Include="DOTNET_ROOT" Value="$(ExplicitDotnetRoot)" Condition="'$(ExplicitDotnetRoot)' != ''" />
    </ItemGroup>

    <Import Project="UserDefinedTestTarget.targets" Condition="'$(ImportUserDefinedTestTarget)' == 'true'" />
</Project>

#file UserDefinedTestTarget.targets
<Project>
    <Target Name="Test">
        <Error Text="Error from UserDefinedTestTarget.targets" />
    </Target>
</Project>

#file global.json
{
  "test": {
    "runner": "VSTest"
  }
}

#file Program.cs
using Microsoft.Testing.Platform.Builder;
using Microsoft.Testing.Platform.Capabilities.TestFramework;
using Microsoft.Testing.Platform.Extensions;
using Microsoft.Testing.Platform.Extensions.Messages;
using Microsoft.Testing.Platform.Extensions.TestFramework;
using Microsoft.Testing.Platform.Helpers;
using Microsoft.Testing.Platform.MSBuild;
using Microsoft.Testing.Platform.Services;
using System.Runtime.InteropServices;

public class Program
{
    public static async Task<int> Main(string[] args)
    {
        string dotnetRootArchVariableName = $"DOTNET_ROOT_{RuntimeInformation.ProcessArchitecture.ToString().ToUpperInvariant()}";
        foreach (string name in new[] { "MTP_ENV_SIMPLE", "MTP_ENV_WITH_SEMICOLON", "MTP_ENV_INHERITED", "DOTNET_ROOT", dotnetRootArchVariableName })
        {
            Console.WriteLine($"[env] {name}={Environment.GetEnvironmentVariable(name)}");
        }

        ITestApplicationBuilder builder = await TestApplication.CreateBuilderAsync(args);
        MyExtension myExtension = new();
        builder.RegisterTestFramework(
            sp => new TestFrameworkCapabilities(),
            (_,sp) => new DummyTestFramework(sp, myExtension));
        builder.AddTreeNodeFilterService(myExtension);
        builder.AddMSBuild();
        using ITestApplication app = await builder.BuildAsync();
        return await app.RunAsync();
    }
}

public class MyExtension : IExtension
{
    public string Uid => "MyExtension";
    public string Version => "1.0.0";
    public string DisplayName => "My Extension";
    public string Description => "My Extension Description";
    public Task<bool> IsEnabledAsync() => Task.FromResult(true);
}

public class DummyTestFramework : ITestFramework, IDataProducer
{
    private IServiceProvider _sp;
    private MyExtension _myExtension;

    public DummyTestFramework(IServiceProvider sp, MyExtension myExtension)
    {
        _sp = sp;
        _myExtension = myExtension;
    }

    public string Uid => _myExtension.Uid;

    public string Version => _myExtension.Version;

    public string DisplayName => _myExtension.DisplayName;

    public string Description => _myExtension.Description;

    public Type[] DataTypesProduced => [typeof(TestNodeUpdateMessage)];

    public Task<bool> IsEnabledAsync() => _myExtension.IsEnabledAsync();

    public Task<CreateTestSessionResult> CreateTestSessionAsync(CreateTestSessionContext context)
        => Task.FromResult(new CreateTestSessionResult() { IsSuccess = true });

    public Task<CloseTestSessionResult> CloseTestSessionAsync(CloseTestSessionContext context)
        => Task.FromResult(new CloseTestSessionResult() { IsSuccess = true });

    public async Task ExecuteRequestAsync(ExecuteRequestContext context)
    {
        await context.MessageBus.PublishAsync(this, new TestNodeUpdateMessage(context.Request.Session.SessionUid,
            new TestNode { Uid = "1", DisplayName = "Test1", Properties = new(DiscoveredTestNodeStateProperty.CachedInstance) }));

        await context.MessageBus.PublishAsync(this, new TestNodeUpdateMessage(context.Request.Session.SessionUid,
            new TestNode { Uid = "1", DisplayName = "Test1", Properties = new(PassedTestNodeStateProperty.CachedInstance) }));

        if (!_sp.GetCommandLineOptions().TryGetOptionArgumentList("--treenode-filter", out _))
        {
            await context.MessageBus.PublishAsync(this, new TestNodeUpdateMessage(context.Request.Session.SessionUid,
                new TestNode { Uid = "2", DisplayName = "Test2", Properties = new(DiscoveredTestNodeStateProperty.CachedInstance) }));

            await context.MessageBus.PublishAsync(this, new TestNodeUpdateMessage(context.Request.Session.SessionUid,
                new TestNode { Uid = "2", DisplayName = "Test2", Properties = new($AssertValue$ ? PassedTestNodeStateProperty.CachedInstance : new FailedTestNodeStateProperty("FAILED: Expected 'true', but got 'false'.")) }));
        }

       context.Complete();
    }
}
""";

    public TestContext TestContext { get; set; }
}
