// 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.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using Microsoft.Build.BackEnd;
using Microsoft.Build.BackEnd.SdkResolution;
using Microsoft.Build.Construction;
using Microsoft.Build.Evaluation;
using Microsoft.Build.Evaluation.Context;
using Microsoft.Build.Execution;
using Microsoft.Build.Framework;
using Microsoft.Build.Unittest;
using Shouldly;
using Xunit;
using Xunit.Abstractions;
using static Microsoft.Build.UnitTests.ObjectModelHelpers;

#nullable disable

namespace Microsoft.Build.UnitTests.BackEnd
{
    public class SdkResultOutOfProc_Tests : IDisposable
    {
        /// <summary>
        /// The mock logger for testing.
        /// </summary>
        private readonly MockLogger _logger;

        /// <summary>
        /// The standard build manager for each test.
        /// </summary>
        private readonly BuildManager _buildManager;

        /// <summary>
        /// The project collection used.
        /// </summary>
        private readonly ProjectCollection _projectCollection;

        private readonly TestEnvironment _env;

        public SdkResultOutOfProc_Tests(ITestOutputHelper output)
        {
            // Ensure that any previous tests which may have been using the default BuildManager do not conflict with us.
            BuildManager.DefaultBuildManager.Dispose();

            _logger = new MockLogger(output);
            _buildManager = new BuildManager();
            _projectCollection = new ProjectCollection();

            _env = TestEnvironment.Create(output);
            _env.SetEnvironmentVariable("MSBUILDINPROCENVCHECK", "1");
            _env.SetEnvironmentVariable("MSBUILDNOINPROCNODE", "1");

            // Need to set this env variable to enable Process.GetCurrentProcess().Id in the project file.
            _env.SetEnvironmentVariable("MSBUILDENABLEALLPROPERTYFUNCTIONS", "1");

            // Set this if you need to debug the out of process build
            // _env.SetEnvironmentVariable("MSBUILDDEBUGONSTART", "1");
        }

        public void Dispose()
        {
            try
            {
                _buildManager.Dispose();
                _projectCollection.Dispose();
            }
            finally
            {
                _env.Dispose();
            }
            EvaluationContext.TestOnlyHookOnCreate = null;
        }

        private const string GetCurrentProcessIdTarget = @"<Target Name='GetCurrentProcessId' Returns='@(CurrentProcessId)'>
    <ItemGroup>
       <CurrentProcessId Include='$([System.Diagnostics.Process]::GetCurrentProcess().Id)'/>
    </ItemGroup>
    <Message Text='[success]'/>
 </Target>";

        private const string GetResolverResultsTarget = @"<Target Name='GetResolverResults' Returns='@(ResolverResult)'>
    <ItemGroup>
       <ResolverResult Include='$(PropertyNameFromResolver)' Type='PropertyNameFromResolver' />
       <ResolverResult Include='@(ItemFromResolver)' Type='ItemFromResolver' />
       <ResolverResult Include='@(SdksImported)' Type='SdksImported' />
    </ItemGroup>
 </Target>";

        [Fact]
        public void RunOutOfProcBuild()
        {
            string contents = $@"
<Project>
<Import Project='Sdk.props' Sdk='TestSdk' />
{GetCurrentProcessIdTarget}
{GetResolverResultsTarget}
</Project>
";

            string projectFolder = _env.CreateFolder().Path;

            string projectPath = Path.Combine(projectFolder, "TestProject.proj");
            File.WriteAllText(projectPath, CleanupFileContents(contents));

            ProjectInstance projectInstance = CreateProjectInstance(projectPath, MSBuildDefaultToolsVersion, _projectCollection);

            var data = new BuildRequestData(projectInstance, new[] { "GetCurrentProcessId", "GetResolverResults" }, _projectCollection.HostServices);
            var customparameters = new BuildParameters { EnableNodeReuse = false, Loggers = new ILogger[] { _logger } };

            BuildResult result = _buildManager.Build(customparameters, data);

            result.OverallResult.ShouldBe(BuildResultCode.Success);

            ValidateRanInSeparateProcess(result);
            ValidateResolverResults(result);
        }

        // Test scenario where using an SdkResolver in a project that hasn't been evaluated
        //  in the main node (which is where the SdkResolver runs).  This validates that
        //  the SdkResult is correctly transferred between nodes.
        [Fact]
        public void RunOutOfProcBuildWithTwoProjects()
        {
            string entryProjectContents = $@"
<Project>
 {GetCurrentProcessIdTarget}
<Target Name='GetResolverResults' Returns='@(ResolverResults)'>
    <MSBuild Projects='ProjectWithSdkImport.proj'
             Targets='GetResolverResults'>
        <Output TaskParameter='TargetOutputs' ItemName='ResolverResults' />
    </MSBuild>
 </Target>
</Project>
";
            string projectFolder = _env.CreateFolder().Path;

            string entryProjectPath = Path.Combine(projectFolder, "EntryProject.proj");
            File.WriteAllText(entryProjectPath, CleanupFileContents(entryProjectContents));

            string projectWithSdkImportContents = $@"
<Project>
<Import Project='Sdk.props' Sdk='TestSdk' />
{GetResolverResultsTarget}
</Project>
";

            string projectWithSdkImportPath = Path.Combine(projectFolder, "ProjectWithSdkImport.proj");
            File.WriteAllText(projectWithSdkImportPath, CleanupFileContents(projectWithSdkImportContents));

            ProjectInstance projectInstance = CreateProjectInstance(entryProjectPath, MSBuildDefaultToolsVersion, _projectCollection);

            var data = new BuildRequestData(projectInstance, new[] { "GetCurrentProcessId", "GetResolverResults" }, _projectCollection.HostServices);
            var customparameters = new BuildParameters { EnableNodeReuse = false, Loggers = new ILogger[] { _logger } };

            BuildResult result = _buildManager.Build(customparameters, data);


            result.OverallResult.ShouldBe(BuildResultCode.Success);

            ValidateRanInSeparateProcess(result);
            ValidateResolverResults(result);
        }


        private void ValidateRanInSeparateProcess(BuildResult result)
        {
            TargetResult targetresult = result.ResultsByTarget["GetCurrentProcessId"];
            ITaskItem[] item = targetresult.Items;

            item.ShouldHaveSingleItem();

            int.TryParse(item[0].ItemSpec, out int processId)
                .ShouldBeTrue($"Process ID passed from the 'test' target is not a valid integer (actual is '{item[0].ItemSpec}')");
            processId.ShouldNotBe(Process.GetCurrentProcess().Id);
        }

        private void ValidateResolverResults(BuildResult result)
        {
            TargetResult targetresult = result.ResultsByTarget["GetResolverResults"];

            IEnumerable<string> GetResolverResults(string type)
            {
                return targetresult.Items.Where(i => i.GetMetadata("Type").Equals(type, StringComparison.OrdinalIgnoreCase))
                    .Select(i => i.ItemSpec)
                    .ToList();
            }

            GetResolverResults("PropertyNameFromResolver").ShouldBeSameIgnoringOrder(new[] { "PropertyValueFromResolver" });
            GetResolverResults("ItemFromResolver").ShouldBeSameIgnoringOrder(new[] { "ItemValueFromResolver" });
            GetResolverResults("SdksImported").ShouldBeSameIgnoringOrder(new[] { "Sdk1", "Sdk2" });
        }

        private ProjectInstance CreateProjectInstance(string projectPath, string toolsVersion, ProjectCollection projectCollection)
        {
            var sdkResolver = SetupSdkResolver(Path.GetDirectoryName(projectPath));

            var projectOptions = SdkUtilities.CreateProjectOptionsWithResolver(sdkResolver);

            projectOptions.ProjectCollection = projectCollection;
            projectOptions.ToolsVersion = toolsVersion;

            ProjectRootElement projectRootElement = ProjectRootElement.Open(projectPath, _projectCollection);

            Project project = Project.FromProjectRootElement(projectRootElement, projectOptions);

            return project.CreateProjectInstance(ProjectInstanceSettings.None, projectOptions.EvaluationContext);
        }

        private SdkResolver SetupSdkResolver(string projectFolder)
        {
            Directory.CreateDirectory(Path.Combine(projectFolder, "Sdk1"));
            Directory.CreateDirectory(Path.Combine(projectFolder, "Sdk2"));

            string sdk1propsContents = @"
<Project>
    <ItemGroup>
        <SdksImported Include='Sdk1' />
    </ItemGroup>
</Project>";

            string sdk2propsContents = @"
<Project>
    <ItemGroup>
        <SdksImported Include='Sdk2' />
    </ItemGroup>
</Project>";

            File.WriteAllText(Path.Combine(projectFolder, "Sdk1", "Sdk.props"), CleanupFileContents(sdk1propsContents));
            File.WriteAllText(Path.Combine(projectFolder, "Sdk2", "Sdk.props"), CleanupFileContents(sdk2propsContents));

            var sdkResolver = new SdkUtilities.ConfigurableMockSdkResolver(
                new Build.BackEnd.SdkResolution.SdkResult(
                        new SdkReference("TestSdk", null, null),
                        new[]
                        {
                            Path.Combine(projectFolder, "Sdk1"),
                            Path.Combine(projectFolder, "Sdk2")
                        },
                        version: null,
                        propertiesToAdd: new Dictionary<string, string>()
                            { {"PropertyNameFromResolver","PropertyValueFromResolver" } },
                        itemsToAdd: new Dictionary<string, SdkResultItem>()
                            {
                                { "ItemFromResolver", new SdkResultItem("ItemValueFromResolver", null) }
                            },
                        warnings: null));

            EvaluationContext.TestOnlyHookOnCreate = context =>
            {
                var sdkService = (SdkResolverService)context.SdkResolverService;

                sdkService.InitializeForTests(null, new List<SdkResolver> { sdkResolver });
            };

            ((IBuildComponentHost)_buildManager).RegisterFactory(BuildComponentType.SdkResolverService, type =>
            {
                var resolverService = new MainNodeSdkResolverService();
                resolverService.InitializeForTests(null, new List<SdkResolver> { sdkResolver });
                return resolverService;
            });

            return sdkResolver;
        }
    }
}
