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

using System;
using System.Collections.Concurrent;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.InternalTesting;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Templates.Test.Helpers;
using Xunit;
using Xunit.Abstractions;

namespace Templates.Test;

#pragma warning disable xUnit1041 // Fixture arguments to test classes must have fixture sources

public class BaselineTest : LoggedTest
{
    private static readonly string BaselineDefinitionFileResourceName = "Templates.Tests.template-baselines.json";

    public BaselineTest(ProjectFactoryFixture projectFactory)
    {
        ProjectFactory = projectFactory;
    }

    public Project Project { get; set; }

    public static TheoryData<string, string[]> TemplateBaselines
    {
        get
        {
            using (var stream = typeof(BaselineTest).Assembly.GetManifestResourceStream(BaselineDefinitionFileResourceName))
            {
                using (var jsonReader = new JsonTextReader(new StreamReader(stream)))
                {
                    var baseline = JObject.Load(jsonReader);
                    var data = new TheoryData<string, string[]>();
                    foreach (var template in baseline)
                    {
                        foreach (var scenarioName in (JObject)template.Value)
                        {
                            data.Add(
                                (string)scenarioName.Value["Arguments"],
                                ((JArray)scenarioName.Value["Files"]).Select(s => (string)s).ToArray());
                        }
                    }

                    return data;
                }
            }
        }
    }

    public ProjectFactoryFixture ProjectFactory { get; }
    private ITestOutputHelper _output;
    public ITestOutputHelper Output
    {
        get
        {
            if (_output == null)
            {
                _output = new TestOutputLogger(Logger);
            }
            return _output;
        }
    }

    // This test should generally not be quarantined as it only is checking that the expected files are on disk
    // and that the namespace declarations in the generated .cs files start with the project name
    [Theory]
    [MemberData(nameof(TemplateBaselines))]
    public async Task Template_Produces_The_Right_Set_Of_FilesAsync(string arguments, string[] expectedFiles)
    {
        Project = await ProjectFactory.CreateProject(Output);
        await Project.RunDotNetNewRawAsync(arguments);

        expectedFiles = expectedFiles.Select(f => f.Replace("{ProjectName}", Project.ProjectName)).ToArray();

        foreach (var file in expectedFiles)
        {
            AssertFileExists(Project.TemplateOutputDir, file, shouldExist: true);
        }

        var filesInFolder = Directory.EnumerateFiles(Project.TemplateOutputDir, "*", SearchOption.AllDirectories).ToArray();
        foreach (var file in filesInFolder)
        {
            var relativePath = file.Replace(Project.TemplateOutputDir, "").Replace("\\", "/").Trim('/');
            if (relativePath.EndsWith(".csproj", StringComparison.Ordinal) ||
                relativePath.EndsWith(".fsproj", StringComparison.Ordinal) ||
                relativePath.EndsWith(".props", StringComparison.Ordinal) ||
                relativePath.EndsWith(".sln", StringComparison.Ordinal) ||
                relativePath.EndsWith(".targets", StringComparison.Ordinal) ||
                relativePath.StartsWith("bin/", StringComparison.Ordinal) ||
                relativePath.StartsWith("obj/", StringComparison.Ordinal) ||
                relativePath.Contains("/bin/", StringComparison.Ordinal) ||
                relativePath.Contains("/obj/", StringComparison.Ordinal))
            {
                continue;
            }
            Assert.Contains(relativePath, expectedFiles);

            if (relativePath.EndsWith(".cs", StringComparison.Ordinal) && !relativePath.EndsWith("Extensions.cs", StringComparison.Ordinal))
            {
                var namespaceDeclarationPrefix = "namespace ";
                var namespaceDeclaration = File.ReadLines(file)
                    .SingleOrDefault(line => line.StartsWith(namespaceDeclarationPrefix, StringComparison.Ordinal))
                    ?.Substring(namespaceDeclarationPrefix.Length);

                // nullable because Program.cs with top-level statements doesn't have a namespace declaration
                if (namespaceDeclaration is not null)
                {
                    Assert.StartsWith(Project.ProjectName, namespaceDeclaration, StringComparison.Ordinal);
                }
            }
        }
    }

    private void AssertFileExists(string basePath, string path, bool shouldExist)
    {
        var fullPath = Path.Combine(basePath, path);
        var doesExist = File.Exists(fullPath);

        if (shouldExist)
        {
            Assert.True(doesExist, "Expected file to exist, but it doesn't: " + path);
        }
        else
        {
            Assert.False(doesExist, "Expected file not to exist, but it does: " + path);
        }
    }
}
