// Copyright (c) .NET Foundation and contributors. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

using Microsoft.Build.Construction;
using Microsoft.DotNet.ProjectJsonMigration;
using Microsoft.DotNet.Internal.ProjectModel;
using Microsoft.DotNet.TestFramework;
using Microsoft.DotNet.Tools.Test.Utilities;
using NuGet.Frameworks;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using Xunit;
using FluentAssertions;
using Microsoft.DotNet.ProjectJsonMigration.Rules;

namespace Microsoft.DotNet.ProjectJsonMigration.Tests
{
    public class GivenThatIWantToMigrateProjectDependencies : TestBase
    {
        [Fact]
        public void ProjectDependenciesAreMigratedToProjectReference()
        {
            var solutionDirectory = TestAssets.Get("TestAppWithLibrary")
                            .CreateInstance(callingMethod: "p")
                            .WithSourceFiles()
                            .Root.FullName;

            var appDirectory = Path.Combine(solutionDirectory, "TestApp");

            var projectContext = ProjectContext.Create(appDirectory, FrameworkConstants.CommonFrameworks.NetCoreApp11);
            var mockProj = ProjectRootElement.Create();
            var testSettings = MigrationSettings.CreateMigrationSettingsTestHook(appDirectory, appDirectory, mockProj, null);
            var testInputs = new MigrationRuleInputs(new[] {projectContext}, mockProj, mockProj.AddItemGroup(),
                mockProj.AddPropertyGroup());
            new MigrateProjectDependenciesRule().Apply(testSettings, testInputs);

            var projectReferences = mockProj.Items.Where(item => item.ItemType.Equals("ProjectReference", StringComparison.Ordinal));
            projectReferences.Count().Should().Be(1);

            var projectReference = projectReferences.First();
            projectReference.Include.Should().Be(Path.Combine("..", "TestLibrary", "TestLibrary.csproj"));
            projectReference.Parent.Condition.Should().BeEmpty();
        }

        [Fact]
        public void ItDoesNotMigrateADependencyWithTargetPackageThatHasAMatchingProjectAsAProjectReference()
        {
            var solutionDirectory = TestAssets.Get("NonRestoredTestProjects", "AppWithProjectDependencyAsTarget")
                        .CreateInstance(callingMethod: "p")
                        .WithSourceFiles()
                        .Root.FullName;

            var appDirectory = Path.Combine(solutionDirectory, "TestApp");

            var projectContext = ProjectContext.Create(appDirectory, FrameworkConstants.CommonFrameworks.NetCoreApp11);
            var mockProj = ProjectRootElement.Create();
            var testSettings = MigrationSettings.CreateMigrationSettingsTestHook(appDirectory, appDirectory, mockProj, null);
            var testInputs = new MigrationRuleInputs(new[] {projectContext}, mockProj, mockProj.AddItemGroup(),
                mockProj.AddPropertyGroup());
            new MigrateProjectDependenciesRule().Apply(testSettings, testInputs);

            var projectReferences = mockProj.Items.Where(
                item => item.ItemType.Equals("ProjectReference", StringComparison.Ordinal));
            projectReferences.Should().BeEmpty();
        }

        [Fact]
        public void TFMSpecificProjectDependenciesAreMigratedToProjectReferenceUnderConditionItemGroup()
        {
            var solutionDirectory = TestAssets.Get("TestAppWithLibraryUnderTFM")
                            .CreateInstance(callingMethod: "p")
                            .WithSourceFiles()
                            .Root.FullName;

            var appDirectory = Path.Combine(solutionDirectory, "TestApp");

            var projectContext = ProjectContext.Create(appDirectory, FrameworkConstants.CommonFrameworks.NetCoreApp11);
            var mockProj = ProjectRootElement.Create();
            var testSettings = MigrationSettings.CreateMigrationSettingsTestHook(appDirectory, appDirectory, mockProj, null);
            var testInputs = new MigrationRuleInputs(new[] {projectContext}, mockProj, mockProj.AddItemGroup(),
                mockProj.AddPropertyGroup());
            new MigrateProjectDependenciesRule().Apply(testSettings, testInputs);

            var projectReferences = mockProj.Items.Where(item => item.ItemType.Equals("ProjectReference", StringComparison.Ordinal));
            projectReferences.Count().Should().Be(1);

            var projectReference = projectReferences.First();
            projectReference.Include.Should().Be(Path.Combine("..", "TestLibrary", "TestLibrary.csproj"));
            projectReference.Parent.Condition.Should().Be(" '$(TargetFramework)' == 'netcoreapp1.1' ");
        }

        [Fact]
        public void ItThrowsWhenProjectDependencyIsUnresolved()
        {
            // No Lock file => unresolved
            var solutionDirectory = TestAssets.Get("TestAppWithLibrary")
                        .CreateInstance()
                        .WithSourceFiles()
                        .Root.FullName;

            var appDirectory = Path.Combine(solutionDirectory, "TestApp");
            var libraryDirectory = Path.Combine(solutionDirectory, "TestLibrary");
            Directory.Delete(libraryDirectory, true);

            var projectContext = ProjectContext.Create(appDirectory, FrameworkConstants.CommonFrameworks.NetCoreApp11);
            var mockProj = ProjectRootElement.Create();
            var testSettings = MigrationSettings.CreateMigrationSettingsTestHook(appDirectory, appDirectory, mockProj);
            var testInputs = new MigrationRuleInputs(new[] {projectContext}, mockProj, mockProj.AddItemGroup(), mockProj.AddPropertyGroup());

            Action action = () => new MigrateProjectDependenciesRule().Apply(testSettings, testInputs);
            action.ShouldThrow<Exception>()
                .Where(e => e.Message.Contains("MIGRATE1014::Unresolved Dependency: Unresolved project dependency (TestLibrary)"));
        }

        [Theory]
        [InlineData(@"some/path/to.cSproj", new [] { @"some/path/to.cSproj" })]
        [InlineData(@"to.CSPROJ",new [] { @"to.CSPROJ" })]
        public void ItMigratesCsprojProjectReferenceInXproj(string projectReference, string[] expectedMigratedReferences)
        {
            var xproj = ProjectRootElement.Create();
            xproj.AddItem("ProjectReference", projectReference);

            var projectReferenceName = Path.GetFileNameWithoutExtension(projectReference);

            var projectJson = @"
                {
                    ""dependencies"": {" +
                        $"\"{projectReferenceName}\"" + @": {
                            ""target"" : ""project""
                        }
                    }
                }
            ";

            var testDirectory = Temp.CreateDirectory().Path;
            var migratedProj = TemporaryProjectFileRuleRunner.RunRules(new IMigrationRule[]
                {
                    new MigrateProjectDependenciesRule()
                }, projectJson, testDirectory, xproj);

            var migratedProjectReferenceItems = migratedProj.Items.Where(i => i.ItemType == "ProjectReference");
            migratedProjectReferenceItems.Should().HaveCount(expectedMigratedReferences.Length);
            migratedProjectReferenceItems.Select(m => m.Include).Should().BeEquivalentTo(expectedMigratedReferences);
        }

        [Fact]
        public void ItMigratesCsprojProjectReferenceInXprojIncludingConditionOnProjectReference()
        {
            var projectReference = "some/to.csproj";
            var xproj = ProjectRootElement.Create();
            var csprojReferenceItem = xproj.AddItem("ProjectReference", projectReference);
            csprojReferenceItem.Condition = " '$(Foo)' == 'bar' ";

            var projectReferenceName = Path.GetFileNameWithoutExtension(projectReference);

            var projectJson = @"
                {
                    ""dependencies"": {" +
                        $"\"{projectReferenceName}\"" + @": {
                            ""target"" : ""project""
                        }
                    }
                }
            ";

            var testDirectory = Temp.CreateDirectory().Path;
            var migratedProj = TemporaryProjectFileRuleRunner.RunRules(new IMigrationRule[]
                {
                    new MigrateProjectDependenciesRule()
                }, projectJson, testDirectory, xproj);

            var migratedProjectReferenceItems = migratedProj.Items.Where(i => i.ItemType == "ProjectReference");
            migratedProjectReferenceItems.Should().HaveCount(1);

            var migratedProjectReferenceItem = migratedProjectReferenceItems.First();
            migratedProjectReferenceItem.Include.Should().Be(projectReference);
            migratedProjectReferenceItem.Condition.Should().Be(" '$(Foo)' == 'bar' ");
        }

        [Fact]
        public void ItMigratesCsprojProjectReferenceInXprojIncludingConditionOnProjectReferenceParent()
        {
            var projectReference = "some/to.csproj";
            var xproj = ProjectRootElement.Create();
            var csprojReferenceItem = xproj.AddItem("ProjectReference", projectReference);
            csprojReferenceItem.Parent.Condition = " '$(Foo)' == 'bar' ";

            var projectReferenceName = Path.GetFileNameWithoutExtension(projectReference);

            var projectJson = @"
                {
                    ""dependencies"": {" +
                        $"\"{projectReferenceName}\"" + @": {
                            ""target"" : ""project""
                        }
                    }
                }
            ";

            var testDirectory = Temp.CreateDirectory().Path;
            var migratedProj = TemporaryProjectFileRuleRunner.RunRules(new IMigrationRule[]
                {
                    new MigrateProjectDependenciesRule()
                }, projectJson, testDirectory, xproj);

            var migratedProjectReferenceItems = migratedProj.Items.Where(i => i.ItemType == "ProjectReference");
            migratedProjectReferenceItems.Should().HaveCount(1);

            var migratedProjectReferenceItem = migratedProjectReferenceItems.First();
            migratedProjectReferenceItem.Include.Should().Be(projectReference);
            migratedProjectReferenceItem.Condition.Should().Be(" '$(Foo)' == 'bar' ");
        }

        [Fact]
        public void ItMigratesCsprojProjectReferenceInXprojIncludingConditionOnProjectReferenceParentAndItem()
        {
            var projectReference = "some/to.csproj";
            var xproj = ProjectRootElement.Create();
            var csprojReferenceItem = xproj.AddItem("ProjectReference", projectReference);
            csprojReferenceItem.Parent.Condition = " '$(Foo)' == 'bar' ";
            csprojReferenceItem.Condition = " '$(Bar)' == 'foo' ";

            var projectReferenceName = Path.GetFileNameWithoutExtension(projectReference);

            var projectJson = @"
                {
                    ""dependencies"": {" +
                        $"\"{projectReferenceName}\"" + @": {
                            ""target"" : ""project""
                        }
                    }
                }
            ";

            var testDirectory = Temp.CreateDirectory().Path;
            var migratedProj = TemporaryProjectFileRuleRunner.RunRules(new IMigrationRule[]
                {
                    new MigrateProjectDependenciesRule()
                }, projectJson, testDirectory, xproj);

            var migratedProjectReferenceItems = migratedProj.Items.Where(i => i.ItemType == "ProjectReference");
            migratedProjectReferenceItems.Should().HaveCount(1);

            var migratedProjectReferenceItem = migratedProjectReferenceItems.First();
            migratedProjectReferenceItem.Include.Should().Be(projectReference);
            migratedProjectReferenceItem.Condition.Should().Be(" '$(Bar)' == 'foo'  and  '$(Foo)' == 'bar' ");
        }

        [Fact]
        public void ItDoesNotPromoteP2PReferencesUpInTheDependencyChain()
        {
            var mockProj = MigrateProject("TestAppDependencyGraph", "ProjectA");

            var projectReferences = mockProj.Items.Where(
                item => item.ItemType.Equals("ProjectReference", StringComparison.Ordinal));
            projectReferences.Count().Should().Be(2);
        }

        [Fact]
        public void ItDoesNotPromoteFrameworkAssembliesFromP2PReferencesUpInTheDependencyChain()
        {
            var solutionDirectory = TestAssets.Get(TestAssetKinds.DesktopTestProjects, "TestAppWithFrameworkAssemblies")
                .CreateInstance()
                .WithSourceFiles().Root;

            var appDirectory = Path.Combine(solutionDirectory.FullName, "ProjectA");

            var projectContext = ProjectContext.Create(appDirectory, FrameworkConstants.CommonFrameworks.Net451);
            var mockProj = ProjectRootElement.Create();
            var testSettings = MigrationSettings.CreateMigrationSettingsTestHook(appDirectory, appDirectory, mockProj, null);
            var testInputs = new MigrationRuleInputs(new[] {projectContext}, mockProj, mockProj.AddItemGroup(),
                mockProj.AddPropertyGroup());
            new MigrateProjectDependenciesRule().Apply(testSettings, testInputs);

            var frameworkAssemblyReferences = mockProj.Items.Where(
                item => item.ItemType == "Reference" &&
                item.Include == "System.ComponentModel.DataAnnotations" &&
                item.Parent.Condition == " '$(TargetFramework)' == 'net451' ");
            frameworkAssemblyReferences.Count().Should().Be(0);
        }

        [Fact]
        public void NoP2PReferenceIsMarkedWithAFromP2PAttribute()
        {
            var expectedNonHoistedProjectReferences = new [] {
                Path.Combine("..", "ProjectB", "ProjectB.csproj"),
                Path.Combine("..", "ProjectC", "ProjectC.csproj")
            };

            var mockProj = MigrateProject("TestAppDependencyGraph", "ProjectA");

            var projectReferences = mockProj.Items
                .Where(item =>
                    item.ItemType == "ProjectReference");

            projectReferences.Should().HaveCount(c => c ==  2)
                .And.OnlyContain(item => item.GetMetadataWithName("FromP2P") == null);

            projectReferences.Select(i => i.Include).Should().BeEquivalentTo(expectedNonHoistedProjectReferences);
        }

        [Fact]
        public void ItMigratesUnqualifiedDependenciesAsProjectReferenceWhenAMatchingProjectIsFound()
        {
            var mockProj = MigrateProject("TestAppWithUnqualifiedDependencies", "ProjectA");
            var projectReferenceInclude = Path.Combine("..", "ProjectB", "ProjectB.csproj");

            var projectReferences = mockProj.Items.Should().ContainSingle(
                item => item.ItemType == "ProjectReference" && item.Include == projectReferenceInclude);
        }

        [Fact]
        public void ItDoesNotReferenceTheProjectUnderBackupWhenMigratingAPartiallyMigratedStructure()
        {
            var solutionDirectory = TestAssets.Get("NonRestoredTestProjects", "PJHalfMigrated")
                            .CreateInstance()
                            .WithSourceFiles()
                            .Root.FullName;

            var appDirectory = Path.Combine(solutionDirectory, "ProjectB");

            var projectContext = ProjectContext.Create(appDirectory, FrameworkConstants.CommonFrameworks.NetCoreApp11);
            var mockProj = ProjectRootElement.Create();
            var testSettings = MigrationSettings.CreateMigrationSettingsTestHook(appDirectory, appDirectory, mockProj, null);
            var testInputs = new MigrationRuleInputs(new[] {projectContext}, mockProj, mockProj.AddItemGroup(),
                mockProj.AddPropertyGroup());
            new MigrateProjectDependenciesRule().Apply(testSettings, testInputs);

            var projectReferences = mockProj.Items.Where(
                item => item.ItemType.Equals("ProjectReference", StringComparison.Ordinal));
            projectReferences.Should().ContainSingle();
            projectReferences.Single().Include.Should().Be(Path.Combine("..", "src", "ProjectA", "ProjectA.csproj"));
        }

        private ProjectRootElement MigrateProject(string solution, string project)
        {
            return MigrateProject(solution, project, FrameworkConstants.CommonFrameworks.NetCoreApp11);
        }

        private ProjectRootElement MigrateProject(
            string solution,
            string project,
            NuGetFramework targetFramework)
        {
            var solutionDirectory = TestAssets.Get(solution)
                        .CreateInstance(callingMethod: "p")
                        .WithSourceFiles()
                        .Root.FullName;

            var appDirectory = Path.Combine(solutionDirectory, project);

            var projectContext = ProjectContext.Create(appDirectory, targetFramework);
            var mockProj = ProjectRootElement.Create();
            var testSettings = MigrationSettings.CreateMigrationSettingsTestHook(appDirectory, appDirectory, mockProj, null);
            var testInputs = new MigrationRuleInputs(new[] {projectContext}, mockProj, mockProj.AddItemGroup(),
                mockProj.AddPropertyGroup());
            new MigrateProjectDependenciesRule().Apply(testSettings, testInputs);

            var s = mockProj.Items.Select(p => $"ItemType = {p.ItemType}, Include = {p.Include}");
            Console.WriteLine(string.Join(Environment.NewLine, s));

            return mockProj;
        }
    }
}
