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

using System.IO.MemoryMappedFiles;
using System.Runtime.CompilerServices;
using Microsoft.NET.Build.Tasks;

namespace Microsoft.NET.Build.Tests
{
    public class GivenWeWantToRequireWindowsForDesktopApps : SdkTest
    {
        public GivenWeWantToRequireWindowsForDesktopApps(ITestOutputHelper log) : base(log)
        {
        }

        [WindowsOnlyTheory]
        [InlineData("UseWPF")]
        [InlineData("UseWindowsForms")]
        public void It_builds_on_windows_with_the_windows_desktop_sdk(string uiFrameworkProperty)
        {
            const string ProjectName = "WindowsDesktopSdkTest";

            var asset = CreateWindowsDesktopSdkTestAsset(ProjectName, uiFrameworkProperty, uiFrameworkProperty);

            var command = new BuildCommand(asset);

            command
                .Execute()
                .Should()
                .Pass();
        }

        [PlatformSpecificTheory(TestPlatforms.Linux | TestPlatforms.OSX | TestPlatforms.FreeBSD)]
        [InlineData("UseWPF")]
        [InlineData("UseWindowsForms")]
        public void It_errors_on_nonwindows_with_the_windows_desktop_sdk(string uiFrameworkProperty)
        {
            const string ProjectName = "WindowsDesktopSdkErrorTest";

            var asset = CreateWindowsDesktopSdkTestAsset(ProjectName, uiFrameworkProperty, uiFrameworkProperty);

            var command = new BuildCommand(asset);

            command
                .Execute()
                .Should()
                .Fail()
                .And
                .HaveStdOutContaining(Strings.WindowsDesktopFrameworkRequiresWindows);
        }

        [WindowsOnlyTheory]
        [InlineData("Microsoft.WindowsDesktop.App")]
        [InlineData("Microsoft.WindowsDesktop.App.WindowsForms")]
        [InlineData("Microsoft.WindowsDesktop.App.WPF")]
        public void It_builds_on_windows_with_a_framework_reference(string desktopFramework)
        {
            const string ProjectName = "WindowsDesktopReferenceTest";

            var asset = CreateWindowsDesktopReferenceTestAsset(ProjectName, desktopFramework, desktopFramework);

            var command = new BuildCommand(asset);

            command
                .Execute()
                .Should()
                .Pass();
        }

        [PlatformSpecificTheory(TestPlatforms.Linux | TestPlatforms.OSX | TestPlatforms.FreeBSD)]
        [InlineData("Microsoft.WindowsDesktop.App")]
        [InlineData("Microsoft.WindowsDesktop.App.WindowsForms")]
        [InlineData("Microsoft.WindowsDesktop.App.WPF")]
        public void It_errors_on_nonwindows_with_a_framework_reference(string desktopFramework)
        {
            const string ProjectName = "WindowsDesktopReferenceErrorTest";

            var asset = CreateWindowsDesktopReferenceTestAsset(ProjectName, desktopFramework, desktopFramework);

            var command = new BuildCommand(asset);

            command
                .Execute()
                .Should()
                .Fail()
                .And
                .HaveStdOutContaining(Strings.WindowsDesktopFrameworkRequiresWindows);
        }

        [PlatformSpecificFact(TestPlatforms.Linux | TestPlatforms.OSX | TestPlatforms.FreeBSD)]
        public void AppTargetingWindows10CanBuildOnNonWindows()
        {
            var testProject = new TestProject()
            {
                TargetFrameworks = ToolsetInfo.CurrentTargetFramework + "-windows10.0.19041.0",
                IsWinExe = true
            };
            testProject.AdditionalProperties["EnableWindowsTargeting"] = "true";

            var testAsset = _testAssetsManager.CreateTestProject(testProject);

            new BuildCommand(testAsset)
                .Execute()
                .Should()
                .Pass();
        }

        [PlatformSpecificFact(TestPlatforms.Linux | TestPlatforms.OSX | TestPlatforms.FreeBSD)]
        public void AppTargetingWindows10WillProduceWindowsGUISubsystemExe()
        {
            // check subsystem is successfully set as WindowsGUISubsystem
            var testProject = new TestProject()
            {
                TargetFrameworks = ToolsetInfo.CurrentTargetFramework + "-windows10.0.19041.0",
                IsWinExe = true
            };
            testProject.AdditionalProperties["EnableWindowsTargeting"] = "true";
            testProject.AdditionalProperties["RuntimeIdentifier"] = "win-x64";

            var testAsset = _testAssetsManager.CreateTestProject(testProject);

            new PublishCommand(testAsset)
                .Execute()
                .Should()
                .Pass();

            var exePath = Path.Combine(testAsset.TestRoot, testAsset.TestProject.Name,
                "bin", "Debug", testAsset.TestProject.TargetFrameworks, "win-x64", "publish",
                $"{testAsset.TestProject.Name}.exe");

            const int PEHeaderPointerOffset = 0x3C;
            const int SubsystemOffset = 0x5C;
            const ushort WindowsGUISubsystem = 0x2;

            using var mmap = MemoryMappedFile.CreateFromFile(exePath);
            using var accessor = mmap.CreateViewAccessor();

            uint peHeaderOffset = accessor.ReadUInt32(PEHeaderPointerOffset);
            ushort subsystem = accessor.ReadUInt16(peHeaderOffset + SubsystemOffset);
            subsystem.Should().Be(WindowsGUISubsystem);
        }

        [PlatformSpecificFact(TestPlatforms.Linux | TestPlatforms.OSX | TestPlatforms.FreeBSD)]
        public void WindowsFormsAppCanBuildOnNonWindows()
        {
            var testInstance = _testAssetsManager.CopyTestAsset("WindowsFormsTestApp")
                .WithSource();

            new BuildCommand(Log, testInstance.Path)
                .WithEnvironmentVariable("EnableWindowsTargeting", "true")
                .Execute()
                .Should()
                .Pass();
        }

        [WindowsOnlyRequiresMSBuildVersionFact("16.8.0")]
        public void It_builds_on_windows_with_the_windows_desktop_sdk_5_0_with_ProjectSdk_set()
        {
            const string ProjectName = "WindowsDesktopSdkTest_50";

            const string tfm = "net5.0-windows";

            var testProject = new TestProject()
            {
                Name = ProjectName,
                TargetFrameworks = tfm,
                ProjectSdk = "Microsoft.NET.Sdk.WindowsDesktop",
                IsWinExe = true,
            };

            testProject.SourceFiles.Add("App.xaml.cs", _fileUseWindowsType);
            testProject.AdditionalProperties.Add("UseWPF", "true");

            var asset = _testAssetsManager.CreateTestProject(testProject);

            var command = new BuildCommand(Log, Path.Combine(asset.Path, ProjectName));

            command
                .Execute()
                .Should()
                .Pass();
        }

        [WindowsOnlyRequiresMSBuildVersionFact("16.8.0")]
        public void It_builds_on_windows_with_the_windows_desktop_sdk_5_0_without_ProjectSdk_set()
        {
            const string ProjectName = "WindowsDesktopSdkTest_without_ProjectSdk_set";

            const string tfm = "net5.0";

            var testProject = new TestProject()
            {
                Name = ProjectName,
                TargetFrameworks = tfm,
                IsWinExe = true,
            };

            testProject.SourceFiles.Add("App.xaml.cs", _fileUseWindowsType);
            testProject.AdditionalProperties.Add("UseWPF", "true");
            testProject.AdditionalProperties.Add("TargetPlatformIdentifier", "Windows");

            var asset = _testAssetsManager.CreateTestProject(testProject);

            var command = new BuildCommand(Log, Path.Combine(asset.Path, ProjectName));

            command
                .Execute()
                .Should()
                .Pass();
        }

        [WindowsOnlyRequiresMSBuildVersionFact("16.8.0")]
        public void When_TargetPlatformVersion_is_set_higher_than_10_It_can_reference_cswinrt_api()
        {
            const string ProjectName = "WindowsDesktopSdkTest_without_ProjectSdk_set";

            const string tfm = "net5.0";

            var testProject = new TestProject()
            {
                Name = ProjectName,
                TargetFrameworks = tfm,
                IsWinExe = true,
            };

            testProject.SourceFiles.Add("Program.cs", _useCsWinrtApi);
            testProject.AdditionalProperties.Add("TargetPlatformIdentifier", "Windows");
            testProject.AdditionalProperties.Add("TargetPlatformVersion", "10.0.17763");

            var asset = _testAssetsManager.CreateTestProject(testProject);

            var buildCommand = new BuildCommand(Log, Path.Combine(asset.Path, ProjectName));

            buildCommand.Execute()
                .Should()
                .Pass();

            static void Assert(DirectoryInfo outputDir)
            {
                outputDir.File("Microsoft.Windows.SDK.NET.dll").Exists.Should().BeTrue("The output has cswinrt dll");
                outputDir.File("WinRT.Runtime.dll").Exists.Should().BeTrue("The output has cswinrt dll");
                var runtimeconfigjson = File.ReadAllText(outputDir.File(ProjectName + ".runtimeconfig.json").FullName);
                runtimeconfigjson.Contains(@"""name"": ""Microsoft.NETCore.App""").Should().BeTrue("runtimeconfig.json only reference Microsoft.NETCore.App");
                runtimeconfigjson.Contains("Microsoft.Windows.SDK.NET").Should().BeFalse("runtimeconfig.json does not reference windows SDK");
            }

            Assert(buildCommand.GetOutputDirectory(tfm));

            var publishCommand = new PublishCommand(asset);
            var runtimeIdentifier = $"{ToolsetInfo.LatestWinRuntimeIdentifier}-x64";
            publishCommand.Execute("-p:SelfContained=true", $"-p:RuntimeIdentifier={runtimeIdentifier}")
                .Should()
                .Pass();

            Assert(publishCommand.GetOutputDirectory(tfm, runtimeIdentifier: runtimeIdentifier));

            var filesCopiedToPublishDirCommand = new GetValuesCommand(
                Log,
                Path.Combine(asset.Path, testProject.Name),
                testProject.TargetFrameworks,
                "FilesCopiedToPublishDir",
                GetValuesCommand.ValueType.Item)
            {
                DependsOnTargets = "ComputeFilesCopiedToPublishDir",
                MetadataNames = { "RelativePath" },
            };

            filesCopiedToPublishDirCommand.Execute().Should().Pass();
            var filesCopiedToPublishDircommandItems
                = from item in filesCopiedToPublishDirCommand.GetValuesWithMetadata()
                  select new
                  {
                      Identity = item.value,
                      RelativePath = item.metadata["RelativePath"]
                  };

            filesCopiedToPublishDircommandItems
                .Should().Contain(i => i.RelativePath == "Microsoft.Windows.SDK.NET.dll" && Path.GetFileName(i.Identity) == "Microsoft.Windows.SDK.NET.dll",
                                  because: "wapproj should copy cswinrt dlls");
            filesCopiedToPublishDircommandItems
                .Should()
                .Contain(i => i.RelativePath == "WinRT.Runtime.dll" && Path.GetFileName(i.Identity) == "WinRT.Runtime.dll",
                         because: "wapproj should copy cswinrt dlls");

            var publishItemsOutputGroupOutputsCommand = new GetValuesCommand(
                Log,
                Path.Combine(asset.Path, testProject.Name),
                testProject.TargetFrameworks,
                "PublishItemsOutputGroupOutputs",
                GetValuesCommand.ValueType.Item)
            {
                DependsOnTargets = "Publish",
                MetadataNames = { "OutputPath" },
            };

            publishItemsOutputGroupOutputsCommand.Execute().Should().Pass();
            var publishItemsOutputGroupOutputsItems =
                from item in publishItemsOutputGroupOutputsCommand.GetValuesWithMetadata()
                select new
                {
                    FullAssetPath = Path.GetFullPath(Path.Combine(asset.Path, testProject.Name, item.metadata["OutputPath"]))
                };

            publishItemsOutputGroupOutputsItems
                .Should().Contain(i => Path.GetFileName(Path.GetFullPath(i.FullAssetPath)) == "WinRT.Runtime.dll" && File.Exists(i.FullAssetPath),
                      because: (string)"as the replacement for FilesCopiedToPublishDir, wapproj should copy cswinrt dlls");
            publishItemsOutputGroupOutputsItems
                .Should()
                .Contain(i => Path.GetFileName(Path.GetFullPath(i.FullAssetPath)) == "WinRT.Runtime.dll" && File.Exists(i.FullAssetPath),
                         because: "as the replacement for FilesCopiedToPublishDir, wapproj should copy cswinrt dlls");

            // ready to run is supported
            publishCommand.Execute("-p:SelfContained=true", $"-p:RuntimeIdentifier={runtimeIdentifier}", $"-p:PublishReadyToRun=true")
                .Should()
                .Pass();

            // PublishSingleFile is supported
            publishCommand.Execute("-p:SelfContained=true", $"-p:RuntimeIdentifier={runtimeIdentifier}", $"-p:PublishSingleFile=true")
                .Should()
                .Pass();
        }

        [WindowsOnlyRequiresMSBuildVersionFact("16.8.0")]
        public void Given_duplicated_ResolvedFileToPublish_It_Can_Publish()
        {
            const string ProjectName = "WindowsDesktopSdkTest_without_ProjectSdk_set";

            const string tfm = "net5.0";

            var testProject = new TestProject()
            {
                Name = ProjectName,
                TargetFrameworks = tfm,
                IsWinExe = true,
            };

            var testAsset = _testAssetsManager.CreateTestProject(testProject).WithProjectChanges((project) =>
            {
                var ns = project.Root.Name.Namespace;
                var duplicatedResolvedFileToPublish = XElement.Parse(@"
<ItemGroup>
    <ResolvedFileToPublish Include=""obj\Debug\net5.0\WindowsDesktopSdkTest_without_ProjectSdk_set.dll"">
      <RelativePath>WindowsDesktopSdkTest_without_ProjectSdk_set.dll</RelativePath>
    </ResolvedFileToPublish>
    <ResolvedFileToPublish Include=""obj\Debug\net5.0\WindowsDesktopSdkTest_without_ProjectSdk_set.dll"">
      <RelativePath>WindowsDesktopSdkTest_without_ProjectSdk_set.dll</RelativePath>
    </ResolvedFileToPublish>
  </ItemGroup>
");
                project.Root.Add(duplicatedResolvedFileToPublish);
            });

            var publishItemsOutputGroupOutputsCommand = new GetValuesCommand(
                Log,
                Path.Combine(testAsset.Path, testProject.Name),
                testProject.TargetFrameworks,
                "PublishItemsOutputGroupOutputs",
                GetValuesCommand.ValueType.Item)
            {
                DependsOnTargets = "Publish",
                MetadataNames = { "OutputPath" },
            };

            publishItemsOutputGroupOutputsCommand.Execute().Should().Pass();
            var publishItemsOutputGroupOutputsItems =
                from item in publishItemsOutputGroupOutputsCommand.GetValuesWithMetadata()
                select new
                {
                    OutputPath = item.metadata["OutputPath"]
                };
        }

        private TestAsset CreateWindowsDesktopSdkTestAsset(string projectName, string uiFrameworkProperty, string identifier, [CallerMemberName] string callingMethod = "")
        {
            const string tfm = "netcoreapp3.0";

            var testProject = new TestProject()
            {
                Name = projectName,
                TargetFrameworks = tfm,
                ProjectSdk = "Microsoft.NET.Sdk.WindowsDesktop",
                IsWinExe = true,
            };

            testProject.AdditionalProperties.Add(uiFrameworkProperty, "true");

            return _testAssetsManager.CreateTestProject(testProject, callingMethod, identifier);
        }

        private TestAsset CreateWindowsDesktopReferenceTestAsset(string projectName, string desktopFramework, string identifier, [CallerMemberName] string callingMethod = "")
        {
            const string tfm = "netcoreapp3.0";

            var testProject = new TestProject()
            {
                Name = projectName,
                TargetFrameworks = tfm,
                IsWinExe = true,
            };

            testProject.FrameworkReferences.Add(desktopFramework);

            return _testAssetsManager.CreateTestProject(testProject, callingMethod, identifier);
        }

        private readonly string _fileUseWindowsType = @"
using System.Windows;

namespace wpf
{
    public partial class App : Application
    {
    }

    class Program
    {
        static void Main(string[] args)
        {
        }
    }
}
";

        private readonly string _useCsWinrtApi = @"
using System;
using Windows.Data.Json;

namespace consolecswinrt
{
    class Program
    {
        static void Main(string[] args)
        {
            var rootObject = JsonObject.Parse(""{\""greet\"": \""Hello\""}"");
            Console.WriteLine(rootObject[""greet""]);
        }
    }
}
";
    }
}
