// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System;
using System.Collections.Generic;
using System.Reflection;
using Xunit;

namespace Microsoft.AspNetCore.Mvc.ApplicationModels
{
    public class AttributeRouteModelTests
    {
        [Fact]
        public void CopyConstructor_CopiesAllProperties()
        {
            // Arrange
            var route = new AttributeRouteModel(new HttpGetAttribute("/api/Products"))
            {
                Name = "products",
                Order = 5,
                SuppressLinkGeneration = true,
                SuppressPathMatching = true,
            };

            // Act
            var route2 = new AttributeRouteModel(route);

            // Assert
            foreach (var property in typeof(AttributeRouteModel).GetProperties())
            {
                var value1 = property.GetValue(route);
                var value2 = property.GetValue(route2);

                if (typeof(IEnumerable<object>).GetTypeInfo().IsAssignableFrom(property.PropertyType.GetTypeInfo()))
                {
                    Assert.Equal<object>((IEnumerable<object>)value1, (IEnumerable<object>)value2);

                    // Ensure non-default value
                    Assert.NotEmpty((IEnumerable<object>)value1);
                }
                else if (property.PropertyType.GetTypeInfo().IsValueType ||
                    Nullable.GetUnderlyingType(property.PropertyType) != null)
                {
                    Assert.Equal(value1, value2);

                    // Ensure non-default value
                    Assert.NotEqual(value1, Activator.CreateInstance(property.PropertyType));
                }
                else
                {
                    Assert.Same(value1, value2);

                    // Ensure non-default value
                    Assert.NotNull(value1);
                }
            }
        }

        [Theory]
        [InlineData(null, null, null)]
        [InlineData("", null, "")]
        [InlineData(null, "", "")]
        [InlineData("/", null, "")]
        [InlineData(null, "/", "")]
        [InlineData("/", "", "")]
        [InlineData("", "/", "")]
        [InlineData("/", "/", "")]
        [InlineData("~/", null, "")]
        [InlineData("~/", "", "")]
        [InlineData("~/", "/", "")]
        [InlineData("~/", "~/", "")]
        [InlineData(null, "~/", "")]
        [InlineData("", "~/", "")]
        [InlineData("/", "~/", "")]
        public void Combine_EmptyTemplates(string left, string right, string expected)
        {
            // Arrange & Act
            var combined = AttributeRouteModel.CombineTemplates(left, right);

            // Assert
            Assert.Equal(expected, combined);
        }

        [Theory]
        [InlineData("home", null, "home")]
        [InlineData("home", "", "home")]
        [InlineData("/home/", "/", "")]
        [InlineData("/home/", "~/", "")]
        [InlineData(null, "GetEmployees", "GetEmployees")]
        [InlineData("/", "GetEmployees", "GetEmployees")]
        [InlineData("~/", "Blog/Index/", "Blog/Index")]
        [InlineData("", "/GetEmployees/{id}/", "GetEmployees/{id}")]
        [InlineData("~/home", null, "home")]
        [InlineData("~/home", "", "home")]
        [InlineData("~/home", "/", "")]
        [InlineData(null, "~/home", "home")]
        [InlineData("", "~/home", "home")]
        [InlineData("", "~/home/", "home")]
        [InlineData("/", "~/home", "home")]
        public void Combine_OneTemplateHasValue(string left, string right, string expected)
        {
            // Arrange & Act
            var combined = AttributeRouteModel.CombineTemplates(left, right);

            // Assert
            Assert.Equal(expected, combined);
        }

        [Theory]
        [InlineData("home", "About", "home/About")]
        [InlineData("home/", "/About", "About")]
        [InlineData("home/", "/About/", "About")]
        [InlineData("/home/{action}", "{id}", "home/{action}/{id}")]
        [InlineData("home", "~/index", "index")]
        [InlineData("home", "~/index/", "index")]
        public void Combine_BothTemplatesHasValue(string left, string right, string expected)
        {
            // Arrange & Act
            var combined = AttributeRouteModel.CombineTemplates(left, right);

            // Assert
            Assert.Equal(expected, combined);
        }

        [Theory]
        [InlineData("~~/", null, "~~")]
        [InlineData("~~/", "", "~~")]
        [InlineData("~~/", "//", "//")]
        [InlineData("~~/", "~~/", "~~/~~")]
        [InlineData("~~/", "home", "~~/home")]
        [InlineData("~~/", "home/", "~~/home")]
        [InlineData("//", null, "//")]
        [InlineData("//", "", "//")]
        [InlineData("//", "//", "//")]
        [InlineData("//", "~~/", "/~~")]
        [InlineData("//", "home", "/home")]
        [InlineData("//", "home/", "/home")]
        [InlineData("////", null, "//")]
        [InlineData("~~//", null, "~~/")]
        public void Combine_InvalidTemplates(string left, string right, string expected)
        {
            // Arrange & Act
            var combined = AttributeRouteModel.CombineTemplates(left, right);

            // Assert
            Assert.Equal(expected, combined);
        }

        [Theory]
        [MemberData(nameof(ReplaceTokens_ValueValuesData))]
        public void ReplaceTokens_ValidValues(string template, Dictionary<string, string> values, string expected)
        {
            // Arrange
            // Act
            var result = AttributeRouteModel.ReplaceTokens(template, values);

            // Assert
            Assert.Equal(expected, result);
        }

        [Theory]
        [MemberData(nameof(ReplaceTokens_InvalidFormatValuesData))]
        public void ReplaceTokens_InvalidFormat(string template, Dictionary<string, string> values, string reason)
        {
            // Arrange
            var expected = string.Format(
                "The route template '{0}' has invalid syntax. {1}",
                template,
                reason);

            // Act
            var ex = Assert.Throws<InvalidOperationException>(
                () => { AttributeRouteModel.ReplaceTokens(template, values); });

            // Assert
            Assert.Equal(expected, ex.Message);
        }

        [Theory]
        [InlineData("[area]/[controller]/[action]/{deptName:regex(^[a-zA-Z]{1}[a-zA-Z0-9_]*$)}", "a-zA-Z")]
        [InlineData("[area]/[controller]/[action2]", "action2")]
        public void ReplaceTokens_UnknownValue(string template, string token)
        {
            // Arrange
            var values = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
            {
                { "area", "Help" },
                { "controller", "Admin" },
                { "action", "SeeUsers" },
            };

            var expected =
                $"While processing template '{template}', " +
                $"a replacement value for the token '{token}' could not be found. " +
                "Available tokens: 'action, area, controller'. To use a '[' or ']' as a literal string in a " +
                "route or within a constraint, use '[[' or ']]' instead.";

            // Act
            var ex = Assert.Throws<InvalidOperationException>(
                () => { AttributeRouteModel.ReplaceTokens(template, values); });

            // Assert
            Assert.Equal(expected, ex.Message);
        }

        [Theory]
        [MemberData(nameof(CombineOrdersTestData))]
        public void Combine_Orders(
            AttributeRouteModel left,
            AttributeRouteModel right,
            int? expected)
        {
            // Arrange & Act
            var combined = AttributeRouteModel.CombineAttributeRouteModel(left, right);

            // Assert
            Assert.NotNull(combined);
            Assert.Equal(expected, combined.Order);
        }

        [Theory]
        [MemberData(nameof(ValidReflectedAttributeRouteModelsTestData))]
        public void Combine_ValidReflectedAttributeRouteModels(
            AttributeRouteModel left,
            AttributeRouteModel right,
            AttributeRouteModel expectedResult)
        {
            // Arrange & Act
            var combined = AttributeRouteModel.CombineAttributeRouteModel(left, right);

            // Assert
            Assert.NotNull(combined);
            Assert.Equal(expectedResult.Template, combined.Template);
        }

        [Theory]
        [MemberData(nameof(NullOrNullTemplateReflectedAttributeRouteModelTestData))]
        public void Combine_NullOrNullTemplateReflectedAttributeRouteModels(
            AttributeRouteModel left,
            AttributeRouteModel right)
        {
            // Arrange & Act
            var combined = AttributeRouteModel.CombineAttributeRouteModel(left, right);

            // Assert
            Assert.Null(combined);
        }

        [Theory]
        [MemberData(nameof(RightOverridesReflectedAttributeRouteModelTestData))]
        public void Combine_RightOverridesReflectedAttributeRouteModel(
            AttributeRouteModel left,
            AttributeRouteModel right)
        {
            // Arrange
            var expectedTemplate = AttributeRouteModel.CombineTemplates(null, right.Template);

            // Act
            var combined = AttributeRouteModel.CombineAttributeRouteModel(left, right);

            // Assert
            Assert.NotNull(combined);
            Assert.Equal(expectedTemplate, combined.Template);
            Assert.Equal(combined.Order, right.Order);
        }

        [Theory]
        [MemberData(nameof(CombineNamesTestData))]
        public void Combine_Names(
            AttributeRouteModel left,
            AttributeRouteModel right,
            string expectedName)
        {
            // Arrange & Act
            var combined = AttributeRouteModel.CombineAttributeRouteModel(left, right);

            // Assert
            Assert.NotNull(combined);
            Assert.Equal(expectedName, combined.Name);
        }

        [Fact]
        public void Combine_SetsSuppressLinkGenerationToFalse_IfNeitherIsTrue()
        {
            // Arrange
            var left = new AttributeRouteModel
            {
                Template = "Template"
            };
            var right = new AttributeRouteModel();
            var combined = AttributeRouteModel.CombineAttributeRouteModel(left, right);

            // Assert
            Assert.False(combined.SuppressLinkGeneration);
        }

        [Theory]
        [InlineData(false, true)]
        [InlineData(true, false)]
        [InlineData(true, true)]
        public void Combine_SetsSuppressLinkGenerationToTrue_IfEitherIsTrue(bool leftSuppress, bool rightSuppress)
        {
            // Arrange
            var left = new AttributeRouteModel
            {
                Template = "Template",
                SuppressLinkGeneration = leftSuppress,
            };
            var right = new AttributeRouteModel
            {
                SuppressLinkGeneration = rightSuppress,
            };
            var combined = AttributeRouteModel.CombineAttributeRouteModel(left, right);

            // Assert
            Assert.True(combined.SuppressLinkGeneration);
        }

        [Fact]
        public void Combine_SetsSuppressPathGenerationToFalse_IfNeitherIsTrue()
        {
            // Arrange
            var left = new AttributeRouteModel
            {
                Template = "Template",
            };
            var right = new AttributeRouteModel();
            var combined = AttributeRouteModel.CombineAttributeRouteModel(left, right);

            // Assert
            Assert.False(combined.SuppressPathMatching);
        }

        [Theory]
        [InlineData(false, true)]
        [InlineData(true, false)]
        [InlineData(true, true)]
        public void Combine_SetsSuppressPathGenerationToTrue_IfEitherIsTrue(bool leftSuppress, bool rightSuppress)
        {
            // Arrange
            var left = new AttributeRouteModel
            {
                Template = "Template",
                SuppressPathMatching = leftSuppress,
            };
            var right = new AttributeRouteModel
            {
                SuppressPathMatching = rightSuppress,
            };
            var combined = AttributeRouteModel.CombineAttributeRouteModel(left, right);

            // Assert
            Assert.True(combined.SuppressPathMatching);
        }

        public static IEnumerable<object[]> CombineNamesTestData
        {
            get
            {
                // AttributeRoute on the controller, attribute route on the action, expected name.
                var data = new TheoryData<AttributeRouteModel, AttributeRouteModel, string>();

                // Combined name is null if no name is provided.
                data.Add(Create(template: "/", order: null, name: null), null, null);
                data.Add(Create(template: "~/", order: null, name: null), null, null);
                data.Add(Create(template: "", order: null, name: null), null, null);
                data.Add(Create(template: "home", order: null, name: null), null, null);
                data.Add(Create(template: "/", order: 1, name: null), null, null);
                data.Add(Create(template: "~/", order: 1, name: null), null, null);
                data.Add(Create(template: "", order: 1, name: null), null, null);
                data.Add(Create(template: "home", order: 1, name: null), null, null);

                // Combined name is inherited if no right name is provided and the template is empty.
                data.Add(Create(template: "/", order: null, name: "Named"), null, "Named");
                data.Add(Create(template: "~/", order: null, name: "Named"), null, "Named");
                data.Add(Create(template: "", order: null, name: "Named"), null, "Named");
                data.Add(Create(template: "home", order: null, name: "Named"), null, "Named");
                data.Add(Create(template: "home", order: null, name: "Named"), Create(null, null, null), "Named");
                data.Add(Create(template: "", order: null, name: "Named"), Create("", null, null), "Named");

                // Order doesn't matter for combining the name.
                data.Add(Create(template: "", order: null, name: "Named"), Create("", 1, null), "Named");
                data.Add(Create(template: "", order: 1, name: "Named"), Create("", 1, null), "Named");
                data.Add(Create(template: "", order: 2, name: "Named"), Create("", 1, null), "Named");
                data.Add(Create(template: "", order: null, name: "Named"), Create("index", 1, null), null);
                data.Add(Create(template: "", order: 1, name: "Named"), Create("index", 1, null), null);
                data.Add(Create(template: "", order: 2, name: "Named"), Create("index", 1, null), null);
                data.Add(Create(template: "", order: null, name: "Named"), Create("", 1, "right"), "right");
                data.Add(Create(template: "", order: 1, name: "Named"), Create("", 1, "right"), "right");
                data.Add(Create(template: "", order: 2, name: "Named"), Create("", 1, "right"), "right");

                // Combined name is not inherited if right name is provided or the template is not empty.
                data.Add(Create(template: "/", order: null, name: "Named"), Create(null, null, "right"), "right");
                data.Add(Create(template: "~/", order: null, name: "Named"), Create(null, null, "right"), "right");
                data.Add(Create(template: "", order: null, name: "Named"), Create(null, null, "right"), "right");
                data.Add(Create(template: "home", order: null, name: "Named"), Create(null, null, "right"), "right");
                data.Add(Create(template: "home", order: null, name: "Named"), Create("index", null, null), null);
                data.Add(Create(template: "home", order: null, name: "Named"), Create("/", null, null), null);
                data.Add(Create(template: "home", order: null, name: "Named"), Create("~/", null, null), null);
                data.Add(Create(template: "home", order: null, name: "Named"), Create("index", null, "right"), "right");
                data.Add(Create(template: "home", order: null, name: "Named"), Create("/", null, "right"), "right");
                data.Add(Create(template: "home", order: null, name: "Named"), Create("~/", null, "right"), "right");
                data.Add(Create(template: "home", order: null, name: "Named"), Create("index", null, ""), "");

                return data;
            }
        }

        public static IEnumerable<object[]> CombineOrdersTestData
        {
            get
            {
                // AttributeRoute on the controller, attribute route on the action, expected order.
                var data = new TheoryData<AttributeRouteModel, AttributeRouteModel, int?>();

                data.Add(Create("", order: 1), Create("", order: 2), 2);
                data.Add(Create("", order: 1), Create("", order: null), 1);
                data.Add(Create("", order: 1), null, 1);
                data.Add(Create("", order: 1), Create("/", order: 2), 2);
                data.Add(Create("", order: 1), Create("/", order: null), null);
                data.Add(Create("", order: null), Create("", order: 2), 2);
                data.Add(Create("", order: null), Create("", order: null), null);
                data.Add(Create("", order: null), null, null);
                data.Add(Create("", order: null), Create("/", order: 2), 2);
                data.Add(Create("", order: null), Create("/", order: null), null);
                data.Add(null, Create("", order: 2), 2);
                data.Add(null, Create("", order: null), null);
                data.Add(null, Create("/", order: 2), 2);
                data.Add(null, Create("/", order: null), null);

                // We don't a test case for (left = null, right = null) as it is already tested in another test
                // and will produce a null ReflectedAttributeRouteModel, which complicates the test case.

                return data;
            }
        }

        public static IEnumerable<object[]> RightOverridesReflectedAttributeRouteModelTestData
        {
            get
            {
                // AttributeRoute on the controller, attribute route on the action.
                var data = new TheoryData<AttributeRouteModel, AttributeRouteModel>();
                var leftModel = Create("Home", order: 3);

                data.Add(leftModel, Create("/"));
                data.Add(leftModel, Create("~/"));
                data.Add(null, Create("/"));
                data.Add(null, Create("~/"));
                data.Add(Create(null), Create("/"));
                data.Add(Create(null), Create("~/"));

                return data;
            }
        }

        public static IEnumerable<object[]> NullOrNullTemplateReflectedAttributeRouteModelTestData
        {
            get
            {
                // AttributeRoute on the controller, attribute route on the action.
                var data = new TheoryData<AttributeRouteModel, AttributeRouteModel>();

                data.Add(null, null);
                data.Add(null, Create(null));
                data.Add(Create(null), null);
                data.Add(Create(null), Create(null));

                return data;
            }
        }

        public static IEnumerable<object[]> ValidReflectedAttributeRouteModelsTestData
        {
            get
            {
                // AttributeRoute on the controller, attribute route on the action, expected combined attribute route.
                var data = new TheoryData<AttributeRouteModel, AttributeRouteModel, AttributeRouteModel>();
                data.Add(null, Create("Index"), Create("Index"));
                data.Add(Create(null), Create("Index"), Create("Index"));
                data.Add(Create("Home"), null, Create("Home"));
                data.Add(Create("Home"), Create(null), Create("Home"));
                data.Add(Create("Home"), Create("Index"), Create("Home/Index"));
                data.Add(Create("Blog"), Create("/Index"), Create("Index"));

                return data;
            }
        }

        public static IEnumerable<object[]> ReplaceTokens_ValueValuesData
        {
            get
            {
                yield return new object[]
                {
                    "[controller]/[action]",
                    new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
                    {
                        { "controller", "Home" },
                        { "action", "Index" }
                    },
                    "Home/Index"
                };

                yield return new object[]
                {
                    "[controller]",
                    new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
                    {
                        { "controller", "Home" },
                        { "action", "Index" }
                    },
                    "Home"
                };

                yield return new object[]
                {
                    "[controller][[",
                    new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
                    {
                        { "controller", "Home" },
                        { "action", "Index" }
                    },
                    "Home["
                };

                yield return new object[]
                {
                    "[coNTroller]",
                    new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
                    {
                        { "controller", "Home" },
                        { "action", "Index" }
                    },
                    "Home"
                };

                yield return new object[]
                {
                    "thisisSomeText[action]",
                    new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
                    {
                        { "controller", "Home" },
                        { "action", "Index" }
                    },
                    "thisisSomeTextIndex"
                };

                yield return new object[]
                {
                    "[[-]][[/[[controller]]",
                    new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
                    {
                        { "controller", "Home" },
                        { "action", "Index" }
                    },
                    "[-][/[controller]"
                };

                yield return new object[]
                {
                    "[contr[[oller]/[act]]ion]",
                    new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
                    {
                        { "contr[oller", "Home" },
                        { "act]ion", "Index" }
                    },
                    "Home/Index"
                };

                yield return new object[]
                {
                    "[controller][action]",
                    new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
                    {
                        { "controller", "Home" },
                        { "action", "Index" }
                    },
                    "HomeIndex"
                };

                yield return new object[]
                {
                    "[contr}oller]/[act{ion]/{id}",
                    new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
                    {
                        { "contr}oller", "Home" },
                        { "act{ion", "Index" }
                    },
                    "Home/Index/{id}"
                };
            }
        }

        public static IEnumerable<object[]> ReplaceTokens_InvalidFormatValuesData
        {
            get
            {
                yield return new object[]
                {
                    "[",
                    new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase),
                    "A replacement token is not closed."
                };

                yield return new object[]
                {
                    "text]",
                    new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase),
                    "Token delimiters ('[', ']') are imbalanced.",
                };

                yield return new object[]
                {
                    "text]morecooltext",
                    new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase),
                    "Token delimiters ('[', ']') are imbalanced.",
                };

                yield return new object[]
                {
                    "[action",
                    new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase),
                    "A replacement token is not closed.",
                };

                yield return new object[]
                {
                    "[action]]][",
                    new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
                    {
                        { "action]", "Index" }
                    },
                    "A replacement token is not closed.",
                };

                yield return new object[]
                {
                    "[action]]",
                    new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase),
                    "A replacement token is not closed."
                };

                yield return new object[]
                {
                    "[ac[tion]",
                    new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase),
                    "An unescaped '[' token is not allowed inside of a replacement token. Use '[[' to escape."
                };

                yield return new object[]
                {
                    "[]",
                    new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase),
                    "An empty replacement token ('[]') is not allowed.",
                };
            }
        }

        private static AttributeRouteModel Create(string template, int? order = null, string name = null)
        {
            return new AttributeRouteModel
            {
                Template = template,
                Order = order,
                Name = name
            };
        }
    }
}