// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Text;
using System.Text.RegularExpressions;
using System.Xml;
using Microsoft.Playwright;
using Xunit;
using Xunit.Abstractions;

namespace Aspire.Workload.Tests;

public abstract partial class PerTestFrameworkTemplatesTests : WorkloadTestsBase
{
    private static readonly XmlWriterSettings s_xmlWriterSettings = new() { ConformanceLevel = ConformanceLevel.Fragment };

    [GeneratedRegex(@"^\s*//")]
    private static partial Regex CommentLineRegex();

    // Regex is from src/Aspire.Hosting.AppHost/build/Aspire.Hosting.AppHost.targets - _GeneratedClassNameFixupRegex
    [GeneratedRegex(@"(((?<=\.)|^)(?=\d)|\W)")]
    private static partial Regex GeneratedClassNameFixupRegex();

    private readonly string _testTemplateName;

    public PerTestFrameworkTemplatesTests(string testType, ITestOutputHelper testOutput) : base(testOutput)
    {
        _testTemplateName = testType;
    }

    public static IEnumerable<object[]> ProjectNamesWithTestTemplate_TestData()
    {
        foreach (var name in GetProjectNamesForTest())
        {
            yield return [name];
        }
    }

    [Theory]
    [MemberData(nameof(ProjectNamesWithTestTemplate_TestData))]
    public async Task TemplatesForIndividualTestFrameworks(string prefix)
    {
        var id = $"{prefix}-{_testTemplateName}";
        var config = "Debug";

        await using var project = await AspireProject.CreateNewTemplateProjectAsync(
            id,
            "aspire",
            _testOutput,
            buildEnvironment: BuildEnvironment.ForDefaultFramework);

        await project.BuildAsync(extraBuildArgs: [$"-c {config}"]).ConfigureAwait(false);
        if (PlaywrightProvider.HasPlaywrightSupport)
        {
            await using (var context = await CreateNewBrowserContextAsync())
            {
                await AssertBasicTemplateAsync(context).ConfigureAwait(false);
            }
        }

        // Add test project
        var testProjectName = $"{id}.{_testTemplateName}Tests";
        using var newTestCmd = new DotNetCommand(_testOutput, label: $"new-test-{_testTemplateName}")
                        .WithWorkingDirectory(project.RootDir);
        var res = await newTestCmd.ExecuteAsync($"new {_testTemplateName} -o \"{testProjectName}\"");
        res.EnsureSuccessful();

        var testProjectDir = Path.Combine(project.RootDir, testProjectName);
        Assert.True(Directory.Exists(testProjectDir), $"Expected tests project at {testProjectDir}");

        var testProjectPath = Path.Combine(testProjectDir, testProjectName + ".csproj");
        Assert.True(File.Exists(testProjectPath), $"Expected tests project file at {testProjectPath}");

        PrepareTestCsFile(project.Id, testProjectDir, _testTemplateName);
        PrepareTestProject(project, testProjectPath);

        Assert.True(Directory.Exists(testProjectDir), $"Expected tests project at {testProjectDir}");
        using var cmd = new DotNetCommand(_testOutput, label: $"test-{_testTemplateName}")
                        .WithWorkingDirectory(testProjectDir)
                        .WithTimeout(TimeSpan.FromMinutes(3));

        res = await cmd.ExecuteAsync($"test -c {config}");

        Assert.True(res.ExitCode != 0, $"Expected the tests project run to fail");
        Assert.Matches("System.ArgumentException.*Resource 'webfrontend' not found.", res.Output);
        Assert.Matches("Failed! * - Failed: *1, Passed: *0, Skipped: *0, Total: *1", res.Output);

        async Task AssertBasicTemplateAsync(IBrowserContext context)
        {
            await project.StartAppHostAsync(extraArgs: [$"-c {config}"]);

            try
            {
                var page = await project.OpenDashboardPageAsync(context);
                await CheckDashboardHasResourcesAsync(page, []);
            }
            finally
            {
                await project.StopAppHostAsync();
            }
        }

        static void PrepareTestProject(AspireProject project, string projectPath)
        {
            // Insert <ProjectReference Include="$(MSBuildThisFileDirectory)..\aspire-starter0.AppHost\aspire-starter0.AppHost.csproj" /> in the project file

            // taken from https://raw.githubusercontent.com/dotnet/templating/a325ffa18edd1590f9b340cf83d51d8eb567ebdc/src/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/ValueForms/XmlEncodeValueFormFactory.cs
            StringBuilder output = new();
            using (var w = XmlWriter.Create(output, s_xmlWriterSettings))
            {
                w.WriteString(project.Id);
            }
            var xmlEncodedId = output.ToString();

            var projectReference = $@"<ProjectReference Include=""$(MSBuildThisFileDirectory)..\{xmlEncodedId}.AppHost\{xmlEncodedId}.AppHost.csproj"" />";

            var newContents = File.ReadAllText(projectPath)
                                    .Replace("</Project>", $"<ItemGroup>{projectReference}</ItemGroup>\n</Project>");
            File.WriteAllText(projectPath, newContents);
        }

        static void PrepareTestCsFile(string id, string projectDir, string testTemplateName)
        {
            var testCsPath = Path.Combine(projectDir, "IntegrationTest1.cs");
            var sb = new StringBuilder();

            // Uncomment everything after the marker line
            var inTest = false;
            var marker = testTemplateName switch
            {
                "aspire-nunit" => "// [Test]",
                "aspire-mstest" => "// [TestMethod]",
                "aspire-xunit" => "// [Fact]",
                _ => throw new NotImplementedException($"Unknown test template: {testTemplateName}")
            };

            foreach (var line in File.ReadAllLines(testCsPath))
            {
                if (!inTest && line.Contains(marker))
                {
                    inTest = true;
                }

                if (inTest && CommentLineRegex().IsMatch(line))
                {
                    sb.AppendLine(CommentLineRegex().Replace(line, "    "));
                    continue;
                }

                sb.AppendLine(line);
            }

            var classNameFromId = GeneratedClassNameFixupRegex().Replace(id, "_");
            sb.Replace("Projects.MyAspireApp_AppHost", $"Projects.{classNameFromId}_AppHost");
            File.WriteAllText(testCsPath, sb.ToString());
        }
    }
}

// Individual class for each test framework so the tests can run in separate helix jobs
public class MSTest_PerTestFrameworkTemplatesTests : PerTestFrameworkTemplatesTests
{
    public MSTest_PerTestFrameworkTemplatesTests(ITestOutputHelper testOutput) : base("aspire-mstest", testOutput)
    {
    }
}

public class Xunit_PerTestFrameworkTemplatesTests : PerTestFrameworkTemplatesTests
{
    public Xunit_PerTestFrameworkTemplatesTests(ITestOutputHelper testOutput) : base("aspire-xunit", testOutput)
    {
    }
}

public class Nunit_PerTestFrameworkTemplatesTests : PerTestFrameworkTemplatesTests
{
    public Nunit_PerTestFrameworkTemplatesTests(ITestOutputHelper testOutput) : base("aspire-nunit", testOutput)
    {
    }
}
