// 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]
// Both test methods share the single asset generated by TestAssetFixture, and one of them deploys (that
// is, enumerates and copies) that whole directory while the other writes a marker file into it. The
// assembly runs methods in parallel, so they must not overlap.
[DoNotParallelize]
public sealed class PackagedAppDeploymentTests : AcceptanceTestBase<PackagedAppDeploymentTests.TestAssetFixture>
{
    private const string AssetName = "PackagedAppDeploymentTest";

    // Mirrors PackagedAppTestHostLauncher.LauncherModeEnvironmentVariable, which is internal to the
    // extension. It is part of the extension's documented contract, so asserting on the literal here
    // also guards against renaming it without noticing the behavior change.
    private const string LauncherModeEnvironmentVariable = "TESTINGPLATFORM_PACKAGEDAPP_LAUNCHER";

    [DynamicData(nameof(TargetFrameworks.NetForDynamicData), typeof(TargetFrameworks))]
    [TestMethod]
    [OSCondition(ConditionMode.Include, OperatingSystems.Windows, IgnoreMessage = "Packaged Windows apps (UWP/WinUI) are a Windows-only scenario.")]
    public async Task PackagedAppDeployment_DeploysAndLaunchesTestHost_WithoutLocalPid(string currentTfm)
    {
        var testHost = TestInfrastructure.TestHost.LocateFrom(AssetFixture.TargetAssetPath, AssetName, currentTfm);

        // The deployed test host reports the directory it actually ran from into this file (it learns
        // the path from the PACKAGEDAPP_BASEDIR_MARKER env var, which the platform forwards to the
        // launched host). This keeps the proof of deployment in the test asset rather than the
        // shipping extension.
        string markerPath = Path.Combine(testHost.DirectoryName, "deployment-basedir.txt");

        TestHostResult testHostResult = await testHost.ExecuteAsync(
            environmentVariables: new Dictionary<string, string?>
            {
                ["PACKAGEDAPP_BASEDIR_MARKER"] = markerPath,

                // This asset is a plain (non-packaged) layout, which the launcher leaves alone by
                // default. 'always' is the documented opt-in that requests deploy-and-launch for a
                // loose layout.
                [LauncherModeEnvironmentVariable] = "always",
            },
            cancellationToken: TestContext.CancellationToken);

        // The test host is deployed elsewhere and launched through a handle that exposes no local
        // PID. The run must still complete successfully, proving the platform's launch contract works
        // for "not just a dumb process".
        testHostResult.AssertExitCodeIs(ExitCode.Success);

        Assert.IsTrue(File.Exists(markerPath), $"Expected the deployed host to write its base directory to '{markerPath}'.");
        Assert.AreNotEqual(
            NormalizeDirectory(testHost.DirectoryName),
            ReadMarkedDirectory(markerPath),
            "The test host must have been deployed to and launched from a different directory.");
    }

    [DynamicData(nameof(TargetFrameworks.NetForDynamicData), typeof(TargetFrameworks))]
    [TestMethod]
    [OSCondition(ConditionMode.Include, OperatingSystems.Windows, IgnoreMessage = "The launcher is only ever enabled on Windows, so there is nothing to opt out of elsewhere.")]
    public async Task PackagedAppDeployment_WithNonPackagedLayoutAndNoOptIn_RunsInPlace(string currentTfm)
    {
        var testHost = TestInfrastructure.TestHost.LocateFrom(AssetFixture.TargetAssetPath, AssetName, currentTfm);
        string markerPath = Path.Combine(testHost.DirectoryName, "in-place-basedir.txt");

        // No opt-in and no AppxManifest.xml in the layout: merely referencing the PackagedApp package
        // must not change how the app runs. This is the guard for unpackaged apps (most notably
        // unpackaged WinUI), which would otherwise pay for the test host controller process model and
        // have their whole layout copied to a deployment directory on every run.
        TestHostResult testHostResult = await testHost.ExecuteAsync(
            environmentVariables: new Dictionary<string, string?> { ["PACKAGEDAPP_BASEDIR_MARKER"] = markerPath },
            cancellationToken: TestContext.CancellationToken);

        testHostResult.AssertExitCodeIs(ExitCode.Success);

        Assert.IsTrue(File.Exists(markerPath), $"Expected the test host to write its base directory to '{markerPath}'.");
        Assert.AreEqual(
            NormalizeDirectory(testHost.DirectoryName),
            ReadMarkedDirectory(markerPath),
            "The test host must have run from its own output directory, without being deployed elsewhere.");
    }

    private static string ReadMarkedDirectory(string markerPath)
        => NormalizeDirectory(File.ReadAllText(markerPath).Trim());

    private static string NormalizeDirectory(string directory)
        => directory.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar);

    public sealed class TestAssetFixture() : TestAssetFixtureBase()
    {
        private const string Sources = """
#file PackagedAppDeploymentTest.csproj

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFrameworks>$TargetFrameworks$</TargetFrameworks>
    <OutputType>Exe</OutputType>
    <UseAppHost>true</UseAppHost>
    <Nullable>enable</Nullable>
    <LangVersion>preview</LangVersion>
  </PropertyGroup>
  <ItemGroup>
    <PackageReference Include="Microsoft.Testing.Platform" Version="$MicrosoftTestingPlatformVersion$" />
    <PackageReference Include="Microsoft.Testing.Extensions.PackagedApp" Version="$MicrosoftTestingExtensionsPackagedAppVersion$" />
  </ItemGroup>
</Project>

#file Program.cs
using System;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Testing.Extensions;
using Microsoft.Testing.Platform.Builder;
using Microsoft.Testing.Platform.Capabilities.TestFramework;
using Microsoft.Testing.Platform.Extensions.Messages;
using Microsoft.Testing.Platform.Extensions.TestFramework;
using Microsoft.Testing.Platform.Services;

public class Startup
{
    public static async Task<int> Main(string[] args)
    {
        // The app reports the directory it is actually running from, so the acceptance tests can tell
        // whether it was deployed-and-launched elsewhere or ran in place. When the launcher is opted in,
        // both the controller process (original directory) and the deployed test host (deployment
        // directory) run this Main, and the deployed host is launched later, so it is the last writer.
        // When the launcher stays out of the way there is a single process and the marker points at the
        // app's own output directory. The marker path comes from the platform-forwarded environment.
        string? markerPath = Environment.GetEnvironmentVariable("PACKAGEDAPP_BASEDIR_MARKER");
        if (!string.IsNullOrEmpty(markerPath))
        {
            System.IO.File.WriteAllText(markerPath, AppContext.BaseDirectory);
        }

        var testApplicationBuilder = await TestApplication.CreateBuilderAsync(args);
        testApplicationBuilder.RegisterTestFramework(_ => new TestFrameworkCapabilities(), (_,__) => new DummyTestFramework());
        testApplicationBuilder.AddPackagedAppDeployment();
        using ITestApplication app = await testApplicationBuilder.BuildAsync();
        return await app.RunAsync();
    }
}

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

    public string Version => "2.0.0";

    public string DisplayName => nameof(DummyTestFramework);

    public string Description => nameof(DummyTestFramework);

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

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

    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 = "Test1",
            DisplayName = "Test1",
            Properties = new PropertyBag(new PassedTestNodeStateProperty()),
        }));

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

        public string TargetAssetPath => GetAssetPath(AssetName);

        public override (string ID, string Name, string Code) GetAssetsToGenerate() => (AssetName, AssetName,
                Sources
                .PatchTargetFrameworks(TargetFrameworks.Net)
                .PatchCodeWithReplace("$MicrosoftTestingPlatformVersion$", MicrosoftTestingPlatformVersion)
                .PatchCodeWithReplace("$MicrosoftTestingExtensionsPackagedAppVersion$", MicrosoftTestingExtensionsPackagedAppVersion));
    }

    public TestContext TestContext { get; set; }
}
