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

using System.CommandLine;
using FakeItEasy;
using Microsoft.TemplateEngine.Abstractions;
using Microsoft.TemplateEngine.Cli.Commands;
using Microsoft.TemplateEngine.Edge;
using Microsoft.TemplateEngine.Edge.Settings;
using Microsoft.TemplateEngine.Mocks;
using Microsoft.TemplateEngine.Utils;
using Newtonsoft.Json.Linq;

namespace Microsoft.TemplateEngine.Cli.UnitTests
{
    public class AliasAssignmentTests
    {
        private static HashSet<string> InitiallyTakenAliases
        {
            get
            {
                HashSet<string> initiallyTakenAliases = new()
                {
                    "-h", "--help",
                    "-l", "--list",
                    "-n", "--name",
                    "-o", "--output",
                    "-i", "--install",
                    "-u", "--uninstall",
                    "--type",
                    "--force",
                    "-lang", "--language",
                    "-a", "--alias",
                    "--show-alias",
                    "-x", "--extra-args",
                    "--quiet",
                    "-all", "--show-all",
                    "--allow-scripts",
                    "--baseline",
                    "-up", "--update",
                    "--skip-update-check"
                };

                return initiallyTakenAliases;
            }
        }

        // also asserts that "--param:<name>" is used if <name> is taken
        [Fact(DisplayName = nameof(LongNameOverrideTakesPrecendence))]
        public void LongNameOverrideTakesPrecendence()
        {
            IReadOnlyList<CliTemplateParameter> paramList = new List<CliTemplateParameter>()
            {
                new CliTemplateParameter("foo"),
                new CliTemplateParameter("bar", longNameOverrides: new[] { "foo" })
            };

            var result = AliasAssignmentCoordinator.AssignAliasesForParameter(paramList, InitiallyTakenAliases).ToDictionary(r => r.Parameter.Name, r => r);

            Assert.Contains("--param:foo", result["foo"].Aliases);
            Assert.Contains("-f", result["foo"].Aliases);
            Assert.Contains("--foo", result["bar"].Aliases);
            Assert.Contains("-fo", result["bar"].Aliases); // the short name is based on the long name override if it exists
            Assert.DoesNotContain(result, r => r.Value.Errors.Any());
        }

        [Fact(DisplayName = nameof(ShortNameOverrideTakesPrecedence))]
        public void ShortNameOverrideTakesPrecedence()
        {
            IReadOnlyList<CliTemplateParameter> paramList = new List<CliTemplateParameter>()
            {
                new CliTemplateParameter("foo"),
                new CliTemplateParameter("bar", shortNameOverrides: new[] { "f" })
            };

            var result = AliasAssignmentCoordinator.AssignAliasesForParameter(paramList, InitiallyTakenAliases).ToDictionary(r => r.Parameter.Name, r => r);

            Assert.Contains("--foo", result["foo"].Aliases);
            Assert.Contains("-fo", result["foo"].Aliases);
            Assert.Contains("--bar", result["bar"].Aliases);
            Assert.Contains("-f", result["bar"].Aliases);
            Assert.DoesNotContain(result, r => r.Value.Errors.Any());
        }

        [Fact(DisplayName = nameof(ShortNameExcludedWithEmptyStringOverride))]
        public void ShortNameExcludedWithEmptyStringOverride()
        {
            IReadOnlyList<CliTemplateParameter> paramList = new List<CliTemplateParameter>()
            {
                new CliTemplateParameter("foo"),
                new CliTemplateParameter("bar", shortNameOverrides: new[] { "" })
            };

            var result = AliasAssignmentCoordinator.AssignAliasesForParameter(paramList, InitiallyTakenAliases).ToDictionary(r => r.Parameter.Name, r => r);

            Assert.Contains("--foo", result["foo"].Aliases);
            Assert.Contains("-f", result["foo"].Aliases);
            Assert.Contains("--bar", result["bar"].Aliases);
            Assert.Single(result["bar"].Aliases);
            Assert.DoesNotContain(result, r => r.Value.Errors.Any());
        }

        [Fact(DisplayName = nameof(ParameterNameCannotContainColon))]
        public void ParameterNameCannotContainColon()
        {
            IReadOnlyList<CliTemplateParameter> paramList = new List<CliTemplateParameter>()
            {
                new CliTemplateParameter("foo:bar"),
            };

            var result = AliasAssignmentCoordinator.AssignAliasesForParameter(paramList, InitiallyTakenAliases).ToDictionary(r => r.Parameter.Name, r => r);
            Assert.Empty(result["foo:bar"].Aliases);
            Assert.Single(result["foo:bar"].Errors);
            Assert.Contains("Parameter name 'foo:bar' contains colon, which is forbidden.", result["foo:bar"].Errors);
        }

        [Fact(DisplayName = nameof(ShortNameGetPrependedPColonIfNeeded))]
        public void ShortNameGetPrependedPColonIfNeeded()
        {
            IReadOnlyList<CliTemplateParameter> paramList = new List<CliTemplateParameter>()
            {
                new CliTemplateParameter("bar", shortNameOverrides: new[] { "f" }),
                new CliTemplateParameter("f")
            };

            var result = AliasAssignmentCoordinator.AssignAliasesForParameter(paramList, InitiallyTakenAliases).ToDictionary(r => r.Parameter.Name, r => r);

            Assert.Contains("--bar", result["bar"].Aliases);
            Assert.Contains("-f", result["bar"].Aliases);
            Assert.Contains("--f", result["f"].Aliases);
            Assert.Contains("-p:f", result["f"].Aliases);
            Assert.DoesNotContain(result, r => r.Value.Errors.Any());
        }

        [Fact]
        public void ShortNameGenerationShouldNotProduceDuplicates()
        {
            List<CliTemplateParameter> paramList = new();
            for (int i = 0; i < 10; i++)
            {
                paramList.Add(new CliTemplateParameter("par" + i));
            }

            var result = AliasAssignmentCoordinator.AssignAliasesForParameter(paramList, InitiallyTakenAliases);

            result.SelectMany(p => p.Aliases).HasDuplicates().Should()
                .BeFalse("Duplicate option aliases should not be generated.");
        }

        [Fact]
        public void ShortNameSkippedAfter4Reps()
        {
            List<CliTemplateParameter> paramList = new();
            for (int i = 0; i < 8; i++)
            {
                paramList.Add(new CliTemplateParameter("par" + i));
            }

            var result = AliasAssignmentCoordinator.AssignAliasesForParameter(paramList, InitiallyTakenAliases);

            result[0].Aliases.Should().BeEquivalentTo(new[] { "-p", "--par0" });
            result[1].Aliases.Should().BeEquivalentTo(new[] { "-pa", "--par1" });
            result[2].Aliases.Should().BeEquivalentTo(new[] { "-p:p", "--par2" });
            result[3].Aliases.Should().BeEquivalentTo(new[] { "-p:pa", "--par3" });
            result[4].Aliases.Should().BeEquivalentTo(new[] { "--par4" });
            result[5].Aliases.Should().BeEquivalentTo(new[] { "--par5" });
            result[6].Aliases.Should().BeEquivalentTo(new[] { "--par6" });
            result[7].Aliases.Should().BeEquivalentTo(new[] { "--par7" });
        }

        // This reflects the MVC 2.0 tempalte as of May 24, 2017
        [Fact(DisplayName = nameof(CheckAliasAssignmentsMvc20))]
        public void CheckAliasAssignmentsMvc20()
        {
            IReadOnlyList<CliTemplateParameter> paramList = new List<CliTemplateParameter>()
            {
                new CliTemplateParameter("auth"),
                new CliTemplateParameter("AAdB2CInstance", longNameOverrides: new[] { "aad-b2c-instance" }, shortNameOverrides: new[] { "" }),
                new CliTemplateParameter("SignUpSignInPolicyId", longNameOverrides: new[] { "susi-policy-id" }, shortNameOverrides: new[] { "ssp" }),
                new CliTemplateParameter("ResetPasswordPolicyId", longNameOverrides: new[] { "reset-password-policy-id" }, shortNameOverrides: new[] { "rp" }),
                new CliTemplateParameter("EditProfilePolicyId", longNameOverrides: new[] { "edit-profile-policy-id" }, shortNameOverrides: new[] { "ep" }),
                new CliTemplateParameter("AADInstance", longNameOverrides: new[] { "aad-instance" }, shortNameOverrides: new[] { "" } ),
                new CliTemplateParameter("ClientId", longNameOverrides: new[] { "client-id" }, shortNameOverrides: new[] { "" }),
                new CliTemplateParameter("Domain", longNameOverrides: new[] { "domain" }, shortNameOverrides: new[] { "" }),
                new CliTemplateParameter("TenantId", longNameOverrides: new[] { "tenant-id" }, shortNameOverrides: new[] { "" }),
                new CliTemplateParameter("CallbackPath", longNameOverrides: new[] { "callback-path" }, shortNameOverrides: new[] { "" }),
                new CliTemplateParameter("OrgReadAccess", longNameOverrides: new[] { "org-read-access" }, shortNameOverrides: new[] { "r" }),
                new CliTemplateParameter("UserSecretsId"),
                new CliTemplateParameter("IncludeLaunchSettings"),
                new CliTemplateParameter("HttpsPort"),
                new CliTemplateParameter("KestrelPort"),
                new CliTemplateParameter("IISExpressPort"),
                new CliTemplateParameter("UseLocalDB", longNameOverrides: new[] { "use-local-db" }),
                new CliTemplateParameter("TargetFrameworkOverride", longNameOverrides: new[] { "target-framework-override" }, shortNameOverrides: new[] { "" }),
                new CliTemplateParameter("Framework", longNameOverrides: new[] { "framework" }),
                new CliTemplateParameter("NoTools", longNameOverrides: new[] { "no-tools" }),
                new CliTemplateParameter("skipRestore", longNameOverrides: new[] { "no-restore" }, shortNameOverrides: new[] { "" })
            };

            var result = AliasAssignmentCoordinator.AssignAliasesForParameter(paramList, InitiallyTakenAliases).ToDictionary(r => r.Parameter.Name, r => r);

            Assert.Contains("-au", result["auth"].Aliases);
            Assert.Contains("--auth", result["auth"].Aliases);
            Assert.Single(result["AAdB2CInstance"].Aliases);
            Assert.Contains("--aad-b2c-instance", result["AAdB2CInstance"].Aliases);
            Assert.Contains("-ssp", result["SignUpSignInPolicyId"].Aliases);
            Assert.Contains("--susi-policy-id", result["SignUpSignInPolicyId"].Aliases);
            Assert.Contains("-rp", result["ResetPasswordPolicyId"].Aliases);
            Assert.Contains("--reset-password-policy-id", result["ResetPasswordPolicyId"].Aliases);
            Assert.Contains("-ep", result["EditProfilePolicyId"].Aliases);
            Assert.Contains("--edit-profile-policy-id", result["EditProfilePolicyId"].Aliases);
            Assert.Single(result["AADInstance"].Aliases);
            Assert.Contains("--aad-instance", result["AADInstance"].Aliases);
            Assert.Single(result["ClientId"].Aliases);
            Assert.Contains("--client-id", result["ClientId"].Aliases);
            Assert.Single(result["Domain"].Aliases);
            Assert.Contains("--domain", result["Domain"].Aliases);
            Assert.Single(result["TenantId"].Aliases);
            Assert.Contains("--tenant-id", result["TenantId"].Aliases);
            Assert.Single(result["CallbackPath"].Aliases);
            Assert.Contains("--callback-path", result["CallbackPath"].Aliases);
            Assert.Contains("-r", result["OrgReadAccess"].Aliases);
            Assert.Contains("--org-read-access", result["OrgReadAccess"].Aliases);
            Assert.Contains("-U", result["UserSecretsId"].Aliases);
            Assert.Contains("--UserSecretsId", result["UserSecretsId"].Aliases);
            Assert.Contains("-I", result["IncludeLaunchSettings"].Aliases);
            Assert.Contains("--IncludeLaunchSettings", result["IncludeLaunchSettings"].Aliases);
            Assert.Contains("-H", result["HttpsPort"].Aliases);
            Assert.Contains("--HttpsPort", result["HttpsPort"].Aliases);
            Assert.Contains("-K", result["KestrelPort"].Aliases);
            Assert.Contains("--KestrelPort", result["KestrelPort"].Aliases);
            Assert.Contains("-II", result["IISExpressPort"].Aliases);
            Assert.Contains("--IISExpressPort", result["IISExpressPort"].Aliases);
            Assert.Contains("-uld", result["UseLocalDB"].Aliases);
            Assert.Contains("--use-local-db", result["UseLocalDB"].Aliases);
            Assert.Single(result["TargetFrameworkOverride"].Aliases);
            Assert.Contains("--target-framework-override", result["TargetFrameworkOverride"].Aliases);
            Assert.Contains("-f", result["Framework"].Aliases);
            Assert.Contains("--framework", result["Framework"].Aliases);
            Assert.Contains("-nt", result["NoTools"].Aliases);
            Assert.Contains("--no-tools", result["NoTools"].Aliases);
            Assert.Single(result["skipRestore"].Aliases);
            Assert.Contains("--no-restore", result["skipRestore"].Aliases);
            Assert.DoesNotContain(result, r => r.Value.Errors.Any());
        }

        [Theory]
        [InlineData("package", "--param:package")]
        [InlineData("u", "-p:u")]
        [InlineData("notreserved", "--notreserved")]
        public void CanAssignAliasForParameterWithReservedAlias(string parameterName, string expectedContainedAlias)
        {
            string command = "foo";
            MockTemplateInfo[] templates = new MockTemplateInfo[]
            {
                new MockTemplateInfo($"{command}", identity: "foo.1", groupIdentity: "foo.group").WithParameters(parameterName)
            };
            ICliTemplateEngineHost host = CliTestHostFactory.GetVirtualHost();
            IEngineEnvironmentSettings settings = new EngineEnvironmentSettings(host, virtualizeSettings: true);
            TemplatePackageManager templatePackageManager = A.Fake<TemplatePackageManager>();

            NewCommand myCommand = (NewCommand)NewCommandFactory.Create("new", _ => host);
            ParseResult parseResult = myCommand.Parse($" new {command}");
            var args = InstantiateCommandArgs.FromNewCommandArgs(new NewCommandArgs(myCommand, parseResult));
            TemplateGroup templateGroup = TemplateGroup
                .FromTemplateList(CliTemplateInfo.FromTemplateInfo(templates, A.Fake<IHostSpecificDataLoader>()))
                .Single();
            var templateCommands = InstantiateCommand.GetTemplateCommand(args, settings, A.Fake<TemplatePackageManager>(), templateGroup);
            Assert.Single(templateCommands);
            var templateOption = templateCommands.Single().TemplateOptions[parameterName];
            Assert.Contains(expectedContainedAlias, templateOption.Aliases);
        }

        [Theory]
        [MemberData(nameof(GetTemplateData))]
        public void CanOverrideAliasesForParameterWithHostData(string hostJsonData, string expectedJsonResult)
        {
            var hostData = new HostSpecificTemplateData(string.IsNullOrEmpty(hostJsonData) ? null : JObject.Parse(hostJsonData));
            var expectedResults = JObject.Parse(expectedJsonResult);
            var template = new MockTemplateInfo("foo", identity: "foo.1", groupIdentity: "foo.group");
            foreach (var expectedResult in expectedResults)
            {
                template.WithParameter(expectedResult.Key);
            }
            var hostDataLoader = A.Fake<IHostSpecificDataLoader>();
            A.CallTo(() => hostDataLoader.ReadHostSpecificTemplateData(template)).Returns(hostData);
            TemplateGroup templateGroup = TemplateGroup.FromTemplateList(
                CliTemplateInfo.FromTemplateInfo(new[] { template }, hostDataLoader))
                .Single();
            ICliTemplateEngineHost host = CliTestHostFactory.GetVirtualHost();
            IEngineEnvironmentSettings settings = new EngineEnvironmentSettings(host, virtualizeSettings: true);
            TemplatePackageManager templatePackageManager = A.Fake<TemplatePackageManager>();
            NewCommand myCommand = (NewCommand)NewCommandFactory.Create("new", _ => host);
            ParseResult parseResult = myCommand.Parse(" new foo");
            InstantiateCommandArgs args = InstantiateCommandArgs.FromNewCommandArgs(new NewCommandArgs(myCommand, parseResult));
            var templateCommands = InstantiateCommand.GetTemplateCommand(args, settings, templatePackageManager, templateGroup);
            Assert.Single(templateCommands);
            foreach (var expectedResult in expectedResults)
            {
                var expectedValues = expectedResult.Value!.Select(s => ((JValue)s).Value).ToArray();
                var expectedLongAlias = expectedValues[0];
                var expectedShortAlias = expectedValues[1];
                var expectedIsHidden = expectedValues[2];
                var templateOptions = templateCommands.Single().TemplateOptions;
                Assert.NotNull(templateOptions);
                Assert.Contains(expectedResult.Key, templateOptions.Keys);
                var templateOption = templateOptions[expectedResult.Key];
                Assert.NotNull(templateOption);
                Assert.True(templateOption.Aliases.Count > 0);
                var longAlias = templateOption.Aliases.ElementAt(0);
                var shortAlias = templateOption.Aliases.Count > 1 ? templateOption.Aliases.ElementAt(1) : null;
                var isHidden = templateOption.Option.Hidden;
                Assert.Equal(expectedLongAlias, longAlias);
                Assert.Equal(expectedShortAlias, shortAlias);
                Assert.Equal(expectedIsHidden, isHidden);
            }
        }

        public static IEnumerable<object[]> GetTemplateData()
        {
            // host data and expected option with long alias, short alias and if it is hidden:
            // [0] host data
            // [1] expected option : 0 - long alias, 1 - short alias, 2 - isHidden
            yield return new object[]
            {
                string.Empty,
                @"{ ""Framework"": [""--Framework"", ""-F"", false] }"
            };

            yield return new object[]
            {
                @"{
                  ""symbolInfo"": {
                    ""Framework"": {
                    }
                  }
                }",
                @"{ ""Framework"": [""--Framework"", ""-F"", false] }"
            };

            yield return new object[]
            {
                @"{
                  ""symbolInfo"": {
                    ""Framework"": {
                      ""longName"": ""targetframework""
                    }
                  }
                }",
                @"{ ""Framework"": [""--targetframework"", ""-t"", false] }"
            };

            yield return new object[]
            {
                @"{
                  ""symbolInfo"": {
                    ""Framework"": {
                      ""shortName"": ""fr""
                    }
                  }
                }",
                @"{ ""Framework"": [""--Framework"", ""-fr"", false] }"
            };

            yield return new object[]
            {
                @"{
                  ""symbolInfo"": {
                    ""Framework"": {
                      ""longName"": ""targetframework"",
                      ""shortName"": ""fr""
                    }
                  }
                }",
                @"{ ""Framework"": [""--targetframework"", ""-fr"", false] }"
            };

            yield return new object[]
            {
                @"{
                  ""symbolInfo"": {
                    ""Framework"": {
                      ""longName"": ""targetframework"",
                      ""shortName"": """"
                    }
                  }
                }",
                @"{ ""Framework"": [""--targetframework"", null, false] }"
            };

            yield return new object[]
            {
                @"{
                  ""symbolInfo"": {
                    ""Framework"": {
                      ""isHidden"": ""true"",
                      ""longName"": ""targetframework"",
                      ""shortName"": ""fr""
                    }
                  }
                }",
                @"{ ""Framework"": [""--targetframework"", ""-fr"", true] }"
            };

            yield return new object[]
            {
                @"{
                  ""symbolInfo"": {
                    ""Framework"": {
                      ""isHidden"": ""false"",
                      ""longName"": ""targetframework"",
                      ""shortName"": ""fr""
                    }
                  }
                }",
                @"{ ""Framework"": [""--targetframework"", ""-fr"", false] }"
            };

            yield return new object[]
            {
                @"{
                  ""symbolInfo"": {
                    ""install"": {
                      ""longName"": ""set""
                    }
                  }
                }",
                @"{ ""install"": [""--set"", ""-s"", false] }"
            };

            yield return new object[]
            {
                @"{
                  ""symbolInfo"": {
                    ""install"": {
                      ""longName"": ""setup"",
                      ""shortName"": ""set""
                    }
                  }
                }",
                @"{ ""install"": [""--setup"", ""-set"", false] }"
            };

            yield return new object[]
            {
                @"{
                  ""symbolInfo"": {
                    ""install"": {
                      ""longName"": ""set"",
                      ""shortName"": """"
                    }
                  }
                }",
                @"{ ""install"": [""--set"", null, false] }"
            };

            yield return new object[]
            {
                @"{
                  ""symbolInfo"": {
                    ""pack"": {
                      ""longName"": ""package""
                    }
                  }
                }",
                @"{ ""pack"": [""--param:package"", ""-p"", false] }"
            };

            yield return new object[]
            {
                @"{
                  ""symbolInfo"": {
                    ""add"": {
                      ""shortName"": ""i""
                    }
                  }
                }",
                @"{ ""add"": [""--add"", ""-p:i"", false] }"
            };

            yield return new object[]
            {
                @"{
                  ""symbolInfo"": {
                    ""delete"": {
                      ""longName"": ""remove""
                    }
                  }
                }",
                @"{
                  ""delete"": [""--remove"", ""-r"", false],
                  ""remove"": [""--param:remove"", ""-re"", false]
                }"
            };

            yield return new object[]
            {
                @"{
                  ""symbolInfo"": {
                    ""delete"": {
                      ""longName"": ""remove""
                    }
                  }
                }",
                @"{
                  ""remove"": [""--param:remove"", ""-r"", false],
                  ""delete"": [""--remove"", ""-re"", false]
                }"
            };
        }
    }
}
