// 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.Linq;
using System.Reflection;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.ActionConstraints;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.Mvc.ModelBinding.Binders;
using Microsoft.AspNetCore.Mvc.ModelBinding.Metadata;
using Microsoft.AspNetCore.Mvc.Routing;
using Microsoft.Extensions.Options;
using Xunit;

namespace Microsoft.AspNetCore.Mvc.ApplicationModels
{
    public class DefaultApplicationModelProviderTest
    {
        private readonly TestApplicationModelProvider Provider = new TestApplicationModelProvider();

        [Fact]
        public void OnProvidersExecuting_AddsGlobalFilters()
        {
            // Arrange
            var options = new MvcOptions()
            {
                Filters =
                {
                    new MyFilterAttribute(),
                },
            };

            var builder = new TestApplicationModelProvider(options, TestModelMetadataProvider.CreateDefaultProvider());
            var context = new ApplicationModelProviderContext(Array.Empty<TypeInfo>());

            // Act
            builder.OnProvidersExecuting(context);

            // Assert
            Assert.Equal(options.Filters.ToArray(), context.Result.Filters);
        }

        [Fact]
        public void OnProvidersExecuting_IncludesAllControllers()
        {
            // Arrange
            var builder = new TestApplicationModelProvider();

            var context = new ApplicationModelProviderContext(new[] { typeof(ModelBinderController).GetTypeInfo(), typeof(ConventionallyRoutedController).GetTypeInfo() });

            // Act
            builder.OnProvidersExecuting(context);

            // Assert
            Assert.Collection(
                context.Result.Controllers.OrderBy(c => c.ControllerType.Name),
                c => Assert.Equal(typeof(ConventionallyRoutedController).GetTypeInfo(), c.ControllerType),
                c => Assert.Equal(typeof(ModelBinderController).GetTypeInfo(), c.ControllerType));
        }

        [Fact]
        public void OnProvidersExecuting_AddsControllerProperties()
        {
            // Arrange
            var builder = new TestApplicationModelProvider(
                new MvcOptions(),
                TestModelMetadataProvider.CreateDefaultProvider());
            var typeInfo = typeof(ModelBinderController).GetTypeInfo();

            var context = new ApplicationModelProviderContext(new[] { typeInfo });

            // Act
            builder.OnProvidersExecuting(context);

            // Assert
            var controllerModel = Assert.Single(context.Result.Controllers);
            Assert.Collection(
                controllerModel.ControllerProperties.OrderBy(p => p.PropertyName),
                property =>
                {
                    Assert.Equal(nameof(ModelBinderController.Bound), property.PropertyName);
                    Assert.Equal(BindingSource.Query, property.BindingInfo.BindingSource);
                    Assert.Same(controllerModel, property.Controller);

                    var attribute = Assert.Single(property.Attributes);
                    Assert.IsType<FromQueryAttribute>(attribute);
                },
                property =>
                {
                    Assert.Equal(nameof(ModelBinderController.FormFile), property.PropertyName);
                    Assert.Equal(BindingSource.FormFile, property.BindingInfo.BindingSource);
                    Assert.Same(controllerModel, property.Controller);

                    Assert.Empty(property.Attributes);
                },
                property =>
                {
                    Assert.Equal(nameof(ModelBinderController.Unbound), property.PropertyName);
                    Assert.Null(property.BindingInfo);
                    Assert.Same(controllerModel, property.Controller);
                });
        }

        [Fact]
        public void OnProvidersExecuting_ReadsBindingSourceForPropertiesFromModelMetadata()
        {
            // Arrange
            var detailsProvider = new BindingSourceMetadataProvider(typeof(string), BindingSource.Services);
            var modelMetadataProvider = TestModelMetadataProvider.CreateDefaultProvider(new[] { detailsProvider });
            var typeInfo = typeof(ModelBinderController).GetTypeInfo();
            var provider = new TestApplicationModelProvider(new MvcOptions(), modelMetadataProvider);

            var context = new ApplicationModelProviderContext(new[] { typeInfo });

            // Act
            provider.OnProvidersExecuting(context);

            // Assert
            var controllerModel = Assert.Single(context.Result.Controllers);
            Assert.Collection(
                controllerModel.ControllerProperties.OrderBy(p => p.PropertyName),
                property =>
                {
                    Assert.Equal(nameof(ModelBinderController.Bound), property.PropertyName);
                    Assert.Equal(BindingSource.Query, property.BindingInfo.BindingSource);
                    Assert.Same(controllerModel, property.Controller);

                    var attribute = Assert.Single(property.Attributes);
                    Assert.IsType<FromQueryAttribute>(attribute);
                },
                property =>
                {
                    Assert.Equal(nameof(ModelBinderController.FormFile), property.PropertyName);
                    Assert.Equal(BindingSource.FormFile, property.BindingInfo.BindingSource);
                    Assert.Same(controllerModel, property.Controller);

                    Assert.Empty(property.Attributes);
                },
                property =>
                {
                    Assert.Equal(nameof(ModelBinderController.Unbound), property.PropertyName);
                    Assert.Equal(BindingSource.Services, property.BindingInfo.BindingSource);
                    Assert.Same(controllerModel, property.Controller);
                });
        }

        [Fact]
        public void OnProvidersExecuting_AddsBindingSources_ForActionParameters()
        {
            // Arrange
            var builder = new TestApplicationModelProvider(
                new MvcOptions(),
                TestModelMetadataProvider.CreateDefaultProvider());
            var typeInfo = typeof(ModelBinderController).GetTypeInfo();

            var context = new ApplicationModelProviderContext(new[] { typeInfo });

            // Act
            builder.OnProvidersExecuting(context);

            // Assert
            var controllerModel = Assert.Single(context.Result.Controllers);
            var action = Assert.Single(controllerModel.Actions, a => a.ActionMethod.Name == nameof(ModelBinderController.PostAction));
            Assert.Collection(
                action.Parameters,
                parameter =>
                {
                    Assert.Equal("fromQuery", parameter.ParameterName);
                    Assert.Equal(BindingSource.Query, parameter.BindingInfo.BindingSource);
                    Assert.Same(action, parameter.Action);

                    var attribute = Assert.Single(parameter.Attributes);
                    Assert.IsType<FromQueryAttribute>(attribute);
                },
                parameter =>
                {
                    Assert.Equal("formFileCollection", parameter.ParameterName);
                    Assert.Equal(BindingSource.FormFile, parameter.BindingInfo.BindingSource);
                    Assert.Same(action, parameter.Action);

                    Assert.Empty(parameter.Attributes);
                },
                parameter =>
                {
                    Assert.Equal("unbound", parameter.ParameterName);
                    Assert.Null(parameter.BindingInfo);
                    Assert.Same(action, parameter.Action);
                });
        }

        [Fact]
        public void OnProvidersExecuting_InfersFormFileSourceForTypesAssignableFromIEnumerableOfFormFiles()
        {
            // Arrange
            var builder = new TestApplicationModelProvider(
                new MvcOptions(),
                TestModelMetadataProvider.CreateDefaultProvider());
            var typeInfo = typeof(ModelBinderController).GetTypeInfo();

            var context = new ApplicationModelProviderContext(new[] { typeInfo });

            // Act
            builder.OnProvidersExecuting(context);

            // Assert
            var controllerModel = Assert.Single(context.Result.Controllers);
            var action = Assert.Single(controllerModel.Actions, a => a.ActionMethod.Name == nameof(ModelBinderController.FormFilesSequences));
            Assert.Collection(
                action.Parameters,
                parameter =>
                {
                    Assert.Equal("formFileEnumerable", parameter.ParameterName);
                    Assert.Equal(BindingSource.FormFile, parameter.BindingInfo.BindingSource);
                },
                parameter =>
                {
                    Assert.Equal("formFileCollection", parameter.ParameterName);
                    Assert.Equal(BindingSource.FormFile, parameter.BindingInfo.BindingSource);
                },
                parameter =>
                {
                    Assert.Equal("formFileIList", parameter.ParameterName);
                    Assert.Equal(BindingSource.FormFile, parameter.BindingInfo.BindingSource);
                },
                parameter =>
                {
                    Assert.Equal("formFileList", parameter.ParameterName);
                    Assert.Equal(BindingSource.FormFile, parameter.BindingInfo.BindingSource);
                },
                parameter =>
                {
                    Assert.Equal("formFileArray", parameter.ParameterName);
                    Assert.Equal(BindingSource.FormFile, parameter.BindingInfo.BindingSource);
                });
        }

        [Fact]
        public void OnProvidersExecuting_AddsBindingSources_ForActionParameters_ReadFromModelMetadata()
        {
            // Arrange
            var options = new MvcOptions();
            var detailsProvider = new BindingSourceMetadataProvider(typeof(Guid), BindingSource.Special);
            var modelMetadataProvider = TestModelMetadataProvider.CreateDefaultProvider(new[] { detailsProvider });

            var provider = new TestApplicationModelProvider(options, modelMetadataProvider);
            var typeInfo = typeof(ModelBinderController).GetTypeInfo();

            var context = new ApplicationModelProviderContext(new[] { typeInfo });

            // Act
            provider.OnProvidersExecuting(context);

            // Assert
            var controllerModel = Assert.Single(context.Result.Controllers);
            var action = Assert.Single(controllerModel.Actions, a => a.ActionName == nameof(ModelBinderController.PostAction1));
            Assert.Collection(
                action.Parameters,
                parameter =>
                {
                    Assert.Equal("guid", parameter.ParameterName);
                    Assert.Equal(BindingSource.Special, parameter.BindingInfo.BindingSource);
                });
        }

        [Fact]
        public void OnProvidersExecuting_UsesBindingSourceSpecifiedOnParameter()
        {
            // Arrange
            var options = new MvcOptions();
            var detailsProvider = new BindingSourceMetadataProvider(typeof(Guid), BindingSource.Special);
            var modelMetadataProvider = TestModelMetadataProvider.CreateDefaultProvider(new[] { detailsProvider });

            var provider = new TestApplicationModelProvider(options, modelMetadataProvider);
            var typeInfo = typeof(ModelBinderController).GetTypeInfo();

            var context = new ApplicationModelProviderContext(new[] { typeInfo });

            // Act
            provider.OnProvidersExecuting(context);

            // Assert
            var controllerModel = Assert.Single(context.Result.Controllers);
            var action = Assert.Single(controllerModel.Actions, a => a.ActionName == nameof(ModelBinderController.PostAction2));
            Assert.Collection(
                action.Parameters,
                parameter =>
                {
                    Assert.Equal("fromQuery", parameter.ParameterName);
                    Assert.Equal(BindingSource.Query, parameter.BindingInfo.BindingSource);
                });
        }

        [Fact]
        public void OnProvidersExecuting_RemovesAsyncSuffix_WhenOptionIsSet()
        {
            // Arrange
            var options = new MvcOptions();
            var provider = new TestApplicationModelProvider(options, new EmptyModelMetadataProvider());
            var typeInfo = typeof(AsyncActionController).GetTypeInfo();
            var methodInfo = typeInfo.GetMethod(nameof(AsyncActionController.GetPersonAsync));

            var context = new ApplicationModelProviderContext(new[] { typeInfo });

            // Act
            provider.OnProvidersExecuting(context);

            // Assert
            var controllerModel = Assert.Single(context.Result.Controllers);
            var action = Assert.Single(controllerModel.Actions, a => a.ActionMethod == methodInfo);
            Assert.Equal("GetPerson", action.ActionName);
        }

        [Fact]
        public void OnProvidersExecuting_DoesNotRemoveAsyncSuffix_WhenOptionIsDisabled()
        {
            // Arrange
            var options = new MvcOptions { SuppressAsyncSuffixInActionNames = false };
            var provider = new TestApplicationModelProvider(options, new EmptyModelMetadataProvider());
            var typeInfo = typeof(AsyncActionController).GetTypeInfo();
            var methodInfo = typeInfo.GetMethod(nameof(AsyncActionController.GetPersonAsync));

            var context = new ApplicationModelProviderContext(new[] { typeInfo });

            // Act
            provider.OnProvidersExecuting(context);

            // Assert
            var controllerModel = Assert.Single(context.Result.Controllers);
            var action = Assert.Single(controllerModel.Actions, a => a.ActionMethod == methodInfo);
            Assert.Equal(nameof(AsyncActionController.GetPersonAsync), action.ActionName);
        }

        [Fact]
        public void OnProvidersExecuting_DoesNotRemoveAsyncSuffix_WhenActionNameIsSpecifiedUsingActionNameAttribute()
        {
            // Arrange
            var options = new MvcOptions();
            var provider = new TestApplicationModelProvider(options, new EmptyModelMetadataProvider());
            var typeInfo = typeof(AsyncActionController).GetTypeInfo();
            var methodInfo = typeInfo.GetMethod(nameof(AsyncActionController.GetAddressAsync));

            var context = new ApplicationModelProviderContext(new[] { typeInfo });

            // Act
            provider.OnProvidersExecuting(context);

            // Assert
            var controllerModel = Assert.Single(context.Result.Controllers);
            var action = Assert.Single(controllerModel.Actions, a => a.ActionMethod == methodInfo);
            Assert.Equal("GetRealAddressAsync", action.ActionName);
        }

        [Fact]
        public void CreateControllerModel_DerivedFromControllerClass_HasFilter()
        {
            // Arrange
            var builder = new TestApplicationModelProvider();
            var typeInfo = typeof(StoreController).GetTypeInfo();

            // Act
            var model = builder.CreateControllerModel(typeInfo);

            // Assert
            var filter = Assert.Single(model.Filters);
            Assert.IsType<ControllerActionFilter>(filter);
        }

        // This class has a filter attribute, but doesn't implement any filter interfaces,
        // so ControllerFilter is not present.
        [Fact]
        public void CreateControllerModel_ClassWithoutFilterInterfaces_HasNoControllerFilter()
        {
            // Arrange
            var builder = new TestApplicationModelProvider();
            var typeInfo = typeof(NoFiltersController).GetTypeInfo();

            // Act
            var model = builder.CreateControllerModel(typeInfo);

            // Assert
            var filter = Assert.Single(model.Filters);
            Assert.IsType<MyFilterAttribute>(filter);
        }

        [Fact]
        public void CreateControllerModel_ClassWithFilterInterfaces_HasFilter()
        {
            // Arrange
            var builder = new TestApplicationModelProvider();
            var typeInfo = typeof(SomeFiltersController).GetTypeInfo();

            // Act
            var model = builder.CreateControllerModel(typeInfo);

            // Assert
            Assert.Single(model.Filters, f => f is ControllerActionFilter);
            Assert.Single(model.Filters, f => f is ControllerResultFilter);
        }

        [Fact]
        public void CreateControllerModel_ClassWithFilterInterfaces_UnsupportedType()
        {
            // Arrange
            var builder = new TestApplicationModelProvider();
            var typeInfo = typeof(UnsupportedFiltersController).GetTypeInfo();

            // Act
            var model = builder.CreateControllerModel(typeInfo);

            // Assert
            Assert.Empty(model.Filters);
        }

        [Fact]
        public void CreateControllerModel_ClassWithInheritedRoutes()
        {
            // Arrange
            var builder = new TestApplicationModelProvider();
            var typeInfo = typeof(DerivedClassInheritingRoutesController).GetTypeInfo();

            // Act
            var model = builder.CreateControllerModel(typeInfo);

            // Assert
            var attributeRoutes = GetAttributeRoutes(model.Selectors);
            Assert.Equal(2, attributeRoutes.Count);
            Assert.Equal(2, model.Attributes.Count);

            var route = Assert.Single(attributeRoutes, r => r.Template == "A");
            Assert.Contains(route.Attribute, model.Attributes);

            route = Assert.Single(attributeRoutes, r => r.Template == "B");
            Assert.Contains(route.Attribute, model.Attributes);
        }

        [Fact]
        public void CreateControllerModel_ClassWithHiddenInheritedRoutes()
        {
            // Arrange
            var builder = new TestApplicationModelProvider();
            var typeInfo = typeof(DerivedClassHidingRoutesController).GetTypeInfo();

            // Act
            var model = builder.CreateControllerModel(typeInfo);

            // Assert
            var attributeRoutes = GetAttributeRoutes(model.Selectors);
            Assert.Equal(2, attributeRoutes.Count);
            Assert.Equal(2, model.Attributes.Count);

            var route = Assert.Single(attributeRoutes, r => r.Template == "C");
            Assert.Contains(route.Attribute, model.Attributes);

            route = Assert.Single(attributeRoutes, r => r.Template == "D");
            Assert.Contains(route.Attribute, model.Attributes);
        }

        [Theory]
        [InlineData("GetFromDerived", true)]
        [InlineData("NewMethod", true)] // "NewMethod" is a public method declared with keyword "new".
        [InlineData("GetFromBase", true)]
        public void IsAction_WithInheritedMethods(string methodName, bool expected)
        {
            // Arrange
            var builder = new TestApplicationModelProvider();
            var method = typeof(DerivedController).GetMethod(methodName);
            Assert.NotNull(method);

            // Act
            var isValid = builder.IsAction(typeof(DerivedController).GetTypeInfo(), method);

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

        [Fact]
        public void IsAction_OverridenMethodControllerClass()
        {
            // Arrange
            var builder = new TestApplicationModelProvider();
            var method = typeof(BaseController).GetMethod(nameof(BaseController.Redirect));
            Assert.NotNull(method);

            // Act
            var isValid = builder.IsAction(typeof(BaseController).GetTypeInfo(), method);

            // Assert
            Assert.False(isValid);
        }

        [Fact]
        public void IsAction_PrivateMethod_FromUserDefinedController()
        {
            // Arrange
            var builder = new TestApplicationModelProvider();
            var method = typeof(DerivedController).GetMethod(
                "PrivateMethod",
                BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance);
            Assert.NotNull(method);

            // Act
            var isValid = builder.IsAction(typeof(DerivedController).GetTypeInfo(), method);

            // Assert
            Assert.False(isValid);
        }

        [Fact]
        public void IsAction_OperatorOverloadingMethod_FromOperatorOverloadingController()
        {
            // Arrange
            var builder = new TestApplicationModelProvider();
            var method = typeof(OperatorOverloadingController).GetMethod("op_Addition");
            Assert.NotNull(method);
            Assert.True(method.IsSpecialName);

            // Act
            var isValid = builder.IsAction(typeof(OperatorOverloadingController).GetTypeInfo(), method);

            // Assert
            Assert.False(isValid);
        }

        [Fact]
        public void IsAction_GenericMethod_FromUserDefinedController()
        {
            // Arrange
            var builder = new TestApplicationModelProvider();
            var method = typeof(DerivedController).GetMethod("GenericMethod");
            Assert.NotNull(method);

            // Act
            var isValid = builder.IsAction(typeof(DerivedController).GetTypeInfo(), method);

            // Assert
            Assert.False(isValid);
        }

        [Fact]
        public void IsAction_OverridenNonActionMethod()
        {
            // Arrange
            var builder = new TestApplicationModelProvider();
            var method = typeof(DerivedController).GetMethod("OverridenNonActionMethod");
            Assert.NotNull(method);

            // Act
            var isValid = builder.IsAction(typeof(DerivedController).GetTypeInfo(), method);

            // Assert
            Assert.False(isValid);
        }

        [Theory]
        [InlineData("Equals")]
        [InlineData("GetHashCode")]
        [InlineData("MemberwiseClone")]
        [InlineData("ToString")]
        public void IsAction_OverriddenMethodsFromObjectClass(string methodName)
        {
            // Arrange
            var builder = new TestApplicationModelProvider();
            var method = typeof(DerivedController).GetMethod(
                methodName,
                BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance);
            Assert.NotNull(method);

            // Act
            var isValid = builder.IsAction(typeof(DerivedController).GetTypeInfo(), method);

            // Assert
            Assert.False(isValid);
        }

        [Fact]
        public void IsAction_DerivedControllerIDisposableDisposeMethod()
        {
            // Arrange
            var builder = new TestApplicationModelProvider();
            var typeInfo = typeof(DerivedController).GetTypeInfo();
            var methodInfo =
                typeInfo.GetRuntimeInterfaceMap(typeof(IDisposable)).TargetMethods[0];
            var method = typeInfo.AsType().GetMethods().SingleOrDefault(m => (m == methodInfo));
            Assert.NotNull(method);

            // Act
            var isValid = builder.IsAction(typeInfo, method);

            // Assert
            Assert.False(isValid);
        }

        [Fact]
        public void IsAction_DerivedControllerDisposeMethod()
        {
            // Arrange
            var builder = new TestApplicationModelProvider();
            var typeInfo = typeof(DerivedController).GetTypeInfo();
            var methodInfo =
                typeInfo.GetRuntimeInterfaceMap(typeof(IDisposable)).TargetMethods[0];
            var methods = typeInfo.AsType().GetMethods().Where(m => m.Name.Equals("Dispose") && m != methodInfo);

            Assert.NotEmpty(methods);

            foreach (var method in methods)
            {
                // Act
                var isValid = builder.IsAction(typeInfo, method);

                // Assert
                Assert.True(isValid);
            }
        }

        [Fact]
        public void IsAction_OverriddenDisposeMethod()
        {
            // Arrange
            var builder = new TestApplicationModelProvider();
            var typeInfo = typeof(DerivedOverriddenDisposeController).GetTypeInfo();
            var method = typeInfo.GetDeclaredMethods("Dispose").SingleOrDefault();
            Assert.NotNull(method);

            // Act
            var isValid = builder.IsAction(typeInfo, method);

            // Assert
            Assert.False(isValid);
        }

        [Fact]
        public void IsAction_NewDisposeMethod()
        {
            // Arrange
            var builder = new TestApplicationModelProvider();
            var typeInfo = typeof(DerivedNewDisposeController).GetTypeInfo();
            var method = typeInfo.GetDeclaredMethods("Dispose").SingleOrDefault();
            Assert.NotNull(method);

            // Act
            var isValid = builder.IsAction(typeInfo, method);

            // Assert
            Assert.True(isValid);
        }

        [Fact]
        public void IsAction_PocoControllerIDisposableDisposeMethod()
        {
            // Arrange
            var builder = new TestApplicationModelProvider();
            var typeInfo = typeof(IDisposablePocoController).GetTypeInfo();
            var methodInfo =
                typeInfo.GetRuntimeInterfaceMap(typeof(IDisposable)).TargetMethods[0];
            var method = typeInfo.AsType().GetMethods().SingleOrDefault(m => (m == methodInfo));
            Assert.NotNull(method);

            // Act
            var isValid = builder.IsAction(typeInfo, method);

            // Assert
            Assert.False(isValid);
        }

        [Fact]
        public void IsAction_PocoControllerDisposeMethod()
        {
            // Arrange
            var builder = new TestApplicationModelProvider();
            var typeInfo = typeof(IDisposablePocoController).GetTypeInfo();
            var methodInfo =
                typeInfo.GetRuntimeInterfaceMap(typeof(IDisposable)).TargetMethods[0];
            var methods = typeInfo.AsType().GetMethods().Where(m => m.Name.Equals("Dispose") && m != methodInfo);

            Assert.NotEmpty(methods);

            foreach (var method in methods)
            {
                // Act
                var isValid = builder.IsAction(typeInfo, method);

                // Assert
                Assert.True(isValid);
            }
        }

        [Fact]
        public void IsAction_SimplePocoControllerDisposeMethod()
        {
            // Arrange
            var builder = new TestApplicationModelProvider();
            var typeInfo = typeof(SimplePocoController).GetTypeInfo();
            var methods = typeInfo.AsType().GetMethods().Where(m => m.Name.Equals("Dispose"));

            Assert.NotEmpty(methods);

            foreach (var method in methods)
            {
                // Act
                var isValid = builder.IsAction(typeInfo, method);

                // Assert
                Assert.True(isValid);
            }
        }

        [Theory]
        [InlineData("StaticMethod")]
        [InlineData("ProtectedStaticMethod")]
        [InlineData("PrivateStaticMethod")]
        public void IsAction_StaticMethods(string methodName)
        {
            // Arrange
            var builder = new TestApplicationModelProvider();
            var method = typeof(DerivedController).GetMethod(
                methodName,
                BindingFlags.Static | BindingFlags.NonPublic | BindingFlags.Public);
            Assert.NotNull(method);

            // Act
            var isValid = builder.IsAction(typeof(DerivedController).GetTypeInfo(), method);

            // Assert
            Assert.False(isValid);
        }

        [Fact]
        public void CreateActionModel_ConventionallyRoutedAction_WithoutHttpConstraints()
        {
            // Arrange
            var builder = new TestApplicationModelProvider();
            var typeInfo = typeof(ConventionallyRoutedController).GetTypeInfo();
            var actionName = nameof(ConventionallyRoutedController.Edit);

            // Act
            var action = builder.CreateActionModel(typeInfo, typeInfo.AsType().GetMethod(actionName));

            // Assert
            Assert.NotNull(action);
            Assert.Equal(actionName, action.ActionName);
            Assert.Empty(action.Attributes);
            Assert.Single(action.Selectors);
            Assert.Empty(action.Selectors[0].ActionConstraints.OfType<HttpMethodActionConstraint>());
            Assert.Empty(GetAttributeRoutes(action.Selectors));
        }

        [Fact]
        public void CreateActionModel_ConventionallyRoutedAction_WithHttpConstraints()
        {
            // Arrange
            var builder = new TestApplicationModelProvider();
            var typeInfo = typeof(ConventionallyRoutedController).GetTypeInfo();
            var actionName = nameof(ConventionallyRoutedController.Update);

            // Act
            var action = builder.CreateActionModel(typeInfo, typeInfo.AsType().GetMethod(actionName));

            // Assert
            Assert.NotNull(action);
            Assert.Single(action.Selectors);
            var methodConstraint = Assert.Single(
                action.Selectors[0].ActionConstraints.OfType<HttpMethodActionConstraint>());
            Assert.Contains("PUT", methodConstraint.HttpMethods);
            Assert.Contains("PATCH", methodConstraint.HttpMethods);

            Assert.Equal(actionName, action.ActionName);
            Assert.Empty(GetAttributeRoutes(action.Selectors));
            Assert.IsType<CustomHttpMethodsAttribute>(Assert.Single(action.Attributes));
        }

        [Fact]
        public void CreateActionModel_ConventionallyRoutedActionWithHttpConstraints_AndInvalidRouteTemplateProvider()
        {
            // Arrange
            var builder = new TestApplicationModelProvider();
            var typeInfo = typeof(ConventionallyRoutedController).GetTypeInfo();
            var actionName = nameof(ConventionallyRoutedController.Delete);

            // Act
            var action = builder.CreateActionModel(typeInfo, typeInfo.AsType().GetMethod(actionName));

            // Assert
            Assert.NotNull(action);
            Assert.Single(action.Selectors);
            var methodConstraint = Assert.Single(
                action.Selectors[0].ActionConstraints.OfType<HttpMethodActionConstraint>());
            Assert.Contains("DELETE", methodConstraint.HttpMethods);
            Assert.Contains("HEAD", methodConstraint.HttpMethods);

            Assert.Equal(actionName, action.ActionName);
            Assert.Empty(GetAttributeRoutes(action.Selectors));
            Assert.Single(action.Attributes.OfType<HttpDeleteAttribute>());
            Assert.Single(action.Attributes.OfType<HttpHeadAttribute>());
        }

        [Fact]
        public void CreateActionModel_ConventionallyRoutedAction_WithMultipleHttpConstraints()
        {
            // Arrange
            var builder = new TestApplicationModelProvider();
            var typeInfo = typeof(ConventionallyRoutedController).GetTypeInfo();
            var actionName = nameof(ConventionallyRoutedController.Details);

            // Act
            var action = builder.CreateActionModel(typeInfo, typeInfo.AsType().GetMethod(actionName));

            // Assert
            Assert.NotNull(action);
            Assert.Single(action.Selectors);
            var methodConstraint = Assert.Single(
                action.Selectors[0].ActionConstraints.OfType<HttpMethodActionConstraint>());
            Assert.Contains("GET", methodConstraint.HttpMethods);
            Assert.Contains("POST", methodConstraint.HttpMethods);
            Assert.Contains("HEAD", methodConstraint.HttpMethods);
            Assert.Equal(actionName, action.ActionName);
            Assert.Empty(GetAttributeRoutes(action.Selectors));
        }

        [Fact]
        public void CreateActionModel_ConventionallyRoutedAction_WithMultipleOverlappingHttpConstraints()
        {
            // Arrange
            var builder = new TestApplicationModelProvider();
            var typeInfo = typeof(ConventionallyRoutedController).GetTypeInfo();
            var actionName = nameof(ConventionallyRoutedController.List);

            // Act
            var action = builder.CreateActionModel(typeInfo, typeInfo.AsType().GetMethod(actionName));

            // Assert
            Assert.NotNull(action);
            Assert.Single(action.Selectors);
            var methodConstraint = Assert.Single(
                action.Selectors[0].ActionConstraints.OfType<HttpMethodActionConstraint>());
            Assert.Contains("GET", methodConstraint.HttpMethods);
            Assert.Contains("PUT", methodConstraint.HttpMethods);
            Assert.Contains("POST", methodConstraint.HttpMethods);
            Assert.Equal(actionName, action.ActionName);
            Assert.Empty(GetAttributeRoutes(action.Selectors));
        }

        [Fact]
        public void CreateActionModel_AttributeRouteOnAction()
        {
            // Arrange
            var builder = new TestApplicationModelProvider();
            var typeInfo = typeof(NoRouteAttributeOnControllerController).GetTypeInfo();
            var actionName = nameof(NoRouteAttributeOnControllerController.Edit);

            // Act
            var action = builder.CreateActionModel(typeInfo, typeInfo.AsType().GetMethod(actionName));

            // Assert
            Assert.NotNull(action);
            Assert.Single(action.Selectors);
            var methodConstraint = Assert.Single(
                action.Selectors[0].ActionConstraints.OfType<HttpMethodActionConstraint>());

            Assert.Equal(actionName, action.ActionName);

            var httpMethod = Assert.Single(methodConstraint.HttpMethods);
            Assert.Equal("HEAD", httpMethod);

            var attributeRoute = Assert.Single(GetAttributeRoutes(action.Selectors));
            Assert.Equal("Change", attributeRoute.Template);

            Assert.IsType<HttpHeadAttribute>(Assert.Single(action.Attributes));
        }

        [Fact]
        public void CreateActionModel_AttributeRouteOnAction_RouteAttribute()
        {
            // Arrange
            var builder = new TestApplicationModelProvider();
            var typeInfo = typeof(NoRouteAttributeOnControllerController).GetTypeInfo();
            var actionName = nameof(NoRouteAttributeOnControllerController.Update);

            // Act
            var action = builder.CreateActionModel(typeInfo, typeInfo.AsType().GetMethod(actionName));

            // Assert
            Assert.NotNull(action);
            Assert.Single(action.Selectors);
            Assert.Empty(action.Selectors[0].ActionConstraints);

            Assert.Equal(actionName, action.ActionName);

            var attributeRoute = Assert.Single(GetAttributeRoutes(action.Selectors));
            Assert.Equal("Update", attributeRoute.Template);

            Assert.IsType<RouteAttribute>(Assert.Single(action.Attributes));
        }

        [Fact]
        public void CreateActionModel_AttributeRouteOnAction_AcceptVerbsAttributeWithTemplate()
        {
            // Arrange
            var builder = new TestApplicationModelProvider();
            var typeInfo = typeof(NoRouteAttributeOnControllerController).GetTypeInfo();
            var actionName = nameof(NoRouteAttributeOnControllerController.List);

            // Act
            var action = builder.CreateActionModel(typeInfo, typeInfo.AsType().GetMethod(actionName));

            // Assert
            Assert.NotNull(action);
            Assert.Single(action.Selectors);
            var methodConstraint = Assert.Single(
                action.Selectors[0].ActionConstraints.OfType<HttpMethodActionConstraint>());

            Assert.Equal(actionName, action.ActionName);

            Assert.Equal(
                new[] { "GET", "HEAD" },
                methodConstraint.HttpMethods.OrderBy(m => m, StringComparer.Ordinal));

            var attributeRoute = Assert.Single(GetAttributeRoutes(action.Selectors));
            Assert.Equal("ListAll", attributeRoute.Template);

            Assert.IsType<AcceptVerbsAttribute>(Assert.Single(action.Attributes));
        }

        [Fact]
        public void CreateActionModel_AttributeRouteOnAction_CreatesOneActionInfoPerRouteTemplate()
        {
            // Arrange
            var builder = new TestApplicationModelProvider();
            var typeInfo = typeof(NoRouteAttributeOnControllerController).GetTypeInfo();
            var actionName = nameof(NoRouteAttributeOnControllerController.Index);

            // Act
            var action = builder.CreateActionModel(typeInfo, typeInfo.AsType().GetMethod(actionName));

            // Assert
            Assert.NotNull(action);
            Assert.Equal(actionName, action.ActionName);
            Assert.NotNull(action.Attributes);
            Assert.Equal(2, action.Attributes.Count);
            Assert.Single(action.Attributes.OfType<HttpGetAttribute>());
            Assert.Single(action.Attributes.OfType<HttpPostAttribute>());
            Assert.Equal(2, action.Selectors.Count);

            foreach (var actionSelectorModel in action.Selectors)
            {
                Assert.NotNull(actionSelectorModel.AttributeRouteModel);
            }

            var selectorModel = Assert.Single(action.Selectors, ai => ai.AttributeRouteModel?.Template == "List");
            var methodConstraint = Assert.Single(selectorModel.ActionConstraints.OfType<HttpMethodActionConstraint>());
            var listMethod = Assert.Single(methodConstraint.HttpMethods);
            Assert.Equal("POST", listMethod);

            var all = Assert.Single(action.Selectors, ai => ai.AttributeRouteModel?.Template == "All");
            methodConstraint = Assert.Single(all.ActionConstraints.OfType<HttpMethodActionConstraint>());
            var allMethod = Assert.Single(methodConstraint.HttpMethods);
            Assert.Equal("GET", allMethod);
        }

        [Fact]
        public void CreateActionModel_NoRouteOnController_AllowsConventionallyRoutedActions_OnTheSameController()
        {
            // Arrange
            var builder = new TestApplicationModelProvider();
            var typeInfo = typeof(NoRouteAttributeOnControllerController).GetTypeInfo();
            var actionName = nameof(NoRouteAttributeOnControllerController.Remove);

            // Act
            var action = builder.CreateActionModel(typeInfo, typeInfo.AsType().GetMethod(actionName));

            // Assert
            Assert.NotNull(action);

            Assert.Equal(actionName, action.ActionName);
            Assert.Empty(action.Attributes);
            Assert.Single(action.Selectors);
            Assert.Empty(action.Selectors[0].ActionConstraints);
            Assert.Null(action.Selectors[0].AttributeRouteModel);
        }

        [Theory]
        [InlineData(typeof(SingleRouteAttributeController))]
        [InlineData(typeof(MultipleRouteAttributeController))]
        public void CreateActionModel_RouteAttributeOnController_CreatesAttributeRoute_ForNonAttributedActions(Type controller)
        {
            // Arrange
            var builder = new TestApplicationModelProvider();
            var typeInfo = controller.GetTypeInfo();

            // Act
            var action = builder.CreateActionModel(typeInfo, typeInfo.AsType().GetMethod("Delete"));

            // Assert
            Assert.NotNull(action);

            Assert.Equal("Delete", action.ActionName);

            Assert.Single(action.Selectors);
            Assert.Empty(action.Selectors[0].ActionConstraints);
            Assert.Empty(GetAttributeRoutes(action.Selectors));
            Assert.Empty(action.Attributes);
        }

        [Theory]
        [InlineData(typeof(SingleRouteAttributeController))]
        [InlineData(typeof(MultipleRouteAttributeController))]
        public void CreateActionModel_RouteOnController_CreatesOneActionInfoPerRouteTemplateOnAction(Type controller)
        {
            // Arrange
            var builder = new TestApplicationModelProvider();
            var typeInfo = controller.GetTypeInfo();

            // Act
            var action = builder.CreateActionModel(typeInfo, typeInfo.AsType().GetMethod("Index"));

            // Assert
            Assert.NotNull(action.Attributes);
            Assert.Equal(2, action.Attributes.Count);
            Assert.Equal(2, action.Selectors.Count);
            Assert.Equal("Index", action.ActionName);

            foreach (var selectorModel in action.Selectors)
            {
                var methodConstraint = Assert.Single(selectorModel.ActionConstraints.OfType<HttpMethodActionConstraint>());
                var httpMethod = Assert.Single(methodConstraint.HttpMethods);
                Assert.Equal("GET", httpMethod);

                Assert.NotNull(selectorModel.AttributeRouteModel.Template);
            }

            Assert.Single(action.Selectors, ai => ai.AttributeRouteModel.Template.Equals("List"));
            Assert.Single(action.Selectors, ai => ai.AttributeRouteModel.Template.Equals("All"));
        }

        [Fact]
        public void CreateActionModel_MixedHttpVerbsAndRoutes_EmptyVerbWithRoute()
        {
            // Arrange
            var builder = new TestApplicationModelProvider();
            var typeInfo = typeof(MixedHttpVerbsAndRouteAttributeController).GetTypeInfo();
            var actionName = nameof(MixedHttpVerbsAndRouteAttributeController.VerbAndRoute);

            // Act
            var action = builder.CreateActionModel(typeInfo, typeInfo.AsType().GetMethod(actionName));

            // Assert
            Assert.NotNull(action);
            Assert.Single(action.Selectors);
            var methodConstraint = Assert.Single(
                action.Selectors[0].ActionConstraints.OfType<HttpMethodActionConstraint>());
            Assert.Equal<string>(new string[] { "GET" }, methodConstraint.HttpMethods);
            var attributeRoute = Assert.Single(GetAttributeRoutes(action.Selectors));
            Assert.Equal("Products", attributeRoute.Template);
        }

        [Fact]
        public void CreateActionModel_MixedHttpVerbsAndRoutes_MultipleEmptyVerbsWithMultipleRoutes()
        {
            // Arrange
            var builder = new TestApplicationModelProvider();
            var typeInfo = typeof(MixedHttpVerbsAndRouteAttributeController).GetTypeInfo();
            var actionName = nameof(MixedHttpVerbsAndRouteAttributeController.MultipleVerbsAndRoutes);

            // Act
            var actions = builder.CreateActionModel(typeInfo, typeInfo.AsType().GetMethod(actionName));

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

            // OrderBy is used because the order of the results may very depending on the platform / client.
            var selectorModel = Assert.Single(actions.Selectors, a => a.AttributeRouteModel.Template == "Products");
            var methodConstraint = Assert.Single(selectorModel.ActionConstraints.OfType<HttpMethodActionConstraint>());
            Assert.Equal(new[] { "GET", "POST" }, methodConstraint.HttpMethods.OrderBy(key => key, StringComparer.Ordinal));

            selectorModel = Assert.Single(actions.Selectors, a => a.AttributeRouteModel.Template == "v2/Products");
            methodConstraint = Assert.Single(selectorModel.ActionConstraints.OfType<HttpMethodActionConstraint>());
            Assert.Equal(new[] { "GET", "POST" }, methodConstraint.HttpMethods.OrderBy(key => key, StringComparer.Ordinal));
        }

        [Fact]
        public void CreateActionModel_MixedHttpVerbsAndRoutes_MultipleEmptyAndNonEmptyVerbsWithMultipleRoutes()
        {
            // Arrange
            var builder = new TestApplicationModelProvider();
            var typeInfo = typeof(MixedHttpVerbsAndRouteAttributeController).GetTypeInfo();
            var actionName = nameof(MixedHttpVerbsAndRouteAttributeController.MultipleVerbsWithAnyWithoutTemplateAndRoutes);

            // Act
            var action = builder.CreateActionModel(typeInfo, typeInfo.AsType().GetMethod(actionName));

            // Assert
            Assert.Equal(3, action.Selectors.Count);

            var selectorModel = Assert.Single(action.Selectors, s => s.AttributeRouteModel.Template == "Products");
            var methodConstraint = Assert.Single(selectorModel.ActionConstraints.OfType<HttpMethodActionConstraint>());
            Assert.Equal<string>(new string[] { "GET" }, methodConstraint.HttpMethods);

            selectorModel = Assert.Single(action.Selectors, s => s.AttributeRouteModel.Template == "v2/Products");
            methodConstraint = Assert.Single(selectorModel.ActionConstraints.OfType<HttpMethodActionConstraint>());
            Assert.Equal<string>(new string[] { "GET" }, methodConstraint.HttpMethods);

            selectorModel = Assert.Single(action.Selectors, s => s.AttributeRouteModel.Template == "Products/Buy");
            methodConstraint = Assert.Single(selectorModel.ActionConstraints.OfType<HttpMethodActionConstraint>());
            Assert.Equal<string>(new string[] { "POST" }, methodConstraint.HttpMethods);
        }

        [Fact]
        public void CreateActionModel_MixedHttpVerbsAndRoutes_WithRouteOnController()
        {
            // Arrange
            var builder = new TestApplicationModelProvider();
            var typeInfo = typeof(RouteAttributeOnController).GetTypeInfo();
            var actionName = nameof(RouteAttributeOnController.Get);

            // Act
            var action = builder.CreateActionModel(typeInfo, typeInfo.AsType().GetMethod(actionName));

            // Assert
            Assert.Equal(2, action.Selectors.Count);

            var selectorModel = Assert.Single(action.Selectors, s => s.AttributeRouteModel == null);
            var methodConstraint = Assert.Single(selectorModel.ActionConstraints.OfType<HttpMethodActionConstraint>());
            Assert.Equal(new string[] { "GET" }, methodConstraint.HttpMethods);

            selectorModel = Assert.Single(action.Selectors, s => s.AttributeRouteModel?.Template == "id/{id?}");
            methodConstraint = Assert.Single(selectorModel.ActionConstraints.OfType<HttpMethodActionConstraint>());
            Assert.Equal(new string[] { "GET" }, methodConstraint.HttpMethods);
        }

        [Fact]
        public void CreateActionModel_MixedHttpVerbsAndRoutes_MultipleEmptyAndNonEmptyVerbs()
        {
            // Arrange
            var builder = new TestApplicationModelProvider();
            var typeInfo = typeof(MixedHttpVerbsAndRouteAttributeController).GetTypeInfo();
            var actionName = nameof(MixedHttpVerbsAndRouteAttributeController.Invalid);

            // Act
            var action = builder.CreateActionModel(typeInfo, typeInfo.AsType().GetMethod(actionName));

            // Assert
            Assert.NotNull(action);
            Assert.Equal(2, action.Selectors.Count);

            var selectorModel = Assert.Single(action.Selectors, s => s.AttributeRouteModel?.Template == "Products");
            var methodConstraint = Assert.Single(selectorModel.ActionConstraints.OfType<HttpMethodActionConstraint>());
            Assert.Equal<string>(new string[] { "POST" }, methodConstraint.HttpMethods);

            selectorModel = Assert.Single(action.Selectors, s => s.AttributeRouteModel?.Template == null);
            methodConstraint = Assert.Single(selectorModel.ActionConstraints.OfType<HttpMethodActionConstraint>());
            Assert.Equal<string>(new string[] { "GET" }, methodConstraint.HttpMethods);
        }

        [Fact]
        public void CreateActionModel_SplitsConstraintsBasedOnRoute()
        {
            // Arrange
            var builder = new TestApplicationModelProvider();
            var typeInfo = typeof(MultipleRouteProviderOnActionController).GetTypeInfo();
            var methodInfo = typeInfo.GetMethod(nameof(MultipleRouteProviderOnActionController.Edit));

            // Act
            var actionModel = builder.CreateActionModel(typeInfo, methodInfo);

            // Assert
            Assert.Equal(3, actionModel.Attributes.Count);
            Assert.Equal(2, actionModel.Attributes.OfType<RouteAndConstraintAttribute>().Count());
            Assert.Single(actionModel.Attributes.OfType<ConstraintAttribute>());
            Assert.Equal(2, actionModel.Selectors.Count);

            var selectorModel = Assert.Single(
                actionModel.Selectors.Where(sm => sm.AttributeRouteModel?.Template == "R1"));

            Assert.Equal(2, selectorModel.ActionConstraints.Count);
            Assert.Single(selectorModel.ActionConstraints.OfType<RouteAndConstraintAttribute>());
            Assert.Single(selectorModel.ActionConstraints.OfType<ConstraintAttribute>());

            selectorModel = Assert.Single(
                actionModel.Selectors.Where(sm => sm.AttributeRouteModel?.Template == "R2"));

            Assert.Equal(2, selectorModel.ActionConstraints.Count);
            Assert.Single(selectorModel.ActionConstraints.OfType<RouteAndConstraintAttribute>());
            Assert.Single(selectorModel.ActionConstraints.OfType<ConstraintAttribute>());
        }

        [Fact]
        public void CreateActionModel_InheritedAttributeRoutes()
        {
            // Arrange
            var builder = new TestApplicationModelProvider();
            var typeInfo = typeof(DerivedClassInheritsAttributeRoutesController).GetTypeInfo();
            var actionName = nameof(DerivedClassInheritsAttributeRoutesController.Edit);

            // Act
            var actions = builder.CreateActionModel(typeInfo, typeInfo.AsType().GetMethod(actionName));

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

            var selectorModel = Assert.Single(actions.Selectors, a => a.AttributeRouteModel?.Template == "A");
            Assert.Contains(selectorModel.AttributeRouteModel.Attribute, actions.Attributes);

            selectorModel = Assert.Single(actions.Selectors, a => a.AttributeRouteModel?.Template == "B");
            Assert.Contains(selectorModel.AttributeRouteModel.Attribute, actions.Attributes);
        }

        [Fact]
        public void CreateActionModel_InheritedAttributeRoutesOverridden()
        {
            // Arrange
            var builder = new TestApplicationModelProvider();
            var typeInfo = typeof(DerivedClassOverridesAttributeRoutesController).GetTypeInfo();
            var actionName = nameof(DerivedClassOverridesAttributeRoutesController.Edit);

            // Act
            var action = builder.CreateActionModel(typeInfo, typeInfo.AsType().GetMethod(actionName));

            // Assert
            Assert.Equal(4, action.Attributes.Count);
            Assert.Equal(2, action.Selectors.Count);

            var selectorModel = Assert.Single(action.Selectors, a => a.AttributeRouteModel?.Template == "C");
            Assert.Contains(selectorModel.AttributeRouteModel.Attribute, action.Attributes);

            selectorModel = Assert.Single(action.Selectors, a => a.AttributeRouteModel?.Template == "D");
            Assert.Contains(selectorModel.AttributeRouteModel.Attribute, action.Attributes);
        }

        [Fact]
        public void ControllerDispose_ExplicitlyImplemented_IDisposableMethods_AreTreatedAs_NonActions()
        {
            // Arrange
            var builder = new TestApplicationModelProvider();
            var typeInfo = typeof(DerivedFromControllerAndExplicitIDisposableImplementationController).GetTypeInfo();
            var context = new ApplicationModelProviderContext(new[] { typeInfo });

            // Act
            builder.OnProvidersExecuting(context);

            // Assert
            var model = Assert.Single(context.Result.Controllers);
            Assert.Empty(model.Actions);
        }

        [Fact]
        public void ControllerDispose_MethodsNamedDispose_AreTreatedAsActions()
        {
            // Arrange
            var builder = new TestApplicationModelProvider();
            var typeInfo = typeof(DerivedFromControllerAndHidesBaseDisposeMethodController).GetTypeInfo();
            var context = new ApplicationModelProviderContext(new[] { typeInfo });

            // Act
            builder.OnProvidersExecuting(context);

            // Assert
            var model = Assert.Single(context.Result.Controllers);
            var action = Assert.Single(model.Actions);

            // Make sure that the Dispose method is from the derived controller and not the base 'Controller' type
            Assert.Equal(typeInfo, action.ActionMethod.DeclaringType.GetTypeInfo());
        }

        [BindProperties]
        public class BindPropertyController
        {
            public string Property { get; set; }

            [ModelBinder(typeof(ComplexObjectModelBinder))]
            public string BinderType { get; set; }

            [FromRoute]
            public string BinderSource { get; set; }
        }

        [Fact]
        public void CreatePropertyModel_AddsBindingInfoToProperty_IfDeclaringTypeHasBindPropertiesAttribute()
        {
            // Arrange
            var propertyInfo = typeof(BindPropertyController).GetProperty(nameof(BindPropertyController.Property));

            // Act
            var property = Provider.CreatePropertyModel(propertyInfo);

            // Assert
            var bindingInfo = property.BindingInfo;
            Assert.NotNull(bindingInfo);
            Assert.Null(bindingInfo.BinderModelName);
            Assert.Null(bindingInfo.BinderType);
            Assert.Null(bindingInfo.BindingSource);
            Assert.Null(bindingInfo.PropertyFilterProvider);
            Assert.NotNull(bindingInfo.RequestPredicate);
        }

        [Fact]
        public void CreatePropertyModel_DoesNotSetBindingInfo_IfPropertySpecifiesBinderType()
        {
            // Arrange
            var propertyInfo = typeof(BindPropertyController).GetProperty(nameof(BindPropertyController.BinderType));

            // Act
            var property = Provider.CreatePropertyModel(propertyInfo);

            // Assert
            var bindingInfo = property.BindingInfo;
            Assert.Same(typeof(ComplexObjectModelBinder), bindingInfo.BinderType);
        }

        [Fact]
        public void CreatePropertyModel_DoesNotSetBindingInfo_IfPropertySpecifiesBinderSource()
        {
            // Arrange
            var propertyInfo = typeof(BindPropertyController).GetProperty(nameof(BindPropertyController.BinderSource));

            // Act
            var property = Provider.CreatePropertyModel(propertyInfo);

            // Assert
            var bindingInfo = property.BindingInfo;
            Assert.Null(bindingInfo.BinderType);
            Assert.Same(BindingSource.Path, property.BindingInfo.BindingSource);
        }


        public class DerivedFromBindPropertyController : BindPropertyController
        {
            public string DerivedProperty { get; set; }
        }

        [Fact]
        public void CreatePropertyModel_AppliesBindPropertyAttributeDeclaredOnBaseType()
        {
            // Arrange
            var propertyInfo = typeof(DerivedFromBindPropertyController).GetProperty(
                nameof(DerivedFromBindPropertyController.DerivedProperty));

            // Act
            var property = Provider.CreatePropertyModel(propertyInfo);

            // Assert
            Assert.NotNull(property.BindingInfo);
        }

        [BindProperties]
        public class UserController : ControllerBase
        {
            public string DerivedProperty { get; set; }
        }

        [Fact]
        public void CreatePropertyModel_DoesNotApplyBindingInfoToPropertiesOnBaseType()
        {
            // This test ensures that applying BindPropertyAttribute on a user defined type does not cause properties on
            // Controller \ ControllerBase to be treated as model bound.
            // Arrange
            var derivedPropertyInfo = typeof(UserController).GetProperty(nameof(UserController.DerivedProperty));
            var basePropertyInfo = typeof(UserController).GetProperty(nameof(ControllerBase.ControllerContext));

            // Act
            var derivedProperty = Provider.CreatePropertyModel(derivedPropertyInfo);
            var baseProperty = Provider.CreatePropertyModel(basePropertyInfo);

            // Assert
            Assert.NotNull(derivedProperty.BindingInfo);
            Assert.Null(baseProperty.BindingInfo);
        }

        private IList<AttributeRouteModel> GetAttributeRoutes(IList<SelectorModel> selectors)
        {
            return selectors
                .Where(sm => sm.AttributeRouteModel != null)
                .Select(sm => sm.AttributeRouteModel)
                .ToList();
        }

        private class DerivedFromControllerAndExplicitIDisposableImplementationController
            : ViewFeaturesController, IDisposable
        {
            void IDisposable.Dispose()
            {
                throw new NotImplementedException();
            }
        }

        private class DerivedFromControllerAndHidesBaseDisposeMethodController : ViewFeaturesController
        {
            public new void Dispose()
            {
                throw new NotImplementedException();
            }
        }

        private class ViewFeaturesController : ControllerBase, IDisposable
        {
            public virtual void Dispose()
            {
            }
        }

        private class BaseClassWithAttributeRoutesController
        {
            [Route("A")]
            [Route("B")]
            public virtual void Edit()
            {
            }
        }

        private class DerivedClassInheritsAttributeRoutesController : BaseClassWithAttributeRoutesController
        {
            public override void Edit()
            {
            }
        }

        private class DerivedClassOverridesAttributeRoutesController : BaseClassWithAttributeRoutesController
        {
            [Route("C")]
            [Route("D")]
            public override void Edit()
            {
            }
        }

        private class Controller : IDisposable
        {
            public void Dispose()
            {
                throw new NotImplementedException();
            }

            [NonAction]
            public virtual IActionResult Redirect(string url)
            {
                return null;
            }
        }

        private class BaseController : Controller
        {
            public void GetFromBase() // Valid action method.
            {
            }

            [NonAction]
            public virtual void OverridenNonActionMethod()
            {
            }

            [NonAction]
            public virtual void NewMethod()
            {
            }

            public override IActionResult Redirect(string url)
            {
                return base.Redirect(url + "#RedirectOverride");
            }
        }

        private class DerivedController : BaseController
        {
            public void GetFromDerived() // Valid action method.
            {
            }

            [HttpGet]
            public override void OverridenNonActionMethod()
            {
            }

            public new void NewMethod() // Valid action method.
            {
            }

            public void GenericMethod<T>()
            {
            }

            private void PrivateMethod()
            {
            }

            public static void StaticMethod()
            {
            }

            protected static void ProtectedStaticMethod()
            {
            }

            private static void PrivateStaticMethod()
            {
            }

            public string Dispose(string s)
            {
                return s;
            }

            public new void Dispose()
            {
            }
        }

        private class IDisposablePocoController : IDisposable
        {
            public string Index()
            {
                return "Hello world";
            }

            public void Dispose()
            {
                Dispose(disposing: true);
                GC.SuppressFinalize(this);
            }

            protected virtual void Dispose(bool disposing)
            {
            }
            public string Dispose(string s)
            {
                return s;
            }
        }

        private class BaseClass : IDisposable
        {
            public virtual void Dispose()
            {
                Dispose(disposing: true);
                GC.SuppressFinalize(this);
            }
            protected virtual void Dispose(bool disposing)
            {
            }
        }
        private class DerivedOverriddenDisposeController : BaseClass
        {
            public override void Dispose()
            {
                base.Dispose();
            }
        }

        private class DerivedNewDisposeController : BaseClass
        {
            public new void Dispose()
            {
                base.Dispose();
            }
        }

        private class SimplePocoController
        {
            public string Index()
            {
                return "Hello world";
            }

            public void Dispose()
            {
            }

            public void Dispose(string s)
            {
            }
        }

        private class OperatorOverloadingController : Controller
        {
            public static OperatorOverloadingController operator +(
                OperatorOverloadingController c1,
                OperatorOverloadingController c2)
            {
                return new OperatorOverloadingController();
            }
        }

        private class NoRouteAttributeOnControllerController : Controller
        {
            [HttpGet("All")]
            [HttpPost("List")]
            public void Index() { }

            [HttpHead("Change")]
            public void Edit() { }

            public void Remove() { }

            [Route("Update")]
            public void Update() { }

            [AcceptVerbs("GET", "HEAD", Route = "ListAll")]
            public void List() { }
        }

        [Route("Products")]
        private class SingleRouteAttributeController : Controller
        {
            [HttpGet("All")]
            [HttpGet("List")]
            public void Index() { }

            public void Delete() { }
        }

        [Route("Products")]
        [Route("Items")]
        private class MultipleRouteAttributeController : Controller
        {
            [HttpGet("All")]
            [HttpGet("List")]
            public void Index() { }

            public void Delete() { }
        }

        private class MixedHttpVerbsAndRouteAttributeController : Controller
        {
            // Should produce a single action constrained to GET
            [HttpGet]
            [Route("Products")]
            public void VerbAndRoute() { }

            // Should produce two actions constrained to GET,POST
            [HttpGet]
            [HttpPost]
            [Route("Products")]
            [Route("v2/Products")]
            public void MultipleVerbsAndRoutes() { }

            // Produces:
            //
            // Products - GET
            // v2/Products - GET
            // Products/Buy - POST
            [HttpGet]
            [Route("Products")]
            [Route("v2/Products")]
            [HttpPost("Products/Buy")]
            public void MultipleVerbsWithAnyWithoutTemplateAndRoutes() { }

            // Produces:
            //
            // (no route) - GET
            // Products - POST
            //
            // This is invalid, and will throw during the ADP construction phase.
            [HttpGet]
            [HttpPost("Products")]
            public void Invalid() { }
        }

        [Route("api/[controller]")]
        private class RouteAttributeOnController : Controller
        {
            [HttpGet]
            [HttpGet("id/{id?}")]
            public object Get(short? id)
            {
                return null;
            }

            [HttpDelete("{id}")]
            public object Delete(int id)
            {
                return null;
            }
        }

        // Here the constraints on the methods are acting as an IActionHttpMethodProvider and
        // not as an IRouteTemplateProvider given that there is no RouteAttribute
        // on the controller and the template for all the constraints on a method is null.
        private class ConventionallyRoutedController : Controller
        {
            public void Edit() { }

            [CustomHttpMethods("PUT", "PATCH")]
            public void Update() { }

            [HttpHead]
            [HttpDelete]
            public void Delete() { }

            [HttpPost]
            [HttpGet]
            [HttpHead]
            public void Details() { }

            [HttpGet]
            [HttpPut]
            [AcceptVerbs("GET", "POST")]
            public void List() { }
        }

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

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

            public IEnumerable<string> HttpMethods => _methods;
        }

        [Route("A")]
        [Route("B")]
        private class BaseClassWithRoutesController
        {
        }

        private class DerivedClassInheritingRoutesController : BaseClassWithRoutesController
        {
        }

        [Route("C")]
        [Route("D")]
        private class DerivedClassHidingRoutesController : BaseClassWithRoutesController
        {
        }

        private class StoreController : Controller, IActionFilter
        {
            public void OnActionExecuted(ActionExecutedContext context)
            {
                throw new NotImplementedException();
            }

            public void OnActionExecuting(ActionExecutingContext context)
            {
                throw new NotImplementedException();
            }
        }

        private class MyFilterAttribute : Attribute, IFilterMetadata
        {
        }

        [MyFilter]
        public class NoFiltersController
        {
        }

        public class ModelBinderController
        {
            [FromQuery]
            public string Bound { get; set; }

            public string Unbound { get; set; }

            public IFormFile FormFile { get; set; }

            public IActionResult PostAction([FromQuery] string fromQuery, IFormFileCollection formFileCollection, string unbound) => null;

            public IActionResult FormFilesSequences(
                IEnumerable<IFormFile> formFileEnumerable,
                ICollection<IFormFile> formFileCollection,
                IList<IFormFile> formFileIList,
                List<IFormFile> formFileList,
                IFormFile[] formFileArray) => null;

            public IActionResult PostAction1(Guid guid) => null;

            public IActionResult PostAction2([FromQuery] Guid fromQuery) => null;
        }

        public class SomeFiltersController : IAsyncActionFilter, IResultFilter
        {
            public Task OnActionExecutionAsync(
                ActionExecutingContext context,
                ActionExecutionDelegate next)
            {
                return null;
            }

            public void OnResultExecuted(ResultExecutedContext context)
            {
            }

            public void OnResultExecuting(ResultExecutingContext context)
            {
            }
        }

        private class UnsupportedFiltersController : IExceptionFilter, IAuthorizationFilter, IAsyncResourceFilter
        {
            public void OnAuthorization(AuthorizationFilterContext context)
            {
                throw new NotImplementedException();
            }

            public void OnException(ExceptionContext context)
            {
                throw new NotImplementedException();
            }

            public Task OnResourceExecutionAsync(ResourceExecutingContext context, ResourceExecutionDelegate next)
            {
                throw new NotImplementedException();
            }
        }

        [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
        {
        }

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

        private class AsyncActionController : Controller
        {
            public Task<IActionResult> GetPersonAsync() => null;

            [ActionName("GetRealAddressAsync")]
            public Task<IActionResult> GetAddressAsync() => null;
        }

        private class TestApplicationModelProvider : DefaultApplicationModelProvider
        {
            public TestApplicationModelProvider()
                : this(new MvcOptions(), new EmptyModelMetadataProvider())
            {
            }

            public TestApplicationModelProvider(
                MvcOptions options,
                IModelMetadataProvider modelMetadataProvider)
                : base(Options.Create(options), modelMetadataProvider)
            {
            }
        }
    }
}
