// 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.Linq;
using System.Reflection;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.Abstractions;
using Microsoft.AspNetCore.Mvc.ActionConstraints;
using Microsoft.AspNetCore.Mvc.ApplicationModels;
using Microsoft.AspNetCore.Mvc.ApplicationParts;
using Microsoft.AspNetCore.Mvc.Controllers;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.Mvc.Routing;
using Microsoft.AspNetCore.Routing;
using Microsoft.AspNetCore.Testing;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Logging.Testing;
using Microsoft.Extensions.Options;
using Moq;
using Xunit;

namespace Microsoft.AspNetCore.Mvc.Infrastructure
{
    public class ActionSelectorTest
    {
        [Fact]
        public void SelectCandidates_SingleMatch()
        {
            var actions = new ActionDescriptor[]
            {
                new ActionDescriptor()
                {
                    DisplayName = "A1",
                    RouteValues = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
                    {
                        { "controller", "Home" },
                        { "action", "Index" }
                    },
                },
                new ActionDescriptor()
                {
                    DisplayName = "A2",
                    RouteValues = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
                    {
                        { "controller", "Home" },
                        { "action", "About" }
                    },
                },
            };

            var selector = CreateSelector(actions);

            var routeContext = CreateRouteContext("GET");
            routeContext.RouteData.Values.Add("controller", "Home");
            routeContext.RouteData.Values.Add("action", "Index");

            // Act
            var candidates = selector.SelectCandidates(routeContext);

            // Assert
            Assert.Collection(candidates, (a) => Assert.Same(actions[0], a));
        }

        [Fact]
        [ReplaceCulture("de-CH", "de-CH")]
        public void SelectCandidates_SingleMatch_UsesInvariantCulture()
        {
            var actions = new ActionDescriptor[]
            {
                new ActionDescriptor()
                {
                    DisplayName = "A1",
                    RouteValues = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
                    {
                        { "controller", "Home" },
                        { "action", "Index" },
                        { "date", "10/31/2018 07:37:38 -07:00" },
                    },
                },
                new ActionDescriptor()
                {
                    DisplayName = "A2",
                    RouteValues = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
                    {
                        { "controller", "Home" },
                        { "action", "About" }
                    },
                },
            };

            var selector = CreateSelector(actions);

            var routeContext = CreateRouteContext("GET");
            routeContext.RouteData.Values.Add("controller", "Home");
            routeContext.RouteData.Values.Add("action", "Index");
            routeContext.RouteData.Values.Add(
                "date",
                new DateTimeOffset(2018, 10, 31, 7, 37, 38, TimeSpan.FromHours(-7)));

            // Act
            var candidates = selector.SelectCandidates(routeContext);

            // Assert
            Assert.Collection(candidates, (a) => Assert.Same(actions[0], a));
        }

        [Fact]
        public void SelectCandidates_MultipleMatches()
        {
            var actions = new ActionDescriptor[]
            {
                new ActionDescriptor()
                {
                    DisplayName = "A1",
                    RouteValues = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
                    {
                        { "controller", "Home" },
                        { "action", "Index" }
                    },
                },
                new ActionDescriptor()
                {
                    DisplayName = "A2",
                    RouteValues = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
                    {
                        { "controller", "Home" },
                        { "action", "Index" }
                    },
                },
            };

            var selector = CreateSelector(actions);

            var routeContext = CreateRouteContext("GET");
            routeContext.RouteData.Values.Add("controller", "Home");
            routeContext.RouteData.Values.Add("action", "Index");

            // Act
            var candidates = selector.SelectCandidates(routeContext);

            // Assert
            Assert.Equal(actions.ToArray(), candidates.ToArray());
        }

        [Fact]
        public void SelectCandidates_NoMatch()
        {
            var actions = new ActionDescriptor[]
            {
                new ActionDescriptor()
                {
                    DisplayName = "A1",
                    RouteValues = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
                    {
                        { "controller", "Home" },
                        { "action", "Index" }
                    },
                },
                new ActionDescriptor()
                {
                    DisplayName = "A2",
                    RouteValues = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
                    {
                        { "controller", "Home" },
                        { "action", "About" }
                    },
                },
            };

            var selector = CreateSelector(actions);

            var routeContext = CreateRouteContext("GET");
            routeContext.RouteData.Values.Add("controller", "Foo");
            routeContext.RouteData.Values.Add("action", "Index");

            // Act
            var candidates = selector.SelectCandidates(routeContext);

            // Assert
            Assert.Empty(candidates);
        }

        [Fact]
        public void SelectCandidates_NoMatch_ExcludesAttributeRoutedActions()
        {
            var actions = new ActionDescriptor[]
            {
                new ActionDescriptor()
                {
                    DisplayName = "A1",
                    RouteValues = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
                    {
                        { "controller", "Home" },
                        { "action", "Index" }
                    },
                    AttributeRouteInfo = new AttributeRouteInfo()
                    {
                        Template = "/Home",
                    }
                },
            };

            var selector = CreateSelector(actions);

            var routeContext = CreateRouteContext("GET");
            routeContext.RouteData.Values.Add("controller", "Home");
            routeContext.RouteData.Values.Add("action", "Index");

            // Act
            var candidates = selector.SelectCandidates(routeContext);

            // Assert
            Assert.Empty(candidates);
        }

        // In this context `CaseSensitiveMatch` means that the input route values exactly match one of the action
        // descriptor's route values in terms of casing. This is important because we optimize for this case
        // in the implementation.
        [Fact]
        public void SelectCandidates_Match_CaseSensitiveMatch_IncludesAllCaseInsensitiveMatches()
        {
            var actions = new ActionDescriptor[]
            {
                new ActionDescriptor()
                {
                    DisplayName = "A1",
                    RouteValues = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
                    {
                        { "controller", "Home" },
                        { "action", "Index" }
                    },
                },
                new ActionDescriptor()
                {
                    DisplayName = "A2",
                    RouteValues = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
                    {
                        { "controller", "home" },
                        { "action", "Index" }
                    },
                },
                new ActionDescriptor() // This won't match the request
                {
                    DisplayName = "A3",
                    RouteValues = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
                    {
                        { "controller", "Home" },
                        { "action", "About" }
                    },
                },
            };

            var expected = actions.Take(2).ToArray();

            var selector = CreateSelector(actions);

            var routeContext = CreateRouteContext("GET");
            routeContext.RouteData.Values.Add("controller", "Home");
            routeContext.RouteData.Values.Add("action", "Index");

            // Act
            var candidates = selector.SelectCandidates(routeContext);

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

        // In this context `CaseInsensitiveMatch` means that the input route values do not match any action
        // descriptor's route values in terms of casing. This is important because we optimize for the case
        // where the casing matches - the non-matching-casing path is handled a bit differently.
        [Fact]
        public void SelectCandidates_Match_CaseInsensitiveMatch_IncludesAllCaseInsensitiveMatches()
        {
            var actions = new ActionDescriptor[]
            {
                new ActionDescriptor()
                {
                    DisplayName = "A1",
                    RouteValues = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
                    {
                        { "controller", "Home" },
                        { "action", "Index" }
                    },
                },
                new ActionDescriptor()
                {
                    DisplayName = "A2",
                    RouteValues = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
                    {
                        { "controller", "home" },
                        { "action", "Index" }
                    },
                },
                new ActionDescriptor() // This won't match the request
                {
                    DisplayName = "A3",
                    RouteValues = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
                    {
                        { "controller", "Home" },
                        { "action", "About" }
                    },
                },
            };

            var expected = actions.Take(2).ToArray();

            var selector = CreateSelector(actions);

            var routeContext = CreateRouteContext("GET");
            routeContext.RouteData.Values.Add("controller", "HOME");
            routeContext.RouteData.Values.Add("action", "iNDex");

            // Act
            var candidates = selector.SelectCandidates(routeContext);

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

        [Fact]
        public void SelectCandidates_Match_CaseSensitiveMatch_MatchesOnEmptyString()
        {
            var actions = new ActionDescriptor[]
            {
                new ActionDescriptor()
                {
                    DisplayName = "A1",
                    RouteValues = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
                    {
                        { "area", null },
                        { "controller", "Home" },
                        { "action", "Index" }
                    },
                }
            };

            var selector = CreateSelector(actions);

            var routeContext = CreateRouteContext("GET");
            // Example: In conventional route, one could set non-inline defaults
            // new { area = "", controller = "Home", action = "Index" }
            routeContext.RouteData.Values.Add("area", "");
            routeContext.RouteData.Values.Add("controller", "Home");
            routeContext.RouteData.Values.Add("action", "Index");

            // Act
            var candidates = selector.SelectCandidates(routeContext);

            // Assert
            var action = Assert.Single(candidates);
            Assert.Same(actions[0], action);
        }

        [Fact]
        public void SelectCandidates_Match_CaseInsensitiveMatch_MatchesOnEmptyString()
        {
            var actions = new ActionDescriptor[]
            {
                new ActionDescriptor()
                {
                    DisplayName = "A1",
                    RouteValues = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
                    {
                        { "area", null },
                        { "controller", "Home" },
                        { "action", "Index" }
                    },
                }
            };

            var selector = CreateSelector(actions);

            var routeContext = CreateRouteContext("GET");
            // Example: In conventional route, one could set non-inline defaults
            // new { area = "", controller = "Home", action = "Index" }
            routeContext.RouteData.Values.Add("area", "");
            routeContext.RouteData.Values.Add("controller", "HoMe");
            routeContext.RouteData.Values.Add("action", "InDeX");

            // Act
            var candidates = selector.SelectCandidates(routeContext);

            // Assert
            var action = Assert.Single(candidates);
            Assert.Same(actions[0], action);
        }

        [Fact]
        public void SelectCandidates_Match_MatchesOnNull()
        {
            var actions = new ActionDescriptor[]
            {
                new ActionDescriptor()
                {
                    DisplayName = "A1",
                    RouteValues = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
                    {
                        { "area", null },
                        { "controller", "Home" },
                        { "action", "Index" }
                    },
                }
            };

            var selector = CreateSelector(actions);

            var routeContext = CreateRouteContext("GET");
            // Example: In conventional route, one could set non-inline defaults
            // new { area = (string)null, controller = "Foo", action = "Index" }
            routeContext.RouteData.Values.Add("area", null);
            routeContext.RouteData.Values.Add("controller", "Home");
            routeContext.RouteData.Values.Add("action", "Index");

            // Act
            var candidates = selector.SelectCandidates(routeContext);

            // Assert
            var action = Assert.Single(candidates);
            Assert.Same(actions[0], action);
        }

        [Fact]
        public void SelectCandidates_Match_ActionDescriptorWithEmptyRouteValues_MatchesOnEmptyString()
        {
            var actions = new ActionDescriptor[]
            {
                new ActionDescriptor()
                {
                    DisplayName = "A1",
                    RouteValues = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
                    {
                        { "foo", "" },
                        { "controller", "Home" },
                        { "action", "Index" }
                    },
                }
            };

            var selector = CreateSelector(actions);

            var routeContext = CreateRouteContext("GET");
            // Example: In conventional route, one could set non-inline defaults
            // new { area = (string)null, controller = "Home", action = "Index" }
            routeContext.RouteData.Values.Add("foo", "");
            routeContext.RouteData.Values.Add("controller", "Home");
            routeContext.RouteData.Values.Add("action", "Index");

            // Act
            var candidates = selector.SelectCandidates(routeContext);

            // Assert
            var action = Assert.Single(candidates);
            Assert.Same(actions[0], action);
        }

        [Fact]
        public void SelectCandidates_Match_ActionDescriptorWithEmptyRouteValues_MatchesOnNull()
        {
            var actions = new ActionDescriptor[]
            {
                new ActionDescriptor()
                {
                    DisplayName = "A1",
                    RouteValues = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
                    {
                        { "foo", "" },
                        { "controller", "Home" },
                        { "action", "Index" }
                    },
                }
            };

            var selector = CreateSelector(actions);

            var routeContext = CreateRouteContext("GET");
            // Example: In conventional route, one could set non-inline defaults
            // new { area = (string)null, controller = "Home", action = "Index" }
            routeContext.RouteData.Values.Add("foo", null);
            routeContext.RouteData.Values.Add("controller", "Home");
            routeContext.RouteData.Values.Add("action", "Index");

            // Act
            var candidates = selector.SelectCandidates(routeContext);

            // Assert
            var action = Assert.Single(candidates);
            Assert.Same(actions[0], action);
        }

        [Fact]
        public void SelectBestCandidate_AmbiguousActions_LogIsCorrect()
        {
            // Arrange
            var sink = new TestSink();
            var loggerFactory = new TestLoggerFactory(sink, enabled: true);

            var actions = new ActionDescriptor[]
            {
                new ActionDescriptor() { DisplayName = "A1" },
                new ActionDescriptor() { DisplayName = "A2" },
            };
            var selector = CreateSelector(actions, loggerFactory);

            var routeContext = CreateRouteContext("POST");
            var actionNames = string.Join(Environment.NewLine, actions.Select(action => action.DisplayName));
            var expectedMessage = "Request matched multiple actions resulting in " +
                $"ambiguity. Matching actions: {actionNames}";

            // Act
            Assert.Throws<AmbiguousActionException>(() => { selector.SelectBestCandidate(routeContext, actions); });

            // Assert
            Assert.Empty(sink.Scopes);
            var write = Assert.Single(sink.Writes);
            Assert.Equal(expectedMessage, write.State?.ToString());
        }

        [Fact]
        public void SelectBestCandidate_PrefersActionWithConstraints()
        {
            // Arrange
            var actionWithConstraints = new ActionDescriptor()
            {
                ActionConstraints = new List<IActionConstraintMetadata>()
                {
                    new HttpMethodActionConstraint(new string[] { "POST" }),
                },
                Parameters = new List<ParameterDescriptor>(),
            };

            var actionWithoutConstraints = new ActionDescriptor()
            {
                Parameters = new List<ParameterDescriptor>(),
            };

            var actions = new ActionDescriptor[] { actionWithConstraints, actionWithoutConstraints };

            var selector = CreateSelector(actions);
            var context = CreateRouteContext("POST");

            // Act
            var action = selector.SelectBestCandidate(context, actions);

            // Assert
            Assert.Same(action, actionWithConstraints);
        }

        [Fact]
        public void SelectBestCandidate_ConstraintsRejectAll()
        {
            // Arrange
            var action1 = new ActionDescriptor()
            {
                ActionConstraints = new List<IActionConstraintMetadata>()
                {
                    new BooleanConstraint() { Pass = false, },
                },
            };

            var action2 = new ActionDescriptor()
            {
                ActionConstraints = new List<IActionConstraintMetadata>()
                {
                    new BooleanConstraint() { Pass = false, },
                },
            };

            var actions = new ActionDescriptor[] { action1, action2 };

            var selector = CreateSelector(actions);
            var context = CreateRouteContext("POST");

            // Act
            var action = selector.SelectBestCandidate(context, actions);

            // Assert
            Assert.Null(action);
        }

        [Fact]
        public void SelectBestCandidate_ConstraintsRejectAll_DifferentStages()
        {
            // Arrange
            var action1 = new ActionDescriptor()
            {
                ActionConstraints = new List<IActionConstraintMetadata>()
                {
                    new BooleanConstraint() { Pass = false, Order = 0 },
                    new BooleanConstraint() { Pass = true, Order = 1 },
                },
            };

            var action2 = new ActionDescriptor()
            {
                ActionConstraints = new List<IActionConstraintMetadata>()
                {
                    new BooleanConstraint() { Pass = true, Order = 0 },
                    new BooleanConstraint() { Pass = false, Order = 1 },
                },
            };

            var actions = new ActionDescriptor[] { action1, action2 };

            var selector = CreateSelector(actions);
            var context = CreateRouteContext("POST");

            // Act
            var action = selector.SelectBestCandidate(context, actions);

            // Assert
            Assert.Null(action);
        }

        [Fact]
        public void SelectBestCandidate_ActionConstraintFactory()
        {
            // Arrange
            var actionWithConstraints = new ActionDescriptor()
            {
                ActionConstraints = new List<IActionConstraintMetadata>()
                {
                    new ConstraintFactory()
                    {
                        Constraint = new BooleanConstraint() { Pass = true },
                    },
                }
            };

            var actionWithoutConstraints = new ActionDescriptor()
            {
                Parameters = new List<ParameterDescriptor>(),
            };

            var actions = new ActionDescriptor[] { actionWithConstraints, actionWithoutConstraints };

            var selector = CreateSelector(actions);
            var context = CreateRouteContext("POST");

            // Act
            var action = selector.SelectBestCandidate(context, actions);

            // Assert
            Assert.Same(action, actionWithConstraints);
        }

        [Fact]
        public void SelectBestCandidate_ActionConstraintFactory_ReturnsNull()
        {
            // Arrange
            var nullConstraint = new ActionDescriptor()
            {
                ActionConstraints = new List<IActionConstraintMetadata>()
                {
                    new ConstraintFactory()
                    {
                    },
                }
            };

            var actions = new ActionDescriptor[] { nullConstraint };

            var selector = CreateSelector(actions);
            var context = CreateRouteContext("POST");

            // Act
            var action = selector.SelectBestCandidate(context, actions);

            // Assert
            Assert.Same(action, nullConstraint);
        }

        // There's a custom constraint provider registered that only understands BooleanConstraintMarker
        [Fact]
        public void SelectBestCandidate_CustomProvider()
        {
            // Arrange
            var actionWithConstraints = new ActionDescriptor()
            {
                ActionConstraints = new List<IActionConstraintMetadata>()
                {
                    new BooleanConstraintMarker() { Pass = true },
                }
            };

            var actionWithoutConstraints = new ActionDescriptor()
            {
                Parameters = new List<ParameterDescriptor>(),
            };

            var actions = new ActionDescriptor[] { actionWithConstraints, actionWithoutConstraints, };

            var selector = CreateSelector(actions);
            var context = CreateRouteContext("POST");

            // Act
            var action = selector.SelectBestCandidate(context, actions);

            // Assert
            Assert.Same(action, actionWithConstraints);
        }

        // Due to ordering of stages, the first action will be better.
        [Fact]
        public void SelectBestCandidate_ConstraintsInOrder()
        {
            // Arrange
            var best = new ActionDescriptor()
            {
                ActionConstraints = new List<IActionConstraintMetadata>()
                {
                    new BooleanConstraint() { Pass = true, Order = 0, },
                },
            };

            var worst = new ActionDescriptor()
            {
                ActionConstraints = new List<IActionConstraintMetadata>()
                {
                    new BooleanConstraint() { Pass = true, Order = 1, },
                },
            };

            var actions = new ActionDescriptor[] { best, worst };

            var selector = CreateSelector(actions);
            var context = CreateRouteContext("POST");

            // Act
            var action = selector.SelectBestCandidate(context, actions);

            // Assert
            Assert.Same(action, best);
        }

        // Due to ordering of stages, the first action will be better.
        [Fact]
        public void SelectBestCandidate_ConstraintsInOrder_MultipleStages()
        {
            // Arrange
            var best = new ActionDescriptor()
            {
                ActionConstraints = new List<IActionConstraintMetadata>()
                {
                    new BooleanConstraint() { Pass = true, Order = 0, },
                    new BooleanConstraint() { Pass = true, Order = 1, },
                    new BooleanConstraint() { Pass = true, Order = 2, },
                },
            };

            var worst = new ActionDescriptor()
            {
                ActionConstraints = new List<IActionConstraintMetadata>()
                {
                    new BooleanConstraint() { Pass = true, Order = 0, },
                    new BooleanConstraint() { Pass = true, Order = 1, },
                    new BooleanConstraint() { Pass = true, Order = 3, },
                },
            };

            var actions = new ActionDescriptor[] { best, worst };

            var selector = CreateSelector(actions);
            var context = CreateRouteContext("POST");

            // Act
            var action = selector.SelectBestCandidate(context, actions);

            // Assert
            Assert.Same(action, best);
        }

        [Fact]
        public void SelectBestCandidate_Fallback_ToActionWithoutConstraints()
        {
            // Arrange
            var nomatch1 = new ActionDescriptor()
            {
                ActionConstraints = new List<IActionConstraintMetadata>()
                {
                    new BooleanConstraint() { Pass = true, Order = 0, },
                    new BooleanConstraint() { Pass = true, Order = 1, },
                    new BooleanConstraint() { Pass = false, Order = 2, },
                },
            };

            var nomatch2 = new ActionDescriptor()
            {
                ActionConstraints = new List<IActionConstraintMetadata>()
                {
                    new BooleanConstraint() { Pass = true, Order = 0, },
                    new BooleanConstraint() { Pass = true, Order = 1, },
                    new BooleanConstraint() { Pass = false, Order = 3, },
                },
            };

            var best = new ActionDescriptor();

            var actions = new ActionDescriptor[] { best, nomatch1, nomatch2 };

            var selector = CreateSelector(actions);
            var context = CreateRouteContext("POST");

            // Act
            var action = selector.SelectBestCandidate(context, actions);

            // Assert
            Assert.Same(action, best);
        }

        [Fact]
        public void SelectBestCandidate_Ambiguous()
        {
            // Arrange
            var expectedMessage =
                "Multiple actions matched. " +
                "The following actions matched route data and had all constraints satisfied:" + Environment.NewLine +
                Environment.NewLine +
                "Ambiguous1" + Environment.NewLine +
                "Ambiguous2";

            var actions = new ActionDescriptor[]
            {
                CreateAction(area: null, controller: "Store", action: "Buy"),
                CreateAction(area: null, controller: "Store", action: "Buy"),
            };

            actions[0].DisplayName = "Ambiguous1";
            actions[1].DisplayName = "Ambiguous2";

            var selector = CreateSelector(actions);
            var context = CreateRouteContext("GET");

            context.RouteData.Values.Add("controller", "Store");
            context.RouteData.Values.Add("action", "Buy");

            // Act
            var ex = Assert.Throws<AmbiguousActionException>(() =>
            {
                selector.SelectBestCandidate(context, actions);
            });

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

        [Theory]
        [InlineData("GET")]
        [InlineData("PUT")]
        [InlineData("POST")]
        [InlineData("DELETE")]
        [InlineData("PATCH")]
        public void HttpMethodAttribute_ActionWithMultipleHttpMethodAttributeViaAcceptVerbs_ORsMultipleHttpMethods(string verb)
        {
            // Arrange
            var routeContext = new RouteContext(GetHttpContext(verb));
            routeContext.RouteData.Values.Add("controller", "HttpMethodAttributeTests_RestOnly");
            routeContext.RouteData.Values.Add("action", "Patch");

            // Act
            var result = InvokeActionSelector(routeContext);

            // Assert
            Assert.Equal("Patch", result.ActionName);
        }

        [Theory]
        [InlineData("GET")]
        [InlineData("PUT")]
        [InlineData("POST")]
        [InlineData("DELETE")]
        [InlineData("PATCH")]
        [InlineData("HEAD")]
        public void HttpMethodAttribute_ActionWithMultipleHttpMethodAttributes_ORsMultipleHttpMethods(string verb)
        {
            // Arrange
            var routeContext = new RouteContext(GetHttpContext(verb));
            routeContext.RouteData.Values.Add("controller", "HttpMethodAttributeTests_RestOnly");
            routeContext.RouteData.Values.Add("action", "Put");

            // Act
            var result = InvokeActionSelector(routeContext);

            // Assert
            Assert.Equal("Put", result.ActionName);
        }

        [Theory]
        [InlineData("GET")]
        [InlineData("PUT")]
        public void HttpMethodAttribute_ActionDecoratedWithHttpMethodAttribute_OverridesConvention(string verb)
        {
            // Arrange
            // Note no action name is passed, hence should return a null action descriptor.
            var routeContext = new RouteContext(GetHttpContext(verb));
            routeContext.RouteData.Values.Add("controller", "HttpMethodAttributeTests_RestOnly");

            // Act
            var result = InvokeActionSelector(routeContext);

            // Assert
            Assert.Null(result);
        }

        [Theory]
        [InlineData("Put")]
        [InlineData("RPCMethod")]
        [InlineData("RPCMethodWithHttpGet")]
        public void NonActionAttribute_ActionNotReachable(string actionName)
        {
            // Arrange
            var actionDescriptorProvider = GetActionDescriptorProvider();

            // Act
            var result = actionDescriptorProvider
                .GetDescriptors()
                .FirstOrDefault(x => x.ControllerName == "NonAction" && x.ActionName == actionName);

            // Assert
            Assert.Null(result);
        }

        [Theory]
        [InlineData("GET")]
        [InlineData("PUT")]
        [InlineData("POST")]
        [InlineData("DELETE")]
        [InlineData("PATCH")]
        public void ActionNameAttribute_ActionGetsExposedViaActionName_UnreachableByConvention(string verb)
        {
            // Arrange
            var routeContext = new RouteContext(GetHttpContext(verb));
            routeContext.RouteData.Values.Add("controller", "ActionName");
            routeContext.RouteData.Values.Add("action", "RPCMethodWithHttpGet");

            // Act
            var result = InvokeActionSelector(routeContext);

            // Assert
            Assert.Null(result);
        }

        [Theory]
        [InlineData("GET", "CustomActionName_Verb")]
        [InlineData("PUT", "CustomActionName_Verb")]
        [InlineData("POST", "CustomActionName_Verb")]
        [InlineData("DELETE", "CustomActionName_Verb")]
        [InlineData("PATCH", "CustomActionName_Verb")]
        [InlineData("GET", "CustomActionName_DefaultMethod")]
        [InlineData("PUT", "CustomActionName_DefaultMethod")]
        [InlineData("POST", "CustomActionName_DefaultMethod")]
        [InlineData("DELETE", "CustomActionName_DefaultMethod")]
        [InlineData("PATCH", "CustomActionName_DefaultMethod")]
        [InlineData("GET", "CustomActionName_RpcMethod")]
        [InlineData("PUT", "CustomActionName_RpcMethod")]
        [InlineData("POST", "CustomActionName_RpcMethod")]
        [InlineData("DELETE", "CustomActionName_RpcMethod")]
        [InlineData("PATCH", "CustomActionName_RpcMethod")]
        public void ActionNameAttribute_DifferentActionName_UsesActionNameFromActionNameAttribute(string verb, string actionName)
        {
            // Arrange
            var routeContext = new RouteContext(GetHttpContext(verb));
            routeContext.RouteData.Values.Add("controller", "ActionName");
            routeContext.RouteData.Values.Add("action", actionName);

            // Act
            var result = InvokeActionSelector(routeContext);

            // Assert
            Assert.Equal(actionName, result.ActionName);
        }

        private ControllerActionDescriptor InvokeActionSelector(RouteContext context)
        {
            var actionDescriptorProvider = GetActionDescriptorProvider();
            var actionDescriptorCollectionProvider = new DefaultActionDescriptorCollectionProvider(
                new[] { actionDescriptorProvider },
                Enumerable.Empty<IActionDescriptorChangeProvider>());

            var actionConstraintProviders = new[]
            {
                new DefaultActionConstraintProvider(),
            };

            var actionSelector = new ActionSelector(
                actionDescriptorCollectionProvider,
                GetActionConstraintCache(actionConstraintProviders),
                NullLoggerFactory.Instance);

            var candidates = actionSelector.SelectCandidates(context);
            return (ControllerActionDescriptor)actionSelector.SelectBestCandidate(context, candidates);
        }

        private ControllerActionDescriptorProvider GetActionDescriptorProvider()
        {
            var controllerTypes = typeof(ActionSelectorTest)
                .GetNestedTypes(BindingFlags.NonPublic)
                .Select(t => t.GetTypeInfo())
                .ToList();

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

            var manager = GetApplicationManager(controllerTypes);

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

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

            return provider;
        }

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

        private static HttpContext GetHttpContext(string httpMethod)
        {
            var httpContext = new DefaultHttpContext();
            httpContext.Request.Method = httpMethod;
            return httpContext;
        }

        private static ActionDescriptor[] GetActions()
        {
            return new ActionDescriptor[]
            {
                // Like a typical RPC controller
                CreateAction(area: null, controller: "Home", action: "Index"),
                CreateAction(area: null, controller: "Home", action: "Edit"),

                // Like a typical REST controller
                CreateAction(area: null, controller: "Product", action: null),
                CreateAction(area: null, controller: "Product", action: null),

                // RPC controller in an area with the same name as home
                CreateAction(area: "Admin", controller: "Home", action: "Index"),
                CreateAction(area: "Admin", controller: "Home", action: "Diagnostics"),
            };
        }

        private static IEnumerable<ActionDescriptor> GetActions(
            IEnumerable<ActionDescriptor> actions,
            string area,
            string controller,
            string action)
        {
            var comparer = new RouteValueEqualityComparer();

            return
                actions
                .Where(a => a.RouteValues.Any(kvp => kvp.Key == "area" && comparer.Equals(kvp.Value, area)))
                .Where(a => a.RouteValues.Any(kvp => kvp.Key == "controller" && comparer.Equals(kvp.Value, controller)))
                .Where(a => a.RouteValues.Any(kvp => kvp.Key == "action" && comparer.Equals(kvp.Value, action)));
        }

        private static ActionSelector CreateSelector(IReadOnlyList<ActionDescriptor> actions, ILoggerFactory loggerFactory = null)
        {
            loggerFactory = loggerFactory ?? NullLoggerFactory.Instance;

            var actionProvider = new Mock<IActionDescriptorCollectionProvider>(MockBehavior.Strict);

            actionProvider
                .Setup(p => p.ActionDescriptors)
                .Returns(new ActionDescriptorCollection(actions, 0));

            var actionConstraintProviders = new IActionConstraintProvider[] {
                    new DefaultActionConstraintProvider(),
                    new BooleanConstraintProvider(),
                };

            return new ActionSelector(
                actionProvider.Object,
                GetActionConstraintCache(actionConstraintProviders),
                loggerFactory);
        }

        private static VirtualPathContext CreateContext(object routeValues)
        {
            return CreateContext(routeValues, ambientValues: null);
        }

        private static VirtualPathContext CreateContext(object routeValues, object ambientValues)
        {
            return new VirtualPathContext(
                new Mock<HttpContext>(MockBehavior.Strict).Object,
                new RouteValueDictionary(ambientValues),
                new RouteValueDictionary(routeValues));
        }

        private static RouteContext CreateRouteContext(string httpMethod)
        {
            var routeData = new RouteData();
            routeData.Routers.Add(new Mock<IRouter>(MockBehavior.Strict).Object);

            var serviceProvider = new ServiceCollection().BuildServiceProvider();

            var httpContext = new Mock<HttpContext>(MockBehavior.Strict);

            var request = new Mock<HttpRequest>(MockBehavior.Strict);
            request.SetupGet(r => r.Method).Returns(httpMethod);
            request.SetupGet(r => r.Path).Returns(new PathString());
            request.SetupGet(r => r.Headers).Returns(new HeaderDictionary());
            httpContext.SetupGet(c => c.Request).Returns(request.Object);
            httpContext.SetupGet(c => c.RequestServices).Returns(serviceProvider);

            return new RouteContext(httpContext.Object)
            {
                RouteData = routeData,
            };
        }

        private static ActionDescriptor CreateAction(string area, string controller, string action)
        {
            var actionDescriptor = new ControllerActionDescriptor()
            {
                ActionName = string.Format("Area: {0}, Controller: {1}, Action: {2}", area, controller, action),
                Parameters = new List<ParameterDescriptor>(),
            };

            actionDescriptor.RouteValues.Add("area", area);
            actionDescriptor.RouteValues.Add("controller", controller);
            actionDescriptor.RouteValues.Add("action", action);

            return actionDescriptor;
        }

        private static ActionConstraintCache GetActionConstraintCache(IActionConstraintProvider[] actionConstraintProviders = null)
        {
            var descriptorProvider = new DefaultActionDescriptorCollectionProvider(
                Enumerable.Empty<IActionDescriptorProvider>(),
                Enumerable.Empty<IActionDescriptorChangeProvider>());
            return new ActionConstraintCache(descriptorProvider, actionConstraintProviders.AsEnumerable() ?? new List<IActionConstraintProvider>());
        }

        private class BooleanConstraint : IActionConstraint
        {
            public bool Pass { get; set; }

            public int Order { get; set; }

            public bool Accept(ActionConstraintContext context)
            {
                return Pass;
            }
        }

        private class ConstraintFactory : IActionConstraintFactory
        {
            public IActionConstraint Constraint { get; set; }

            public bool IsReusable => true;

            public IActionConstraint CreateInstance(IServiceProvider services)
            {
                return Constraint;
            }
        }

        private class BooleanConstraintMarker : IActionConstraintMetadata
        {
            public bool Pass { get; set; }
        }

        private class BooleanConstraintProvider : IActionConstraintProvider
        {
            public int Order { get; set; }

            public void OnProvidersExecuting(ActionConstraintProviderContext context)
            {
                foreach (var item in context.Results)
                {
                    if (item.Metadata is BooleanConstraintMarker marker)
                    {
                        Assert.Null(item.Constraint);
                        item.Constraint = new BooleanConstraint() { Pass = marker.Pass };
                    }
                }
            }

            public void OnProvidersExecuted(ActionConstraintProviderContext context)
            {
            }
        }

        private class NonActionController
        {
            [NonAction]
            public void Put()
            {
            }

            [NonAction]
            public void RPCMethod()
            {
            }

            [NonAction]
            [HttpGet]
            public void RPCMethodWithHttpGet()
            {
            }
        }

        private class ActionNameController
        {
            [ActionName("CustomActionName_Verb")]
            public void Put()
            {
            }

            [ActionName("CustomActionName_DefaultMethod")]
            public void Index()
            {
            }

            [ActionName("CustomActionName_RpcMethod")]
            public void RPCMethodWithHttpGet()
            {
            }
        }

        private class HttpMethodAttributeTests_RestOnlyController
        {
            [HttpGet]
            [HttpPut]
            [HttpPost]
            [HttpDelete]
            [HttpPatch]
            [HttpHead]
            public void Put()
            {
            }

            [AcceptVerbs("PUT", "post", "GET", "delete", "pATcH")]
            public void Patch()
            {
            }
        }
    }
}
