// 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.Globalization;
using System.Linq;
using System.Reflection;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc.Abstractions;
using Microsoft.AspNetCore.Mvc.ActionConstraints;
using Microsoft.AspNetCore.Mvc.ApplicationParts;
using Microsoft.AspNetCore.Mvc.Controllers;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.Mvc.Routing;
using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.Options;
using Moq;
using Xunit;

namespace Microsoft.AspNetCore.Mvc.ApplicationModels
{
    public class ControllerActionDescriptorProviderTests
    {
        [Fact]
        public void GetDescriptors_GetsDescriptorsOnlyForValidActions()
        {
            // Arrange
            var provider = GetProvider(typeof(PersonController).GetTypeInfo());

            // Act
            var descriptors = provider.GetDescriptors();
            var actionNames = descriptors.Select(ad => ad.ActionName);

            // Assert
            Assert.Equal(new[] { "GetPerson", "ShowPeople", }, actionNames);
        }

        [Fact]
        public void GetDescriptors_DisplayNameIncludesAssemblyName()
        {
            // Arrange
            var controllerTypeInfo = typeof(PersonController).GetTypeInfo();
            var provider = GetProvider(controllerTypeInfo);

            // Act
            var descriptors = provider.GetDescriptors();
            var descriptor = descriptors.Single(ad => ad.ActionName == nameof(PersonController.GetPerson));

            // Assert
            Assert.Equal($"{controllerTypeInfo.FullName}.{nameof(PersonController.GetPerson)} ({controllerTypeInfo.Assembly.GetName().Name})", descriptor.DisplayName);
        }

        [Fact]
        public void GetDescriptors_IncludesFilters()
        {
            // Arrange
            var globalFilter = new MyFilterAttribute(1);
            var provider = GetProvider(typeof(FiltersController).GetTypeInfo(), new IFilterMetadata[]
            {
                globalFilter,
            });

            // Act
            var descriptors = provider.GetDescriptors();
            var descriptor = Assert.Single(descriptors);

            // Assert
            Assert.Equal(3, descriptor.FilterDescriptors.Count);

            var filter1 = descriptor.FilterDescriptors[0];
            Assert.Same(globalFilter, filter1.Filter);
            Assert.Equal(FilterScope.Global, filter1.Scope);

            var filter2 = descriptor.FilterDescriptors[1];
            Assert.Equal(2, Assert.IsType<MyFilterAttribute>(filter2.Filter).Value);
            Assert.Equal(FilterScope.Controller, filter2.Scope);

            var filter3 = descriptor.FilterDescriptors[2];
            Assert.Equal(3, Assert.IsType<MyFilterAttribute>(filter3.Filter).Value);
            Assert.Equal(FilterScope.Action, filter3.Scope);
        }

        [Fact]
        public void GetDescriptors_AddsHttpMethodConstraints_ForConventionallyRoutedActions()
        {
            // Arrange
            var provider = GetProvider(typeof(HttpMethodController).GetTypeInfo());

            // Act
            var descriptors = provider.GetDescriptors();
            var descriptor = Assert.Single(descriptors);

            // Assert
            Assert.Equal("OnlyPost", descriptor.ActionName);

            var constraint = Assert.IsType<HttpMethodActionConstraint>(Assert.Single(descriptor.ActionConstraints));
            Assert.Equal(new string[] { "POST" }, constraint.HttpMethods);
        }

        [Fact]
        public void GetDescriptors_HttpMethodConstraint_RouteOnController()
        {
            // Arrange
            var provider = GetProvider(typeof(AttributeRoutedHttpMethodController).GetTypeInfo());

            // Act
            var descriptors = provider.GetDescriptors();
            var descriptor = Assert.Single(descriptors);

            // Assert
            Assert.Equal("Items", descriptor.AttributeRouteInfo.Template);

            var constraint = Assert.IsType<HttpMethodActionConstraint>(Assert.Single(descriptor.ActionConstraints));
            Assert.Equal(new string[] { "PUT", "PATCH" }, constraint.HttpMethods);
        }

        [Fact]
        public void GetDescriptors_AddsParameters_ToActionDescriptor()
        {
            // Arrange & Act
            var descriptors = GetDescriptors(
                typeof(ActionParametersController).GetTypeInfo());

            // Assert
            var main = Assert.Single(descriptors.Cast<ControllerActionDescriptor>(),
                d => d.ActionName.Equals(nameof(ActionParametersController.RequiredInt)));

            Assert.NotNull(main.Parameters);
            var id = Assert.Single(main.Parameters);

            Assert.Equal("id", id.Name);
            Assert.Null(id.BindingInfo?.BindingSource);
            Assert.Equal(typeof(int), id.ParameterType);
        }

        [Fact]
        public void GetDescriptors_AddsMultipleParameters_ToActionDescriptor()
        {
            // Arrange & Act
            var descriptors = GetDescriptors(
                typeof(ActionParametersController).GetTypeInfo());

            // Assert
            var main = Assert.Single(descriptors.Cast<ControllerActionDescriptor>(),
                d => d.ActionName.Equals(nameof(ActionParametersController.MultipleParameters)));

            Assert.NotNull(main.Parameters);
            var id = Assert.Single(main.Parameters, p => p.Name == "id");

            Assert.Equal("id", id.Name);
            Assert.Null(id.BindingInfo?.BindingSource);
            Assert.Equal(typeof(int), id.ParameterType);

            var entity = Assert.Single(main.Parameters, p => p.Name == "entity");

            Assert.Equal("entity", entity.Name);
            Assert.Equal(entity.BindingInfo.BindingSource, BindingSource.Body);
            Assert.Equal(typeof(TestActionParameter), entity.ParameterType);
        }

        [Fact]
        public void GetDescriptors_AddsMultipleParametersWithDifferentCasing_ToActionDescriptor()
        {
            // Arrange & Act
            var descriptors = GetDescriptors(
                typeof(ActionParametersController).GetTypeInfo());

            // Assert
            var main = Assert.Single(descriptors.Cast<ControllerActionDescriptor>(),
                d => d.ActionName.Equals(nameof(ActionParametersController.DifferentCasing)));

            Assert.NotNull(main.Parameters);
            var id = Assert.Single(main.Parameters, p => p.Name == "id");

            Assert.Equal("id", id.Name);
            Assert.Null(id.BindingInfo?.BindingSource);
            Assert.Equal(typeof(int), id.ParameterType);

            var upperCaseId = Assert.Single(main.Parameters, p => p.Name == "ID");

            Assert.Equal("ID", upperCaseId.Name);
            Assert.Null(upperCaseId.BindingInfo?.BindingSource);
            Assert.Equal(typeof(int), upperCaseId.ParameterType);

            var pascalCaseId = Assert.Single(main.Parameters, p => p.Name == "Id");

            Assert.Equal("Id", pascalCaseId.Name);
            Assert.Null(id.BindingInfo?.BindingSource);
            Assert.Equal(typeof(int), pascalCaseId.ParameterType);
        }

        [Fact]
        public void GetDescriptors_AddsParameters_DetectsFromBodyParameters()
        {
            // Arrange & Act
            var actionName = nameof(ActionParametersController.FromBodyParameter);

            var descriptors = GetDescriptors(
                typeof(ActionParametersController).GetTypeInfo());

            // Assert
            var fromBody = Assert.Single(descriptors.Cast<ControllerActionDescriptor>(),
                d => d.ActionName.Equals(actionName));

            Assert.NotNull(fromBody.Parameters);
            var entity = Assert.Single(fromBody.Parameters);

            Assert.Equal("entity", entity.Name);
            Assert.Equal(entity.BindingInfo.BindingSource, BindingSource.Body);
            Assert.Equal(typeof(TestActionParameter), entity.ParameterType);
        }

        [Fact]
        public void GetDescriptors_AddsParameters_DoesNotDetectParameterFromBody_IfNoFromBodyAttribute()
        {
            // Arrange & Act
            var actionName = nameof(ActionParametersController.NotFromBodyParameter);

            var descriptors = GetDescriptors(
                typeof(ActionParametersController).GetTypeInfo());

            // Assert
            var notFromBody = Assert.Single(descriptors.Cast<ControllerActionDescriptor>(),
                d => d.ActionName.Equals(actionName));

            Assert.NotNull(notFromBody.Parameters);
            var entity = Assert.Single(notFromBody.Parameters);

            Assert.Equal("entity", entity.Name);
            Assert.Null(entity.BindingInfo?.BindingSource);
            Assert.Equal(typeof(TestActionParameter), entity.ParameterType);
        }

        [Fact]
        public void GetDescriptors_AddsControllerAndActionConstraints_ToConventionallyRoutedActions()
        {
            // Arrange & Act
            var descriptors = GetDescriptors(
                typeof(ConventionallyRoutedController).GetTypeInfo());

            // Assert
            var action = Assert.Single(descriptors);

            Assert.NotNull(action.RouteValues);

            var controller = Assert.Single(action.RouteValues, kvp => kvp.Key.Equals("controller"));
            Assert.Equal("ConventionallyRouted", controller.Value);

            var actionConstraint = Assert.Single(action.RouteValues, kvp => kvp.Key.Equals("action"));
            Assert.Equal(nameof(ConventionallyRoutedController.ConventionalAction), actionConstraint.Value);
        }

        [Fact]
        public void GetDescriptors_EndpointMetadata_ContainsAttributesFromActionAndController()
        {
            // Arrange & Act
            var descriptors = GetDescriptors(
                typeof(AuthorizeController).GetTypeInfo());

            // Assert
            Assert.Equal(2, descriptors.Count());

            var anonymousAction = Assert.Single(descriptors, a => a.RouteValues["action"] == "AllowAnonymousAction");

            Assert.NotNull(anonymousAction.EndpointMetadata);

            Assert.Collection(anonymousAction.EndpointMetadata,
                metadata => Assert.IsType<AuthorizeAttribute>(metadata),
                metadata => Assert.IsType<AllowAnonymousAttribute>(metadata));

            var authorizeAction = Assert.Single(descriptors, a => a.RouteValues["action"] == "AuthorizeAction");

            Assert.NotNull(authorizeAction.EndpointMetadata);

            Assert.Collection(authorizeAction.EndpointMetadata,
                metadata => Assert.Equal("ControllerPolicy", Assert.IsType<AuthorizeAttribute>(metadata).Policy),
                metadata => Assert.Equal("ActionPolicy", Assert.IsType<AuthorizeAttribute>(metadata).Policy));
        }

        [Fact]
        public void GetDescriptors_ActionWithHttpMethods_AddedToEndpointMetadata()
        {
            // Arrange & Act
            var descriptors = GetDescriptors(
                typeof(AttributeRoutedController).GetTypeInfo());

            // Assert
            var action = Assert.Single(descriptors);

            Assert.NotNull(action.EndpointMetadata);

            Assert.Collection(action.EndpointMetadata,
                metadata => Assert.IsType<RouteAttribute>(metadata),
                metadata => Assert.IsType<HttpGetAttribute>(metadata),
                metadata =>
                {
                    var httpMethodMetadata = Assert.IsType<HttpMethodMetadata>(metadata);

                    Assert.False(httpMethodMetadata.AcceptCorsPreflight);
                    Assert.Equal("GET", Assert.Single(httpMethodMetadata.HttpMethods));
                });
        }

        [Fact]
        public void GetDescriptors_ActionWithMultipleHttpMethods_LastHttpMethodMetadata()
        {
            // Arrange & Act
            var descriptors = GetDescriptors(
                typeof(NonDuplicatedAttributeRouteController).GetTypeInfo());

            // Assert
            var actions = descriptors
                .OfType<ControllerActionDescriptor>()
                .Where(d => d.ActionName == nameof(NonDuplicatedAttributeRouteController.DifferentHttpMethods));

            Assert.Collection(actions,
                InspectElement("GET"),
                InspectElement("POST"),
                InspectElement("PUT"),
                InspectElement("PATCH"),
                InspectElement("DELETE"));

            Action<ControllerActionDescriptor> InspectElement(string httpMethod)
            {
                return (descriptor) =>
                {
                    var httpMethodAttribute = Assert.Single(descriptor.EndpointMetadata.OfType<HttpMethodAttribute>());
                    Assert.Equal(httpMethod, httpMethodAttribute.HttpMethods.Single(), ignoreCase: true);

                    var lastHttpMethodMetadata = descriptor.EndpointMetadata.OfType<IHttpMethodMetadata>().Last();
                    Assert.Equal(httpMethod, lastHttpMethodMetadata.HttpMethods.Single(), ignoreCase: true);
                    Assert.False(lastHttpMethodMetadata.AcceptCorsPreflight);
                };
            }
        }

        [Fact]
        public void GetDescriptors_AddsControllerAndActionDefaults_ToAttributeRoutedActions()
        {
            // Arrange & Act
            var descriptors = GetDescriptors(
                typeof(AttributeRoutedController).GetTypeInfo());

            // Assert
            var action = Assert.Single(descriptors);

            var controller = Assert.Single(action.RouteValues, kvp => kvp.Key.Equals("controller"));
            Assert.Equal("AttributeRouted", controller.Value);

            var actionConstraint = Assert.Single(action.RouteValues, kvp => kvp.Key.Equals("action"));
            Assert.Equal(nameof(AttributeRoutedController.AttributeRoutedAction), actionConstraint.Value);
        }

        [Fact]
        public void GetDescriptors_WithRouteValueAttribute()
        {
            // Arrange & Act
            var descriptors = GetDescriptors(
                typeof(HttpMethodController).GetTypeInfo(),
                typeof(RouteValueController).GetTypeInfo()).ToArray();

            var descriptorWithoutValue = Assert.Single(
                descriptors,
                ad => !ad.RouteValues.ContainsKey("key"));

            var descriptorWithValue = Assert.Single(
                descriptors,
                ad => ad.RouteValues.ContainsKey("key"));

            // Assert
            Assert.Equal(2, descriptors.Length);

            Assert.Equal(3, descriptorWithValue.RouteValues.Count);
            Assert.Single(
                descriptorWithValue.RouteValues,
                c =>
                    c.Key == "controller" &&
                    c.Value == "RouteValue");
            Assert.Single(
                descriptorWithValue.RouteValues,
                c =>
                    c.Key == "action" &&
                    c.Value == "Edit");
            Assert.Single(
                descriptorWithValue.RouteValues,
                c =>
                    c.Key == "key" &&
                    c.Value == "value");

            Assert.Equal(2, descriptorWithoutValue.RouteValues.Count);
            Assert.Single(
                descriptorWithoutValue.RouteValues,
                c =>
                    c.Key == "controller" &&
                    c.Value == "HttpMethod");
            Assert.Single(
                descriptorWithoutValue.RouteValues,
                c =>
                    c.Key == "action" &&
                    c.Value == "OnlyPost");
        }

        [Fact]
        public void AttributeRouting_TokenReplacement_InActionDescriptor()
        {
            // Arrange
            var provider = GetProvider(typeof(TokenReplacementController).GetTypeInfo());

            // Act
            var actions = provider.GetDescriptors();

            // Assert
            var action = Assert.Single(actions);
            Assert.Equal("api/Token/value/TokenReplacement/stub/ThisIsAnAction", action.AttributeRouteInfo.Template);
        }

        [Fact]
        public void AttributeRouting_TokenReplacement_ThrowsWithMultipleMessages()
        {
            // Arrange
            var controllerTypeInfo = typeof(MultipleErrorsController).GetTypeInfo();
            var assemblyName = controllerTypeInfo.Assembly.GetName().Name;
            var provider = GetProvider(controllerTypeInfo);

            var expectedMessage =
                "The following errors occurred with attribute routing information:" + Environment.NewLine +
                Environment.NewLine +
                "Error 1:" + Environment.NewLine +
                $"For action: '{controllerTypeInfo.FullName}.Unknown ({assemblyName})'" + Environment.NewLine +
                "Error: While processing template 'stub/[action]/[unknown]', a replacement value for the token 'unknown' " +
                "could not be found. Available tokens: 'action, controller'. To use a '[' or ']' as a literal string in" +
                " a route or within a constraint, use '[[' or ']]' instead." + Environment.NewLine +
                Environment.NewLine +
                "Error 2:" + Environment.NewLine +
                $"For action: '{controllerTypeInfo.FullName}.Invalid ({assemblyName})'" + Environment.NewLine +
                "Error: The route template '[invalid/syntax' has invalid syntax. A replacement token is not closed.";

            // Act
            var ex = Assert.Throws<InvalidOperationException>(() => { provider.GetDescriptors(); });

            // Assert
            VerifyMultiLineError(expectedMessage, ex.Message, unorderedStart: 2, unorderedLineCount: 6);
        }

        [Fact]
        public void AttributeRouting_CreatesOneActionDescriptor_PerControllerAndActionRouteCombination()
        {
            // Arrange
            var provider = GetProvider(typeof(MultiRouteAttributesController).GetTypeInfo());

            // Act
            var descriptors = provider.GetDescriptors();

            // Assert
            var actions = descriptors.Where(d => d.ActionName == "MultipleHttpGet");
            Assert.Equal(4, actions.Count());

            foreach (var action in actions)
            {
                Assert.Equal("MultipleHttpGet", action.ActionName);
                Assert.Equal("MultiRouteAttributes", action.ControllerName);
            }

            Assert.Single(actions, a => a.AttributeRouteInfo.Template.Equals("v1/List"));
            Assert.Single(actions, a => a.AttributeRouteInfo.Template.Equals("v1/All"));
            Assert.Single(actions, a => a.AttributeRouteInfo.Template.Equals("v2/List"));
            Assert.Single(actions, a => a.AttributeRouteInfo.Template.Equals("v2/All"));
        }

        [Fact]
        public void AttributeRouting_AcceptVerbsOnAction_CreatesActionPerControllerAttributeRouteCombination()
        {
            // Arrange
            var provider = GetProvider(typeof(MultiRouteAttributesController).GetTypeInfo());

            // Act
            var descriptors = provider.GetDescriptors();

            // Assert
            var actions = descriptors.Where(d => d.ActionName == nameof(MultiRouteAttributesController.AcceptVerbs));
            Assert.Equal(2, actions.Count());

            foreach (var action in actions)
            {
                Assert.Equal("MultiRouteAttributes", action.ControllerName);

                Assert.NotNull(action.ActionConstraints);
                var methodConstraint = Assert.IsType<HttpMethodActionConstraint>(Assert.Single(action.ActionConstraints));

                Assert.NotNull(methodConstraint.HttpMethods);
                Assert.Equal(new[] { "POST" }, methodConstraint.HttpMethods);
            }

            Assert.Single(actions, a => a.AttributeRouteInfo.Template.Equals("v1/List"));
            Assert.Single(actions, a => a.AttributeRouteInfo.Template.Equals("v2/List"));
        }

        [Fact]
        public void AttributeRouting_AcceptVerbsOnActionWithOverrideTemplate_CreatesSingleAttributeRoutedAction()
        {
            // Arrange
            var provider = GetProvider(typeof(MultiRouteAttributesController).GetTypeInfo());

            // Act
            var descriptors = provider.GetDescriptors();

            // Assert
            var action = Assert.Single(descriptors, d => d.ActionName == "AcceptVerbsOverride");

            Assert.Equal("MultiRouteAttributes", action.ControllerName);

            Assert.NotNull(action.ActionConstraints);
            var methodConstraint = Assert.IsType<HttpMethodActionConstraint>(Assert.Single(action.ActionConstraints));

            Assert.NotNull(methodConstraint.HttpMethods);
            Assert.Equal(new[] { "PUT" }, methodConstraint.HttpMethods);

            Assert.NotNull(action.AttributeRouteInfo);
            Assert.Equal("Override", action.AttributeRouteInfo.Template);
        }

        [Fact]
        public void AttributeRouting_AcceptVerbsOnAction_WithoutTemplate_MergesVerb()
        {
            // Arrange
            var provider = GetProvider(typeof(MultiRouteAttributesController).GetTypeInfo());

            // Act
            var descriptors = provider.GetDescriptors();

            // Assert
            var actions = descriptors.Where(d => d.ActionName == "AcceptVerbsRouteAttributeAndHttpPut");
            Assert.Equal(4, actions.Count());

            foreach (var action in actions)
            {
                Assert.Equal("MultiRouteAttributes", action.ControllerName);

                Assert.NotNull(action.AttributeRouteInfo);
                Assert.NotNull(action.AttributeRouteInfo.Template);
            }

            var constrainedActions = actions.Where(a => a.ActionConstraints != null);
            Assert.Equal(4, constrainedActions.Count());

            // Actions generated by PutAttribute
            var putActions = constrainedActions.Where(
                a => a.ActionConstraints.OfType<HttpMethodActionConstraint>().Single().HttpMethods.Single() == "PUT");
            Assert.Equal(2, putActions.Count());
            Assert.Single(putActions, a => a.AttributeRouteInfo.Template.Equals("v1/All"));
            Assert.Single(putActions, a => a.AttributeRouteInfo.Template.Equals("v2/All"));

            // Actions generated by RouteAttribute
            var routeActions = actions.Where(
                a => a.ActionConstraints.OfType<HttpMethodActionConstraint>().Single().HttpMethods.Single() == "POST");
            Assert.Equal(2, routeActions.Count());
            Assert.Single(routeActions, a => a.AttributeRouteInfo.Template.Equals("v1/List"));
            Assert.Single(routeActions, a => a.AttributeRouteInfo.Template.Equals("v2/List"));
        }

        [Fact]
        public void AttributeRouting_AcceptVerbsOnAction_WithTemplate_DoesNotMergeVerb()
        {
            // Arrange
            var provider = GetProvider(typeof(MultiRouteAttributesController).GetTypeInfo());

            // Act
            var descriptors = provider.GetDescriptors();

            // Assert
            var actions = descriptors.Where(d => d.ActionName == "AcceptVerbsRouteAttributeWithTemplateAndHttpPut");
            Assert.Equal(6, actions.Count());

            foreach (var action in actions)
            {
                Assert.Equal("MultiRouteAttributes", action.ControllerName);

                Assert.NotNull(action.AttributeRouteInfo);
                Assert.NotNull(action.AttributeRouteInfo.Template);
            }

            var constrainedActions = actions.Where(a => a.ActionConstraints != null);
            Assert.Equal(4, constrainedActions.Count());

            // Actions generated by AcceptVerbs
            var postActions = constrainedActions.Where(
                a => a.ActionConstraints.OfType<HttpMethodActionConstraint>().Single().HttpMethods.Single() == "POST");
            Assert.Equal(2, postActions.Count());
            Assert.Single(postActions, a => a.AttributeRouteInfo.Template.Equals("v1"));
            Assert.Single(postActions, a => a.AttributeRouteInfo.Template.Equals("v2"));

            // Actions generated by PutAttribute
            var putActions = constrainedActions.Where(
                a => a.ActionConstraints.OfType<HttpMethodActionConstraint>().Single().HttpMethods.Single() == "PUT");
            Assert.Equal(2, putActions.Count());
            Assert.Single(putActions, a => a.AttributeRouteInfo.Template.Equals("v1/All"));
            Assert.Single(putActions, a => a.AttributeRouteInfo.Template.Equals("v2/All"));

            // Actions generated by RouteAttribute
            var unconstrainedActions = actions.Where(a => a.ActionConstraints == null);
            Assert.Equal(2, unconstrainedActions.Count());
            Assert.Single(unconstrainedActions, a => a.AttributeRouteInfo.Template.Equals("v1/List"));
            Assert.Single(unconstrainedActions, a => a.AttributeRouteInfo.Template.Equals("v2/List"));
        }

        [Fact]
        public void AttributeRouting_AllowsDuplicateAttributeRoutedActions_WithTheSameTemplateAndSameHttpMethodsOnDifferentActions()
        {
            // Arrange
            var provider = GetProvider(typeof(NonDuplicatedAttributeRouteController).GetTypeInfo());
            var firstActionName = nameof(NonDuplicatedAttributeRouteController.ControllerAndAction);
            var secondActionName = nameof(NonDuplicatedAttributeRouteController.OverrideOnAction);

            // Act
            var actions = provider.GetDescriptors();

            // Assert
            var controllerAndAction = Assert.Single(actions, a => a.ActionName.Equals(firstActionName));
            Assert.NotNull(controllerAndAction.AttributeRouteInfo);

            var controllerActionAndOverride = Assert.Single(actions, a => a.ActionName.Equals(secondActionName));
            Assert.NotNull(controllerActionAndOverride.AttributeRouteInfo);

            Assert.Equal(
                controllerAndAction.AttributeRouteInfo.Template,
                controllerActionAndOverride.AttributeRouteInfo.Template,
                StringComparer.OrdinalIgnoreCase);
        }

        [Fact]
        public void AttributeRouting_AllowsDuplicateAttributeRoutedActions_WithTheSameTemplateAndDifferentHttpMethodsOnTheSameAction()
        {
            // Arrange
            var provider = GetProvider(typeof(NonDuplicatedAttributeRouteController).GetTypeInfo());
            var actionName = nameof(NonDuplicatedAttributeRouteController.DifferentHttpMethods);

            // Act
            var descriptors = provider.GetDescriptors();

            // Assert
            var actions = descriptors.Where(d => d.ActionName.Equals(actionName));
            Assert.Equal(5, actions.Count());

            foreach (var method in new[] { "GET", "POST", "PUT", "PATCH", "DELETE" })
            {
                var action = Assert.Single(
                    actions,
                    a => a.ActionConstraints
                        .OfType<HttpMethodActionConstraint>()
                        .SelectMany(c => c.HttpMethods)
                        .Contains(method));

                Assert.NotNull(action.AttributeRouteInfo);
                Assert.Equal("Products/list", action.AttributeRouteInfo.Template);
            }
        }

        [Fact]
        public void AttributeRouting_ThrowsIfAttributeRoutedAndNonAttributedActions_OnTheSameMethod()
        {
            // Arrange
            var controllerTypeInfo = typeof(AttributeAndNonAttributeRoutedActionsOnSameMethodController).GetTypeInfo();
            var assemblyName = controllerTypeInfo.Assembly.GetName().Name;

            var expectedMessage =
                "The following errors occurred with attribute routing information:" + Environment.NewLine +
                Environment.NewLine +
                "Error 1:" + Environment.NewLine +
                $"A method '{controllerTypeInfo.FullName}.Method ({assemblyName})'" +
                " must not define attribute routed actions and non attribute routed actions at the same time:" + Environment.NewLine +
                $"Action: '{controllerTypeInfo.FullName}.Method ({assemblyName})' " +
                "- Route Template: 'AttributeRouted' - " +
                "HTTP Verbs: 'GET'" + Environment.NewLine +
                $"Action: '{controllerTypeInfo.FullName}.Method ({assemblyName})' - " +
                "Route Template: '(none)' - HTTP Verbs: 'DELETE, PATCH, POST, PUT'" + Environment.NewLine +
                Environment.NewLine +
                "Use 'AcceptVerbsAttribute' to create a single route that allows multiple HTTP verbs and defines a " +
                "route, or set a route template in all attributes that constrain HTTP verbs.";

            var provider = GetProvider(controllerTypeInfo);

            // Act
            var exception = Assert.Throws<InvalidOperationException>(() => provider.GetDescriptors());

            // Assert
            VerifyMultiLineError(expectedMessage, exception.Message, unorderedStart: 1, unorderedLineCount: 2);
        }

        // Verify that the expected exception and error message is thrown even when the user builds the model
        // incorrectly.
        [Fact]
        public void AttributeRouting_ThrowsIfAttributeRoutedAndNonAttributedActions_OnTheSameMethod_UsingCustomConvention()
        {
            // Arrange
            var controllerTypeInfo = typeof(UserController).GetTypeInfo();
            var manager = GetApplicationManager(new[] { controllerTypeInfo });

            var options = Options.Create(new MvcOptions());

            var modelProvider = new DefaultApplicationModelProvider(options, new EmptyModelMetadataProvider());
            var provider = new ControllerActionDescriptorProvider(
                manager,
                new ApplicationModelFactory(new[] { modelProvider }, options));

            var assemblyName = controllerTypeInfo.Assembly.GetName().Name;
            var expectedMessage =
                "The following errors occurred with attribute routing information:"
                + Environment.NewLine
                + Environment.NewLine
                + "Error 1:"
                + Environment.NewLine
                + $"A method '{controllerTypeInfo.FullName}.GetUser ({assemblyName})'" +
                " must not define attribute routed actions and non attribute routed actions at the same time:"
                + Environment.NewLine +
                $"Action: '{controllerTypeInfo.FullName}.GetUser ({assemblyName})' " +
                "- Route Template: '(none)' - " +
                "HTTP Verbs: ''"
                + Environment.NewLine
                + $"Action: '{controllerTypeInfo.FullName}.GetUser ({assemblyName})' " +
                "- Route Template: '!!!' - " +
                "HTTP Verbs: ''" + Environment.NewLine +
                Environment.NewLine +
                "Use 'AcceptVerbsAttribute' to create a single route that allows multiple HTTP verbs and defines a " +
                "route, or set a route template in all attributes that constrain HTTP verbs.";

            // Act
            var exception = Assert.Throws<InvalidOperationException>(() => provider.GetDescriptors());

            // Assert
            VerifyMultiLineError(expectedMessage, exception.Message, unorderedStart: 1, unorderedLineCount: 2);
        }

        [Fact]
        public void AttributeRouting_RouteOnControllerAndAction_CreatesActionDescriptorWithoutHttpConstraints()
        {
            // Arrange
            var provider = GetProvider(typeof(OnlyRouteController).GetTypeInfo());

            // Act
            var actions = provider.GetDescriptors();

            // Assert
            var action = Assert.Single(actions);

            Assert.Equal("Action", action.ActionName);
            Assert.Equal("OnlyRoute", action.ControllerName);

            Assert.NotNull(action.AttributeRouteInfo);
            Assert.Equal("Products/Index", action.AttributeRouteInfo.Template);

            Assert.Null(action.ActionConstraints);
        }

        [Fact]
        public void AttributeRouting_Name_ThrowsIfMultipleActions_WithDifferentTemplatesHaveTheSameName()
        {
            // Arrange
            var sameNameType = typeof(SameNameDifferentTemplatesController).GetTypeInfo();
            var provider = GetProvider(sameNameType);

            var assemblyName = sameNameType.Assembly.GetName().Name;
            var expectedMessage =
                "The following errors occurred with attribute routing information:"
                + Environment.NewLine + Environment.NewLine +
                "Error 1:" + Environment.NewLine +
                "Attribute routes with the same name 'Products' must have the same template:"
                + Environment.NewLine +
                $"Action: '{sameNameType.FullName}.Get ({assemblyName})' - Template: 'Products'"
                + Environment.NewLine +
                $"Action: '{sameNameType.FullName}.Get ({assemblyName})' - Template: 'Products/{{id}}'"
                + Environment.NewLine +
                $"Action: '{sameNameType.FullName}.Put ({assemblyName})' - Template: 'Products/{{id}}'"
                + Environment.NewLine +
                $"Action: '{sameNameType.FullName}.Post ({assemblyName})' - Template: 'Products'"
                + Environment.NewLine +
                $"Action: '{sameNameType.FullName}.Delete ({assemblyName})' - Template: 'Products/{{id}}'"
                + Environment.NewLine + Environment.NewLine +
                "Error 2:" + Environment.NewLine +
                "Attribute routes with the same name 'Items' must have the same template:"
                + Environment.NewLine +
                $"Action: '{sameNameType.FullName}.GetItems ({assemblyName})' - Template: 'Items/{{id}}'"
                + Environment.NewLine +
                $"Action: '{sameNameType.FullName}.PostItems ({assemblyName})' - Template: 'Items'"
                + Environment.NewLine +
                $"Action: '{sameNameType.FullName}.PutItems ({assemblyName})' - Template: 'Items/{{id}}'"
                + Environment.NewLine +
                $"Action: '{sameNameType.FullName}.DeleteItems ({assemblyName})' - Template: 'Items/{{id}}'"
                + Environment.NewLine +
                $"Action: '{sameNameType.FullName}.PatchItems ({assemblyName})' - Template: 'Items'";

            // Act
            var ex = Assert.Throws<InvalidOperationException>(() => { provider.GetDescriptors(); });

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

        [Fact]
        public void AttributeRouting_Name_AllowsMultipleAttributeRoutesInDifferentActions_WithTheSameNameAndTemplate()
        {
            // Arrange
            var provider = GetProvider(typeof(DifferentCasingsAttributeRouteNamesController).GetTypeInfo());

            // Act
            var descriptors = provider.GetDescriptors();

            // Assert
            foreach (var descriptor in descriptors)
            {
                Assert.NotNull(descriptor.AttributeRouteInfo);
                Assert.Equal("{id}", descriptor.AttributeRouteInfo.Template, StringComparer.OrdinalIgnoreCase);
                Assert.Equal("Products", descriptor.AttributeRouteInfo.Name, StringComparer.OrdinalIgnoreCase);
            }
        }

        [Fact]
        public void AttributeRouting_RouteNameTokenReplace_AllowsMultipleActions_WithSameRouteNameTemplate()
        {
            // Arrange
            var provider = GetProvider(typeof(ActionRouteNameTemplatesController).GetTypeInfo());
            var editActionName = nameof(ActionRouteNameTemplatesController.Edit);
            var getActionName = nameof(ActionRouteNameTemplatesController.Get);

            // Act
            var actions = provider.GetDescriptors();

            // Assert
            var getActions = actions.Where(a => a.ActionName.Equals(getActionName));
            Assert.Equal(2, getActions.Count());

            foreach (var getAction in getActions)
            {
                Assert.NotNull(getAction.AttributeRouteInfo);
                Assert.Equal("Products/Get", getAction.AttributeRouteInfo.Template, StringComparer.OrdinalIgnoreCase);
                Assert.Equal("Products_Get", getAction.AttributeRouteInfo.Name, StringComparer.OrdinalIgnoreCase);
            }

            var editAction = Assert.Single(actions, a => a.ActionName.Equals(editActionName));
            Assert.NotNull(editAction.AttributeRouteInfo);
            Assert.Equal("Products/Edit", editAction.AttributeRouteInfo.Template, StringComparer.OrdinalIgnoreCase);
            Assert.Equal("Products_Edit", editAction.AttributeRouteInfo.Name, StringComparer.OrdinalIgnoreCase);
        }

        [Fact]
        public void AttributeRouting_RouteNameTokenReplace_AreaControllerActionTokensInRoute()
        {
            // Arrange
            var provider = GetProvider(typeof(ControllerActionRouteNameTemplatesController).GetTypeInfo());
            var editActionName = nameof(ControllerActionRouteNameTemplatesController.Edit);
            var getActionName = nameof(ControllerActionRouteNameTemplatesController.Get);

            // Act
            var actions = provider.GetDescriptors();

            // Assert
            var getActions = actions.Where(a => a.ActionName.Equals(getActionName));
            Assert.Equal(2, getActions.Count());

            foreach (var getAction in getActions)
            {
                Assert.NotNull(getAction.AttributeRouteInfo);
                Assert.Equal(
                    "ControllerActionRouteNameTemplates/Get",
                    getAction.AttributeRouteInfo.Template, StringComparer.OrdinalIgnoreCase);
                Assert.Equal(
                    "Products_ControllerActionRouteNameTemplates_Get",
                    getAction.AttributeRouteInfo.Name, StringComparer.OrdinalIgnoreCase);
            }

            var editAction = Assert.Single(actions, a => a.ActionName.Equals(editActionName));
            Assert.NotNull(editAction.AttributeRouteInfo);
            Assert.Equal(
                "ControllerActionRouteNameTemplates/Edit",
                editAction.AttributeRouteInfo.Template, StringComparer.OrdinalIgnoreCase);
            Assert.Equal(
                "Products_ControllerActionRouteNameTemplates_Edit",
                editAction.AttributeRouteInfo.Name, StringComparer.OrdinalIgnoreCase);
        }

        [Fact]
        public void AttributeRouting_RouteNameTokenReplace_InvalidToken()
        {
            // Arrange
            var controllerTypeInfo = typeof(RouteNameIncorrectTokenController).GetTypeInfo();
            var assemblyName = controllerTypeInfo.Assembly.GetName().Name;

            var provider = GetProvider(controllerTypeInfo);

            var expectedMessage =
                "The following errors occurred with attribute routing information:" + Environment.NewLine +
                Environment.NewLine +
                "Error 1:" + Environment.NewLine +
                $"For action: '{controllerTypeInfo.FullName}.Get ({assemblyName})'" + Environment.NewLine +
                "Error: While processing template 'Products_[unknown]', a replacement value for the token 'unknown' " +
                "could not be found. Available tokens: 'action, controller'. To use a '[' or ']' as a literal string" +
                " in a route or within a constraint, use '[[' or ']]' instead.";

            // Act & Assert
            var ex = Assert.Throws<InvalidOperationException>(() => { provider.GetDescriptors(); });
            Assert.Equal(expectedMessage, ex.Message);
        }

        [Fact]
        public void AttributeRouting_AddsDefaultRouteValues_ForAttributeRoutedActions()
        {
            // Arrange
            var provider = GetProvider(
                typeof(ConventionalAndAttributeRoutedActionsWithAreaController).GetTypeInfo(),
                typeof(ConstrainedController).GetTypeInfo());

            // Act
            var actionDescriptors = provider.GetDescriptors();

            // Assert
            Assert.NotNull(actionDescriptors);
            Assert.Equal(4, actionDescriptors.Count());

            var indexAction = Assert.Single(actionDescriptors, ad => ad.ActionName.Equals("Index"));

            Assert.Equal(3, indexAction.RouteValues.Count);

            var controllerDefault = Assert.Single(indexAction.RouteValues, rd => rd.Key.Equals("controller", StringComparison.OrdinalIgnoreCase));
            Assert.Equal("ConventionalAndAttributeRoutedActionsWithArea", controllerDefault.Value);

            var actionDefault = Assert.Single(indexAction.RouteValues, rd => rd.Key.Equals("action", StringComparison.OrdinalIgnoreCase));
            Assert.Equal("Index", actionDefault.Value);

            var areaDefault = Assert.Single(indexAction.RouteValues, rd => rd.Key.Equals("area", StringComparison.OrdinalIgnoreCase));
            Assert.Equal("Home", areaDefault.Value);
        }

        [Fact]
        public void AttributeRouting_TokenReplacement_CaseInsensitive()
        {
            // Arrange
            var provider = GetProvider(typeof(CaseInsensitiveController).GetTypeInfo());

            // Act
            var actions = provider.GetDescriptors();

            // Assert
            var action = Assert.Single(actions);
            Assert.Equal("stub/ThisIsAnAction", action.AttributeRouteInfo.Template);
        }

        // Parameters are validated later. This action uses the forbidden {action} and {controller}
        [Fact]
        public void AttributeRouting_DoesNotValidateParameters()
        {
            // Arrange
            var provider = GetProvider(typeof(InvalidParametersController).GetTypeInfo());

            // Act
            var actions = provider.GetDescriptors();

            // Assert
            var action = Assert.Single(actions);
            Assert.Equal("stub/{controller}/{action}", action.AttributeRouteInfo.Template);
        }

        [Fact]
        public void ApiExplorer_SetsExtensionData_WhenVisible()
        {
            // Arrange
            var provider = GetProvider(typeof(ApiExplorerVisibleController).GetTypeInfo());

            // Act
            var actions = provider.GetDescriptors();

            // Assert
            var action = Assert.Single(actions);
            Assert.NotNull(action.GetProperty<ApiDescriptionActionData>());
        }

        [Fact]
        public void ApiExplorer_SetsExtensionData_WhenVisible_CanOverrideControllerOnAction()
        {
            // Arrange
            var provider = GetProvider(typeof(ApiExplorerVisibilityOverrideController).GetTypeInfo());

            // Act
            var actions = provider.GetDescriptors();

            // Assert
            Assert.Equal(2, actions.Count());

            var action = Assert.Single(actions, a => a.ActionName == "Edit");
            Assert.NotNull(action.GetProperty<ApiDescriptionActionData>());

            action = Assert.Single(actions, a => a.ActionName == "Create");
            Assert.Null(action.GetProperty<ApiDescriptionActionData>());
        }

        [Theory]
        [InlineData(typeof(ApiExplorerNotVisibleController))]
        [InlineData(typeof(ApiExplorerExplicitlyNotVisibleController))]
        [InlineData(typeof(ApiExplorerExplicitlyNotVisibleOnActionController))]
        public void ApiExplorer_DoesNotSetExtensionData_WhenNotVisible(Type controllerType)
        {
            // Arrange
            var provider = GetProvider(controllerType.GetTypeInfo());

            // Act
            var actions = provider.GetDescriptors();

            // Assert
            var action = Assert.Single(actions);
            Assert.Null(action.GetProperty<ApiDescriptionActionData>());
        }

        [Fact]
        public void ApiExplorer_SetsName_DefaultToNull()
        {
            // Arrange
            var provider = GetProvider(typeof(ApiExplorerNoNameController).GetTypeInfo());

            // Act
            var actions = provider.GetDescriptors();

            // Assert

            var action = Assert.Single(actions);
            Assert.Null(action.GetProperty<ApiDescriptionActionData>().GroupName);
        }

        [Fact]
        public void ApiExplorer_SetsName_OnController()
        {
            // Arrange
            var provider = GetProvider(typeof(ApiExplorerNameOnControllerController).GetTypeInfo());

            // Act
            var actions = provider.GetDescriptors();

            // Assert

            var action = Assert.Single(actions);
            Assert.Equal("Store", action.GetProperty<ApiDescriptionActionData>().GroupName);
        }

        [Fact]
        public void ApiExplorer_SetsName_OnAction()
        {
            // Arrange
            var provider = GetProvider(typeof(ApiExplorerNameOnActionController).GetTypeInfo());

            // Act
            var actions = provider.GetDescriptors();

            // Assert
            var action = Assert.Single(actions);
            Assert.Equal("Blog", action.GetProperty<ApiDescriptionActionData>().GroupName);
        }

        [Fact]
        public void ApiExplorer_SetsName_CanOverrideControllerOnAction()
        {
            // Arrange
            var provider = GetProvider(typeof(ApiExplorerNameOverrideController).GetTypeInfo());

            // Act
            var actions = provider.GetDescriptors();

            // Assert
            Assert.Equal(2, actions.Count());

            var action = Assert.Single(actions, a => a.ActionName == "Edit");
            Assert.Equal("Blog", action.GetProperty<ApiDescriptionActionData>().GroupName);

            action = Assert.Single(actions, a => a.ActionName == "Create");
            Assert.Equal("Store", action.GetProperty<ApiDescriptionActionData>().GroupName);
        }

        [Fact]
        public void ApiExplorer_IsVisibleOnApplication_CanOverrideOnController()
        {
            // Arrange
            var convention = new ApiExplorerIsVisibleConvention(isVisible: true);
            var provider = GetProvider(typeof(ApiExplorerExplicitlyNotVisibleController).GetTypeInfo(), convention);

            // Act
            var actions = provider.GetDescriptors();

            // Assert
            var action = Assert.Single(actions);
            Assert.Null(action.GetProperty<ApiDescriptionActionData>());
        }

        [Fact]
        public void ApiExplorer_IsVisibleOnApplication_CanOverrideOnAction()
        {
            // Arrange
            var convention = new ApiExplorerIsVisibleConvention(isVisible: true);
            var provider = GetProvider(
                typeof(ApiExplorerExplicitlyNotVisibleOnActionController).GetTypeInfo(),
                convention);

            // Act
            var actions = provider.GetDescriptors();

            // Assert
            var action = Assert.Single(actions);
            Assert.Null(action.GetProperty<ApiDescriptionActionData>());
        }

        [Theory]
        [InlineData("A", typeof(ApiExplorerEnabledConventionalRoutedController))]
        [InlineData("A", typeof(ApiExplorerEnabledActionConventionalRoutedController))]
        public void ApiExplorer_ThrowsForConventionalRouting(string actionName, Type type)
        {
            // Arrange
            var assemblyName = type.GetTypeInfo().Assembly.GetName().Name;
            var expected = $"The action '{type.FullName}.{actionName} ({assemblyName})' has ApiExplorer enabled, but is using conventional routing. " +
                "Only actions which use attribute routing support ApiExplorer.";

            var provider = GetProvider(type.GetTypeInfo());

            // Act & Assert
            var ex = Assert.Throws<InvalidOperationException>(() => provider.GetDescriptors());
            Assert.Equal(expected, ex.Message);
        }

        [Fact]
        public void ApiExplorer_SkipsConventionalRoutedController_WhenConfiguredOnApplication()
        {
            // Arrange
            var convention = new ApiExplorerIsVisibleConvention(isVisible: true);
            var provider = GetProvider(
                typeof(ConventionallyRoutedController).GetTypeInfo(),
                convention);

            // Act
            var actions = provider.GetDescriptors();

            // Assert
            var action = Assert.Single(actions);
            Assert.Null(action.GetProperty<ApiDescriptionActionData>());
        }

        // Verifies the sequence of conventions running
        [Fact]
        public void ApplyConventions_RunsInOrderOfDecreasingScope()
        {
            // Arrange
            var sequence = 0;

            var applicationConvention = new Mock<IApplicationModelConvention>();
            applicationConvention
                .Setup(c => c.Apply(It.IsAny<ApplicationModel>()))
                .Callback(() => { Assert.Equal(0, sequence++); });

            var controllerConvention = new Mock<IControllerModelConvention>();
            controllerConvention
                .Setup(c => c.Apply(It.IsAny<ControllerModel>()))
                .Callback(() => { Assert.Equal(1, sequence++); });

            var actionConvention = new Mock<IActionModelConvention>();
            actionConvention
                .Setup(c => c.Apply(It.IsAny<ActionModel>()))
                .Callback(() => { Assert.Equal(2, sequence++); });

            var parameterConvention = new Mock<IParameterModelConvention>();
            parameterConvention
                .Setup(c => c.Apply(It.IsAny<ParameterModel>()))
                .Callback(() => { Assert.Equal(3, sequence++); });

            var options = Options.Create(new MvcOptions());
            options.Value.Conventions.Add(applicationConvention.Object);

            var applicationModel = new ApplicationModel();

            var controller = new ControllerModel(typeof(ConventionsController).GetTypeInfo(),
                                                 new List<object>() { controllerConvention.Object });
            controller.Application = applicationModel;
            applicationModel.Controllers.Add(controller);

            var methodInfo = typeof(ConventionsController).GetMethod("Create");
            var actionModel = new ActionModel(methodInfo, new List<object>() { actionConvention.Object });
            actionModel.Controller = controller;
            controller.Actions.Add(actionModel);

            var parameterInfo = actionModel.ActionMethod.GetParameters().Single();
            var parameterModel = new ParameterModel(parameterInfo,
                                           new List<object>() { parameterConvention.Object });
            parameterModel.Action = actionModel;
            actionModel.Parameters.Add(parameterModel);

            // Act
            ApplicationModelConventions.ApplyConventions(applicationModel, options.Value.Conventions);

            // Assert
            Assert.Equal(4, sequence);
        }

        [Fact]
        public void GetDescriptors_SplitsConstraintsBasedOnRoute()
        {
            // Arrange
            var provider = GetProvider(typeof(MultipleRouteProviderOnActionController).GetTypeInfo());

            // Act
            var actions = provider.GetDescriptors();

            // Assert
            Assert.Equal(2, actions.Count());

            var action = Assert.Single(actions, a => a.AttributeRouteInfo.Template == "R1");

            Assert.Equal(2, action.ActionConstraints.Count);
            Assert.Single(action.ActionConstraints, a => a is RouteAndConstraintAttribute);
            Assert.Single(action.ActionConstraints, a => a is ConstraintAttribute);

            action = Assert.Single(actions, a => a.AttributeRouteInfo.Template == "R2");

            Assert.Equal(2, action.ActionConstraints.Count);
            Assert.Single(action.ActionConstraints, a => a is RouteAndConstraintAttribute);
            Assert.Single(action.ActionConstraints, a => a is ConstraintAttribute);
        }

        [Fact]
        public void GetDescriptors_SplitsConstraintsBasedOnControllerRoute()
        {
            // Arrange
            var actionName = nameof(MultipleRouteProviderOnActionAndControllerController.Edit);
            var provider = GetProvider(typeof(MultipleRouteProviderOnActionAndControllerController).GetTypeInfo());

            // Act
            var actions = provider.GetDescriptors().Where(a => a.ActionName == actionName);

            // Assert
            Assert.Equal(2, actions.Count());

            var action = Assert.Single(actions, a => a.AttributeRouteInfo.Template == "C1/A1");
            Assert.Equal(3, action.ActionConstraints.Count);
            Assert.Single(action.ActionConstraints, a => (a as RouteAndConstraintAttribute)?.Template == "C1");
            Assert.Single(action.ActionConstraints, a => (a as RouteAndConstraintAttribute)?.Template == "A1");
            Assert.Single(action.ActionConstraints, a => a is ConstraintAttribute);

            action = Assert.Single(actions, a => a.AttributeRouteInfo.Template == "C2/A1");
            Assert.Equal(3, action.ActionConstraints.Count);
            Assert.Single(action.ActionConstraints, a => (a as RouteAndConstraintAttribute)?.Template == "C2");
            Assert.Single(action.ActionConstraints, a => (a as RouteAndConstraintAttribute)?.Template == "A1");
            Assert.Single(action.ActionConstraints, a => a is ConstraintAttribute);
        }

        [Fact]
        public void GetDescriptors_SplitsConstraintsBasedOnControllerRoute_MultipleRoutesOnAction()
        {
            // Arrange
            var actionName = nameof(MultipleRouteProviderOnActionAndControllerController.Delete);

            var provider = GetProvider(typeof(MultipleRouteProviderOnActionAndControllerController).GetTypeInfo());

            // Act
            var actions = provider.GetDescriptors().Where(a => a.ActionName == actionName);

            // Assert
            Assert.Equal(4, actions.Count());

            var action = Assert.Single(actions, a => a.AttributeRouteInfo.Template == "C1/A3");
            Assert.Equal(3, action.ActionConstraints.Count);
            Assert.Single(action.ActionConstraints, a => (a as RouteAndConstraintAttribute)?.Template == "C1");
            Assert.Single(action.ActionConstraints, a => (a as RouteAndConstraintAttribute)?.Template == "A3");
            Assert.Single(action.ActionConstraints, a => a is ConstraintAttribute);

            action = Assert.Single(actions, a => a.AttributeRouteInfo.Template == "C2/A3");
            Assert.Equal(3, action.ActionConstraints.Count);
            Assert.Single(action.ActionConstraints, a => (a as RouteAndConstraintAttribute)?.Template == "C2");
            Assert.Single(action.ActionConstraints, a => (a as RouteAndConstraintAttribute)?.Template == "A3");
            Assert.Single(action.ActionConstraints, a => a is ConstraintAttribute);

            action = Assert.Single(actions, a => a.AttributeRouteInfo.Template == "C1/A4");
            Assert.Equal(3, action.ActionConstraints.Count);
            Assert.Single(action.ActionConstraints, a => (a as RouteAndConstraintAttribute)?.Template == "C1");
            Assert.Single(action.ActionConstraints, a => (a as RouteAndConstraintAttribute)?.Template == "A4");
            Assert.Single(action.ActionConstraints, a => a is ConstraintAttribute);

            action = Assert.Single(actions, a => a.AttributeRouteInfo.Template == "C2/A4");
            Assert.Equal(3, action.ActionConstraints.Count);
            Assert.Single(action.ActionConstraints, a => (a as RouteAndConstraintAttribute)?.Template == "C2");
            Assert.Single(action.ActionConstraints, a => (a as RouteAndConstraintAttribute)?.Template == "A4");
            Assert.Single(action.ActionConstraints, a => a is ConstraintAttribute);
        }

        // This method overrides the route from the controller, and so doesn't inherit its metadata.
        [Fact]
        public void GetDescriptors_SplitsConstraintsBasedOnControllerRoute_Override()
        {
            // Arrange
            var actionName = nameof(MultipleRouteProviderOnActionAndControllerController.Create);
            var provider = GetProvider(typeof(MultipleRouteProviderOnActionAndControllerController).GetTypeInfo());

            // Act
            var actions = provider.GetDescriptors().Where(a => a.ActionName == actionName);

            // Assert
            Assert.Single(actions);

            var action = Assert.Single(actions, a => a.AttributeRouteInfo.Template == "A2");
            Assert.Equal(2, action.ActionConstraints.Count);
            Assert.Single(action.ActionConstraints, a => (a as RouteAndConstraintAttribute)?.Template == "~/A2");
            Assert.Single(action.ActionConstraints, a => a is ConstraintAttribute);
        }

        [Fact]
        public void OnProviderExecuted_AddsGlobalRouteValues()
        {
            // Arrange
            var context = new ActionDescriptorProviderContext();
            context.Results.Add(new ActionDescriptor()
            {
                RouteValues = new Dictionary<string, string>()
                {
                    { "controller", "Home" },
                    { "action", "Index" },
                }
            });
            context.Results.Add(new ActionDescriptor()
            {
                RouteValues = new Dictionary<string, string>()
                {
                    { "page", "/Some/Page" }
                }
            });

            var provider = GetProvider();

            // Act
            provider.OnProvidersExecuted(context);

            // Assert
            Assert.True(context.Results[0].RouteValues.ContainsKey("page"));
            Assert.Null(context.Results[0].RouteValues["page"]);

            Assert.True(context.Results[1].RouteValues.ContainsKey("controller"));
            Assert.Null(context.Results[1].RouteValues["controller"]);
            Assert.True(context.Results[1].RouteValues.ContainsKey("action"));
            Assert.Null(context.Results[1].RouteValues["action"]);
        }

        private ControllerActionDescriptorProvider GetProvider(
            TypeInfo controllerTypeInfo,
            IEnumerable<IFilterMetadata> filters = null)
        {
            var options = Options.Create(new MvcOptions());
            if (filters != null)
            {
                foreach (var filter in filters)
                {
                    options.Value.Filters.Add(filter);
                }
            }

            var manager = GetApplicationManager(new[] { controllerTypeInfo });

            var modelProvider = new DefaultApplicationModelProvider(options, new EmptyModelMetadataProvider());

            var provider = new ControllerActionDescriptorProvider(
                manager,
                new ApplicationModelFactory(new[] { modelProvider }, options));

            return provider;
        }

        private ControllerActionDescriptorProvider GetProvider(
            params TypeInfo[] controllerTypeInfos)
        {
            var options = Options.Create(new MvcOptions());

            var manager = GetApplicationManager(controllerTypeInfos);
            var modelProvider = new DefaultApplicationModelProvider(options, new EmptyModelMetadataProvider());

            var provider = new ControllerActionDescriptorProvider(
                manager,
                new ApplicationModelFactory(new[] { modelProvider }, options));

            return provider;
        }

        private ControllerActionDescriptorProvider GetProvider(
            TypeInfo controllerTypeInfo,
            IApplicationModelConvention convention)
        {
            var options = Options.Create(new MvcOptions());
            options.Value.Conventions.Add(convention);

            var manager = GetApplicationManager(new[] { controllerTypeInfo });

            var modelProvider = new DefaultApplicationModelProvider(options, new EmptyModelMetadataProvider());

            var provider = new ControllerActionDescriptorProvider(
                manager,
                new ApplicationModelFactory(new[] { modelProvider }, options));

            return provider;
        }

        private static ApplicationPartManager GetApplicationManager(IEnumerable<TypeInfo> controllerTypes)
        {
            var manager = new ApplicationPartManager();
            manager.ApplicationParts.Add(new TestApplicationPart(controllerTypes));
            manager.FeatureProviders.Add(new TestFeatureProvider());
            return manager;
        }

        private IEnumerable<ActionDescriptor> GetDescriptors(params TypeInfo[] controllerTypeInfos)
        {
            var provider = GetProvider(controllerTypeInfos);
            return provider.GetDescriptors();
        }

        private static void VerifyMultiLineError(
            string expectedMessage,
            string actualMessage,
            int unorderedStart,
            int unorderedLineCount)
        {
            var expectedLines = expectedMessage
                .Split(new[] { Environment.NewLine }, StringSplitOptions.None)
                .ToArray();

            var actualLines = actualMessage
                .Split(new[] { Environment.NewLine }, StringSplitOptions.None)
                .ToArray();

            for (var i = 0; i < unorderedStart; i++)
            {
                Assert.Equal(expectedLines[i], actualLines[i]);
            }

            var orderedExpectedLines = expectedLines
                .Skip(unorderedStart)
                .Take(unorderedLineCount)
                .OrderBy(l => l, StringComparer.Ordinal)
                .ToArray();

            var orderedActualLines = actualLines
                .Skip(unorderedStart)
                .Take(unorderedLineCount)
                .OrderBy(l => l, StringComparer.Ordinal)
                .ToArray();

            for (var i = 0; i < unorderedLineCount; i++)
            {
                Assert.Equal(orderedExpectedLines[i], orderedActualLines[i]);
            }

            for (var i = unorderedStart + unorderedLineCount; i < expectedLines.Length; i++)
            {
                Assert.Equal(expectedLines[i], actualLines[i]);
            }

            Assert.Equal(expectedLines.Length, actualLines.Length);
        }

        private class HttpMethodController
        {
            [HttpPost]
            public void OnlyPost()
            {
            }
        }

        [Route("Items")]
        private class AttributeRoutedHttpMethodController
        {
            [CustomHttpMethodConstraint("PUT", "PATCH")]
            public void PutOrPatch() { }
        }

        private class PersonController
        {
            public void GetPerson()
            { }

            [ActionName("ShowPeople")]
            public void ListPeople()
            { }

            [NonAction]
            public void NotAnAction()
            { }
        }

        private class MyRouteValueAttribute : RouteValueAttribute
        {
            public MyRouteValueAttribute()
                : base("key", "value")
            {
            }
        }

        private class MySecondRouteValueAttribute : RouteValueAttribute
        {
            public MySecondRouteValueAttribute()
                : base("second", "value")
            {
            }
        }

        [MyRouteValue]
        private class RouteValueController
        {
            public void Edit()
            {
            }
        }

        private class MyFilterAttribute : Attribute, IFilterMetadata
        {
            public MyFilterAttribute(int value)
            {
                Value = value;
            }

            public int Value { get; private set; }
        }

        [MyFilter(2)]
        private class FiltersController
        {
            [MyFilter(3)]
            public void FilterAction()
            {
            }
        }

        [Route("api/Token/[key]/[controller]")]
        [MyRouteValue]
        private class TokenReplacementController
        {
            [HttpGet("stub/[action]")]
            public void ThisIsAnAction() { }
        }

        private class CaseInsensitiveController
        {
            [HttpGet("stub/[ActIon]")]
            public void ThisIsAnAction() { }
        }

        private class MultipleErrorsController
        {
            [HttpGet("stub/[action]/[unknown]")]
            public void Unknown() { }

            [HttpGet("[invalid/syntax")]
            public void Invalid() { }
        }

        private class InvalidParametersController
        {
            [HttpGet("stub/{controller}/{action}")]
            public void Action1() { }
        }

        private class SameGroupIdController
        {
            [HttpGet("stub/[action]")]
            public void Action1() { }

            [HttpGet("stub/Action1")]
            public void Action2() { }
        }

        [Area("Home")]
        private class ConventionalAndAttributeRoutedActionsWithAreaController
        {
            [HttpGet("Index")]
            public void Index() { }

            [HttpGet("Edit")]
            public void Edit() { }

            public void AnotherNonAttributedAction() { }
        }

        [Route("Products", Name = "Products")]
        private class SameNameDifferentTemplatesController
        {
            [HttpGet]
            public void Get() { }

            [HttpGet("{id}", Name = "Products")]
            public void Get(int id) { }

            [HttpPut("{id}", Name = "Products")]
            public void Put(int id) { }

            [HttpPost]
            public void Post() { }

            [HttpDelete("{id}", Name = "Products")]
            public void Delete(int id) { }

            [HttpGet("/Items/{id}", Name = "Items")]
            public void GetItems(int id) { }

            [HttpPost("/Items", Name = "Items")]
            public void PostItems() { }

            [HttpPut("/Items/{id}", Name = "Items")]
            public void PutItems(int id) { }

            [HttpDelete("/Items/{id}", Name = "Items")]
            public void DeleteItems(int id) { }

            [HttpPatch("/Items", Name = "Items")]
            public void PatchItems() { }
        }

        [Route("Products/[action]", Name = "Products_[action]")]
        private class ActionRouteNameTemplatesController
        {
            [HttpGet]
            public void Get() { }

            [HttpPost]
            public void Get(int id) { }

            public void Edit() { }
        }

        [Area("Products")]
        [Route("[controller]/[action]", Name = "[area]_[controller]_[action]")]
        private class ControllerActionRouteNameTemplatesController
        {
            [HttpGet]
            public void Get() { }

            [HttpPost]
            public void Get(int id) { }

            public void Edit() { }
        }

        [Route("Products/[action]", Name = "Products_[unknown]")]
        private class RouteNameIncorrectTokenController
        {
            public void Get() { }
        }

        private class DifferentCasingsAttributeRouteNamesController
        {
            [HttpGet("{id}", Name = "Products")]
            public void Get() { }

            [HttpGet("{ID}", Name = "Products")]
            public void Get(int id) { }

            [HttpPut("{id}", Name = "PRODUCTS")]
            public void Put(int id) { }

            [HttpDelete("{ID}", Order = 1, Name = "PRODUCTS")]
            public void Delete(int id) { }
        }

        [Route("v1")]
        [Route("v2")]
        public class MultiRouteAttributesController
        {
            [HttpGet("List")]
            [HttpGet("All")]
            public void MultipleHttpGet() { }

            [AcceptVerbs("POST", Route = "List")]
            public void AcceptVerbs() { }

            [AcceptVerbs("PUT", Route = "/Override")]
            public void AcceptVerbsOverride() { }

            [AcceptVerbs("POST")]
            [Route("List")]
            [HttpPut("All")]
            public void AcceptVerbsRouteAttributeAndHttpPut() { }

            [AcceptVerbs("POST", Route = "")]
            [Route("List")]
            [HttpPut("All")]
            public void AcceptVerbsRouteAttributeWithTemplateAndHttpPut() { }
        }

        [Route("Products")]
        public class OnlyRouteController
        {
            [Route("Index")]
            public void Action() { }
        }

        public class AttributeAndNonAttributeRoutedActionsOnSameMethodController
        {
            [HttpGet("AttributeRouted")]
            [HttpPost]
            [AcceptVerbs("PUT", "PATCH")]
            [CustomHttpMethodConstraint("DELETE")]
            public void Method() { }
        }

        [Route("Product")]
        [Route("/Product")]
        [Route("/product")]
        public class DuplicatedAttributeRouteController
        {
            [HttpGet("/List")]
            [HttpGet("/List")]
            public void Action() { }

            public void Controller() { }

            [HttpPut("list")]
            [PutOrPatch("list")]
            public void CommonHttpMethod() { }
        }

        [Route("Products")]
        public class NonDuplicatedAttributeRouteController
        {
            [HttpGet("list")]
            public void ControllerAndAction() { }

            [HttpGet("/PRODUCTS/LIST")]
            public void OverrideOnAction() { }

            [HttpGet("list")]
            [HttpPost("list")]
            [HttpPut("list")]
            [HttpPatch("list")]
            [HttpDelete("list")]
            public void DifferentHttpMethods() { }
        }

        [MyRouteValue]
        [MySecondRouteValue]
        private class ConstrainedController
        {
            public void ConstrainedNonAttributedAction() { }
        }

        private class ActionParametersController
        {
            public void RequiredInt(int id) { }

            public void FromBodyParameter([FromBody] TestActionParameter entity) { }

            public void NotFromBodyParameter(TestActionParameter entity) { }

            public void MultipleParameters(int id, [FromBody] TestActionParameter entity) { }

            public void DifferentCasing(int id, int ID, int Id) { }
        }

        private class ConventionallyRoutedController
        {
            public void ConventionalAction() { }
        }

        [Route("api")]
        private class AttributeRoutedController
        {
            [HttpGet("AttributeRoute")]
            public void AttributeRoutedAction() { }
        }

        [Authorize("ControllerPolicy")]
        private class AuthorizeController
        {
            [AllowAnonymous]
            public void AllowAnonymousAction() { }

            [Authorize("ActionPolicy")]
            public void AuthorizeAction() { }
        }

        private class EmptyController
        {
        }

        private class NonActionAttributeController
        {
            [NonAction]
            public void Action() { }
        }

        private class CustomHttpMethodConstraintAttribute : Attribute, IActionHttpMethodProvider
        {
            private readonly string[] _methods;

            public CustomHttpMethodConstraintAttribute(params string[] methods)
            {
                _methods = methods;
            }

            public IEnumerable<string> HttpMethods
            {
                get
                {
                    return _methods;
                }
            }
        }

        private class PutOrPatchAttribute : HttpMethodAttribute
        {
            private static readonly string[] _httpMethods = new string[] { "PUT", "PATCH" };

            public PutOrPatchAttribute(string template)
                : base(_httpMethods, template)
            {
            }
        }

        private class TestActionParameter
        {
            public int Id { get; set; }
            public int Name { get; set; }
        }

        [Route("AttributeRouting/IsRequired/ForApiExplorer")]
        private class ApiExplorerNotVisibleController
        {
            public void Edit() { }
        }

        [Route("AttributeRouting/IsRequired/ForApiExplorer")]
        [ApiExplorerSettings()]
        private class ApiExplorerVisibleController
        {
            public void Edit() { }
        }

        [Route("AttributeRouting/IsRequired/ForApiExplorer")]
        [ApiExplorerSettings(IgnoreApi = true)]
        private class ApiExplorerExplicitlyNotVisibleController
        {
            public void Edit() { }
        }

        [Route("AttributeRouting/IsRequired/ForApiExplorer")]
        private class ApiExplorerExplicitlyNotVisibleOnActionController
        {
            [ApiExplorerSettings(IgnoreApi = true)]
            public void Edit() { }
        }

        [Route("AttributeRouting/IsRequired/ForApiExplorer")]
        [ApiExplorerSettings(IgnoreApi = true)]
        private class ApiExplorerVisibilityOverrideController
        {
            [ApiExplorerSettings(IgnoreApi = false)]
            public void Edit() { }

            public void Create() { }
        }

        [Route("AttributeRouting/IsRequired/ForApiExplorer")]
        [ApiExplorerSettings(GroupName = "Store")]
        private class ApiExplorerNameOnControllerController
        {
            public void Edit() { }
        }

        [Route("AttributeRouting/IsRequired/ForApiExplorer")]
        private class ApiExplorerNameOnActionController
        {
            [ApiExplorerSettings(GroupName = "Blog")]
            public void Edit() { }
        }

        [Route("AttributeRouting/IsRequired/ForApiExplorer")]
        [ApiExplorerSettings()]
        private class ApiExplorerNoNameController
        {
            public void Edit() { }
        }

        [Route("AttributeRouting/IsRequired/ForApiExplorer")]
        [ApiExplorerSettings(GroupName = "Store")]
        private class ApiExplorerNameOverrideController
        {
            [ApiExplorerSettings(GroupName = "Blog")]
            public void Edit() { }

            public void Create() { }
        }

        private class ConventionsController
        {
            public void Create(int productId) { }
        }

        private class MultipleRouteProviderOnActionController
        {
            [Constraint]
            [RouteAndConstraint("R1")]
            [RouteAndConstraint("R2")]
            public void Edit() { }
        }

        [Constraint]
        [RouteAndConstraint("C1")]
        [RouteAndConstraint("C2")]
        private class MultipleRouteProviderOnActionAndControllerController
        {
            [RouteAndConstraint("A1")]
            public void Edit() { }

            [RouteAndConstraint("~/A2")]
            public void Create() { }

            [RouteAndConstraint("A3")]
            [RouteAndConstraint("A4")]
            public void Delete() { }
        }

        [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, Inherited = true, AllowMultiple = true)]
        private class RouteAndConstraintAttribute : Attribute, IActionConstraintMetadata, IRouteTemplateProvider
        {
            public RouteAndConstraintAttribute(string template)
            {
                Template = template;
            }

            public string Name { get; set; }

            public int? Order { get; set; }

            public string Template { get; private set; }
        }

        [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, Inherited = true, AllowMultiple = true)]
        private class ConstraintAttribute : Attribute, IActionConstraintMetadata
        {
        }

        [ApiExplorerSettings(GroupName = "Default")]
        private class ApiExplorerEnabledConventionalRoutedController
        {
            public void A()
            {
            }
        }

        [ApiExplorerSettings(IgnoreApi = true)]
        private class ApiExplorerEnabledActionConventionalRoutedController
        {
            [ApiExplorerSettings(GroupName = "Default")]
            public void A()
            {
            }
        }

        private class ApiExplorerIsVisibleConvention : IApplicationModelConvention
        {
            private readonly bool _isVisible;

            public ApiExplorerIsVisibleConvention(bool isVisible)
            {
                _isVisible = isVisible;
            }

            public void Apply(ApplicationModel application)
            {
                application.ApiExplorer.IsVisible = _isVisible;
            }
        }

        private class MixedRoutingConventionAttribute : Attribute, IActionModelConvention
        {
            public void Apply(ActionModel action)
            {
                action.Selectors.Add(new SelectorModel()
                {
                    AttributeRouteModel = new AttributeRouteModel()
                    {
                        Template = "/!!!",
                    }
                });
            }
        }

        private class UserController : ControllerBase
        {
            [MixedRoutingConvention]
            public string GetUser(int id)
            {
                return string.Format(CultureInfo.InvariantCulture, "User {0} retrieved successfully", id);
            }
        }
    }
}
