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

using Combinatorial.MSTest;

using SL = Microsoft.Build.Logging.StructuredLogger;

namespace Microsoft.Testing.Platform.Acceptance.IntegrationTests;

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

    [CombinatorialData]
    [TestMethod]
    public async Task Microsoft_Testing_Platform_Extensions_ShouldBe_Correctly_Registered([AllTargetFrameworks] string tfm, BuildConfiguration compilationMode, Verb verb)
    {
        using TestAsset testAsset = await TestAsset.GenerateAssetAsync(
            nameof(Microsoft_Testing_Platform_Extensions_ShouldBe_Correctly_Registered),
            SourceCode
            .PatchCodeWithReplace("$TargetFrameworks$", tfm)
            .PatchCodeWithReplace("$MicrosoftTestingPlatformVersion$", MicrosoftTestingPlatformVersion)
            .PatchCodeWithReplace("$MicrosoftTestingExtensionsCtrfReportVersion$", MicrosoftTestingExtensionsCtrfReportVersion)
            .PatchCodeWithReplace("$MicrosoftTestingExtensionsGitHubActionsReportVersion$", MicrosoftTestingExtensionsGitHubActionsReportVersion)
            .PatchCodeWithReplace("$MicrosoftTestingExtensionsJUnitReportVersion$", MicrosoftTestingExtensionsJUnitReportVersion)
            .PatchCodeWithReplace("$MicrosoftTestingExtensionsVideoRecorderVersion$", MicrosoftTestingExtensionsVideoRecorderVersion));
        DotnetMuxerResult result = await DotnetCli.RunAsync($"{(verb == Verb.publish ? $"publish -f {tfm}" : "build")}  -c {compilationMode} -r {RID} {testAsset.TargetAssetPath} -v:n", cancellationToken: TestContext.CancellationToken);
        string binlogFile = result.BinlogPath!;

        var testHost = TestInfrastructure.TestHost.LocateFrom(testAsset.TargetAssetPath, AssetName, tfm, rid: RID, verb: verb, buildConfiguration: compilationMode);
        TestHostResult testHostResult = await testHost.ExecuteAsync("--help", cancellationToken: TestContext.CancellationToken);
        testHostResult.AssertOutputContains("--crashdump");
        testHostResult.AssertOutputContains("--hangdump");
        testHostResult.AssertOutputContains("--publish-azdo-run-name");
        testHostResult.AssertOutputContains("--publish-azdo-test-results");
        testHostResult.AssertOutputContains("--report-ctrf");
        testHostResult.AssertOutputContains("--report-gh");
        testHostResult.AssertOutputContains("--report-html");
        testHostResult.AssertOutputContains("--report-junit");
        testHostResult.AssertOutputContains("--report-trx");
        testHostResult.AssertOutputContains("--retry-failed-tests");
        testHostResult.AssertOutputContains("--capture-video");

        SL.Build binLog = SL.Serialization.Read(binlogFile);
        SL.Target generateSelfRegisteredExtensions = binLog.FindChildrenRecursive<SL.Target>().Single(t => t.Name == "_GenerateSelfRegisteredExtensions");
        SL.Task testingPlatformSelfRegisteredExtensions = generateSelfRegisteredExtensions.FindChildrenRecursive<SL.Task>().Single(t => t.Name == "TestingPlatformSelfRegisteredExtensions");
        SL.Message generatedSource = testingPlatformSelfRegisteredExtensions.FindChildrenRecursive<SL.Message>().Single(m => m.Text.Contains("SelfRegisteredExtensions source:"));

        Assert.Contains("Microsoft.Testing.Extensions.AzureDevOpsReport.TestingPlatformBuilderHook.AddExtensions", generatedSource.Text, generatedSource.Text);
        Assert.Contains("Microsoft.Testing.Extensions.CrashDump.TestingPlatformBuilderHook.AddExtensions", generatedSource.Text, generatedSource.Text);
        Assert.Contains("Microsoft.Testing.Extensions.CtrfReport.TestingPlatformBuilderHook.AddExtensions", generatedSource.Text, generatedSource.Text);
        Assert.Contains("Microsoft.Testing.Extensions.GitHubActionsReport.TestingPlatformBuilderHook.AddExtensions", generatedSource.Text, generatedSource.Text);
        Assert.Contains("Microsoft.Testing.Extensions.HangDump.TestingPlatformBuilderHook.AddExtensions", generatedSource.Text, generatedSource.Text);
        Assert.Contains("Microsoft.Testing.Extensions.HotReload.TestingPlatformBuilderHook.AddExtensions", generatedSource.Text, generatedSource.Text);
        Assert.Contains("Microsoft.Testing.Extensions.HtmlReport.TestingPlatformBuilderHook.AddExtensions", generatedSource.Text, generatedSource.Text);
        Assert.Contains("Microsoft.Testing.Extensions.JUnitReport.TestingPlatformBuilderHook.AddExtensions", generatedSource.Text, generatedSource.Text);
        Assert.Contains("Microsoft.Testing.Extensions.Retry.TestingPlatformBuilderHook.AddExtensions", generatedSource.Text, generatedSource.Text);
        Assert.Contains("Microsoft.Testing.Extensions.Telemetry.TestingPlatformBuilderHook.AddExtensions", generatedSource.Text, generatedSource.Text);
        Assert.Contains("Microsoft.Testing.Extensions.TrxReport.TestingPlatformBuilderHook.AddExtensions", generatedSource.Text, generatedSource.Text);
        Assert.Contains("Microsoft.Testing.Extensions.VideoRecorder.TestingPlatformBuilderHook.AddExtensions", generatedSource.Text, generatedSource.Text);
    }

    [TestMethod]
    public async Task TestingPlatformBuilderHook_With_Conflicting_Metadata_Fails_Build()
    {
        string tfm = TargetFrameworks.NetCurrent;
        using TestAsset testAsset = await TestAsset.GenerateAssetAsync(
            nameof(TestingPlatformBuilderHook_With_Conflicting_Metadata_Fails_Build),
            ConflictingHookSourceCode
            .PatchCodeWithReplace("$TargetFrameworks$", tfm)
            .PatchCodeWithReplace("$MicrosoftTestingPlatformVersion$", MicrosoftTestingPlatformVersion));

        DotnetMuxerResult result = await DotnetCli.RunAsync(
            $"build -c {BuildConfiguration.Release} -r {RID} {testAsset.TargetAssetPath} -v:n",
            failIfReturnValueIsNotZero: false,
            cancellationToken: TestContext.CancellationToken);

        result.AssertExitCodeIsNot(0);
        result.AssertOutputContains("Duplicate 'TestingPlatformBuilderHook' item with Include 'CONFLICT-HOOK-ID' has conflicting metadata.");

        // Ensure no self-registered extensions source file was generated when validation failed.
        SL.Build binLog = SL.Serialization.Read(result.BinlogPath!);
        SL.Target? generateSelfRegisteredExtensions = binLog.FindChildrenRecursive<SL.Target>().SingleOrDefault(t => t.Name == "_GenerateSelfRegisteredExtensions");
        Assert.IsNotNull(generateSelfRegisteredExtensions);
        SL.Task testingPlatformSelfRegisteredExtensions = generateSelfRegisteredExtensions.FindChildrenRecursive<SL.Task>().Single(t => t.Name == "TestingPlatformSelfRegisteredExtensions");
        Assert.IsEmpty(testingPlatformSelfRegisteredExtensions.FindChildrenRecursive<SL.Message>().Where(m => m.Text.Contains("SelfRegisteredExtensions source:")));
    }

    private const string SourceCode = """
#file MSBuildTests.csproj
<Project Sdk="Microsoft.NET.Sdk">

    <ItemGroup>
      <TestingPlatformBuilderHook Include="A" >
        <DisplayName>DummyTestFramework</DisplayName>
        <TypeFullName>MyNamespaceRoot.Level1.Level2.DummyTestFrameworkRegistration</TypeFullName>
      </TestingPlatformBuilderHook>
    </ItemGroup>

    <PropertyGroup>
        <TargetFramework>$TargetFrameworks$</TargetFramework>
        <ImplicitUsings>enable</ImplicitUsings>
        <Nullable>enable</Nullable>
        <LangVersion>preview</LangVersion>
        <OutputType>Exe</OutputType>
        <!-- Do not warn about package downgrade. NuGet uses alphabetical sort as ordering so -dev or -ci are considered downgrades of -preview. -->
        <NoWarn>$(NoWarn);NETSDK1201</NoWarn>

        <RootNamespace>(MSBuild Tests)</RootNamespace>
    </PropertyGroup>

    <ItemGroup>
        <PackageReference Include="Microsoft.Testing.Platform.MSBuild" Version="$MicrosoftTestingPlatformVersion$" />
        <PackageReference Include="Microsoft.Testing.Extensions.AzureDevOpsReport" Version="$MicrosoftTestingPlatformVersion$" />
        <PackageReference Include="Microsoft.Testing.Extensions.CrashDump" Version="$MicrosoftTestingPlatformVersion$" />
        <PackageReference Include="Microsoft.Testing.Extensions.CtrfReport" Version="$MicrosoftTestingExtensionsCtrfReportVersion$" />
        <PackageReference Include="Microsoft.Testing.Extensions.GitHubActionsReport" Version="$MicrosoftTestingExtensionsGitHubActionsReportVersion$" />
        <PackageReference Include="Microsoft.Testing.Extensions.HangDump" Version="$MicrosoftTestingPlatformVersion$" />
        <PackageReference Include="Microsoft.Testing.Extensions.HotReload" Version="$MicrosoftTestingPlatformVersion$" />
        <PackageReference Include="Microsoft.Testing.Extensions.HtmlReport" Version="$MicrosoftTestingPlatformVersion$" />
        <PackageReference Include="Microsoft.Testing.Extensions.JUnitReport" Version="$MicrosoftTestingExtensionsJUnitReportVersion$" />
        <PackageReference Include="Microsoft.Testing.Extensions.Retry" Version="$MicrosoftTestingPlatformVersion$" />
        <PackageReference Include="Microsoft.Testing.Extensions.Telemetry" Version="$MicrosoftTestingPlatformVersion$" />
        <PackageReference Include="Microsoft.Testing.Extensions.TrxReport" Version="$MicrosoftTestingPlatformVersion$" />
        <PackageReference Include="Microsoft.Testing.Extensions.VideoRecorder" Version="$MicrosoftTestingExtensionsVideoRecorderVersion$" />
    </ItemGroup>
</Project>

#file Program.cs
using MSBuildTests;
using System.Collections.Generic;
using Microsoft.Testing.Platform.Builder;
using Microsoft.Testing.Platform.Capabilities;
using Microsoft.Testing.Platform.Capabilities.TestFramework;
using Microsoft.Testing.Platform.Extensions.Messages;
using Microsoft.Testing.Platform.Extensions.TestFramework;

namespace MyNamespaceRoot.Level1.Level2;

public static class DummyTestFrameworkRegistration
{
    public static void AddExtensions(ITestApplicationBuilder testApplicationBuilder, string[] args)
    {
        testApplicationBuilder.RegisterTestFramework(_ => new Capabilities(), (_, __) => new DummyTestFramework());
    }
}

internal sealed class DummyTestFramework : ITestFramework, IDataProducer
{
    public string Uid => nameof(DummyTestFramework);

    public string Version => string.Empty;

    public string DisplayName => string.Empty;

    public string Description => string.Empty;

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

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

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

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

    public Task<bool> IsEnabledAsync() => Task.FromResult(true);
}

internal sealed class Capabilities : ITestFrameworkCapabilities
{
    IReadOnlyCollection<ITestFrameworkCapability> ICapabilities<ITestFrameworkCapability>.Capabilities => Array.Empty<ITestFrameworkCapability>();
}

#file DummyClass.cs

namespace MSBuildTests.Microsoft;

// This serves as a test to ensure that we don't regress generation of 'global::'
// If the self registered generation used Microsoft.Testing.Extensions.ExtensionName.TestingPlatformBuilderHook.AddExtension,
// Then, without global::, Microsoft will be referring to MSBuildTests.Microsoft namespace and will fail to compile
public static class DummyClass { }
""";

    private const string ConflictingHookSourceCode = """
#file MSBuildTests.csproj
<Project Sdk="Microsoft.NET.Sdk">

    <ItemGroup>
      <TestingPlatformBuilderHook Include="CONFLICT-HOOK-ID" >
        <DisplayName>FirstHook</DisplayName>
        <TypeFullName>Contoso.FirstHook</TypeFullName>
      </TestingPlatformBuilderHook>
      <TestingPlatformBuilderHook Include="CONFLICT-HOOK-ID" >
        <DisplayName>SecondHook</DisplayName>
        <TypeFullName>Contoso.SecondHook</TypeFullName>
      </TestingPlatformBuilderHook>
    </ItemGroup>

    <PropertyGroup>
        <TargetFramework>$TargetFrameworks$</TargetFramework>
        <ImplicitUsings>enable</ImplicitUsings>
        <Nullable>enable</Nullable>
        <LangVersion>preview</LangVersion>
        <OutputType>Exe</OutputType>
        <!-- Do not warn about package downgrade. NuGet uses alphabetical sort as ordering so -dev or -ci are considered downgrades of -preview. -->
        <NoWarn>$(NoWarn);NETSDK1201</NoWarn>
    </PropertyGroup>

    <ItemGroup>
        <PackageReference Include="Microsoft.Testing.Platform.MSBuild" Version="$MicrosoftTestingPlatformVersion$" />
    </ItemGroup>
</Project>

#file Program.cs
// Empty program. Build should fail before reaching compilation because of the
// duplicate TestingPlatformBuilderHook items with conflicting metadata.
""";

    public TestContext TestContext { get; set; }
}
