// 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.ComponentModel.DataAnnotations;
using System.Linq;
using System.Reflection;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.JsonPatch;
using Microsoft.AspNetCore.Mvc.Abstractions;
using Microsoft.AspNetCore.Mvc.Controllers;
using Microsoft.AspNetCore.Mvc.DataAnnotations;
using Microsoft.AspNetCore.Mvc.ModelBinding.Binders;
using Microsoft.AspNetCore.Mvc.ModelBinding.Metadata;
using Microsoft.AspNetCore.Mvc.ModelBinding.Validation;
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.ModelBinding
{
    public class ParameterBinderTest
    {
        private static readonly IOptions<MvcOptions> _optionsAccessor = Options.Create(new MvcOptions());

        public static TheoryData BindModelAsyncData
        {
            get
            {
                var emptyBindingInfo = new BindingInfo();
                var bindingInfoWithName = new BindingInfo
                {
                    BinderModelName = "bindingInfoName",
                    BinderType = typeof(SimpleTypeModelBinder),
                };

                // parameterBindingInfo, metadataBinderModelName, parameterName, expectedBinderModelName
                return new TheoryData<BindingInfo, string, string, string>
                {
                    // If the parameter name is not a prefix match, it is ignored. But name is required to create a
                    // ModelBindingContext.
                    { null, null, "parameterName", string.Empty },
                    { emptyBindingInfo, null, "parameterName", string.Empty },
                    { bindingInfoWithName, null, "parameterName", "bindingInfoName" },
                    { null, "modelBinderName", "parameterName", "modelBinderName" },
                    { null, null, "parameterName", string.Empty },
                    // Parameter's BindingInfo has highest precedence
                    { bindingInfoWithName, "modelBinderName", "parameterName", "bindingInfoName" },
                };
            }
        }

        [Fact]
        public async Task BindModelAsync_EnforcesTopLevelBindRequired()
        {
            // Arrange
            var actionContext = GetControllerContext();

            var mockModelMetadata = CreateMockModelMetadata();
            mockModelMetadata.Setup(o => o.IsBindingRequired).Returns(true);
            mockModelMetadata.Setup(o => o.DisplayName).Returns("Ignored Display Name"); // Bind attribute errors are phrased in terms of the model name, not display name

            var parameterBinder = CreateParameterBinder(mockModelMetadata.Object);
            var modelBindingResult = ModelBindingResult.Failed();

            // Act
            var result = await parameterBinder.BindModelAsync(
                actionContext,
                CreateMockModelBinder(modelBindingResult),
                CreateMockValueProvider(),
                new ParameterDescriptor { Name = "myParam", ParameterType = typeof(Person) },
                mockModelMetadata.Object,
                "ignoredvalue");

            // Assert
            Assert.False(actionContext.ModelState.IsValid);
            Assert.Equal("myParam", actionContext.ModelState.Single().Key);
            Assert.Equal(
                new DefaultModelBindingMessageProvider().MissingBindRequiredValueAccessor("myParam"),
                actionContext.ModelState.Single().Value.Errors.Single().ErrorMessage);
        }

        [Fact]
        public async Task BindModelAsync_EnforcesTopLevelRequired()
        {
            // Arrange
            var actionContext = GetControllerContext();
            var mockModelMetadata = CreateMockModelMetadata();
            mockModelMetadata.Setup(o => o.IsRequired).Returns(true);
            mockModelMetadata.Setup(o => o.DisplayName).Returns("My Display Name");
            mockModelMetadata.Setup(o => o.ValidatorMetadata).Returns(new[]
            {
                new RequiredAttribute()
            });

            var validator = new DataAnnotationsModelValidator(
                new ValidationAttributeAdapterProvider(),
                new RequiredAttribute(),
                stringLocalizer: null);

            var parameterBinder = CreateParameterBinder(mockModelMetadata.Object, validator);
            var modelBindingResult = ModelBindingResult.Success(null);

            // Act
            var result = await parameterBinder.BindModelAsync(
                actionContext,
                CreateMockModelBinder(modelBindingResult),
                CreateMockValueProvider(),
                new ParameterDescriptor { Name = "myParam", ParameterType = typeof(Person) },
                mockModelMetadata.Object,
                "ignoredvalue");

            // Assert
            Assert.False(actionContext.ModelState.IsValid);
            Assert.Equal("myParam", actionContext.ModelState.Single().Key);
            Assert.Equal(
                new RequiredAttribute().FormatErrorMessage("My Display Name"),
                actionContext.ModelState.Single().Value.Errors.Single().ErrorMessage);
        }

        public static TheoryData<RequiredAttribute, ParameterDescriptor, ModelMetadata> EnforcesTopLevelRequiredDataSet
        {
            get
            {
                var attribute = new RequiredAttribute();
                var bindingInfo = new BindingInfo
                {
                    BinderModelName = string.Empty,
                };
                var parameterDescriptor = new ParameterDescriptor
                {
                    Name = string.Empty,
                    BindingInfo = bindingInfo,
                    ParameterType = typeof(Person),
                };

                var method = typeof(Person).GetMethod(nameof(Person.Equals), new[] { typeof(Person) });
                var parameter = method.GetParameters()[0]; // Equals(Person other)
                var controllerParameterDescriptor = new ControllerParameterDescriptor
                {
                    Name = string.Empty,
                    BindingInfo = bindingInfo,
                    ParameterInfo = parameter,
                    ParameterType = typeof(Person),
                };

                var provider1 = new TestModelMetadataProvider();
                provider1
                    .ForParameter(parameter)
                    .ValidationDetails(d =>
                    {
                        d.IsRequired = true;
                        d.ValidatorMetadata.Add(attribute);
                    });
                provider1
                    .ForProperty(typeof(Family), nameof(Family.Mom))
                    .ValidationDetails(d =>
                    {
                        d.IsRequired = true;
                        d.ValidatorMetadata.Add(attribute);
                    });

                var provider2 = new TestModelMetadataProvider();
                provider2
                    .ForType(typeof(Person))
                    .ValidationDetails(d =>
                    {
                        d.IsRequired = true;
                        d.ValidatorMetadata.Add(attribute);
                    });

                return new TheoryData<RequiredAttribute, ParameterDescriptor, ModelMetadata>
                {
                    { attribute, parameterDescriptor, provider1.GetMetadataForParameter(parameter) },
                    { attribute, parameterDescriptor, provider1.GetMetadataForProperty(typeof(Family), nameof(Family.Mom)) },
                    { attribute, parameterDescriptor, provider2.GetMetadataForType(typeof(Person)) },
                    { attribute, controllerParameterDescriptor, provider2.GetMetadataForType(typeof(Person)) },
                };
            }
        }

        [Theory]
        [MemberData(nameof(EnforcesTopLevelRequiredDataSet))]
        public async Task BindModelAsync_EnforcesTopLevelRequiredAndLogsSuccessfully_WithEmptyPrefix(
            RequiredAttribute attribute,
            ParameterDescriptor parameterDescriptor,
            ModelMetadata metadata)
        {
            // Arrange
            var expectedKey = string.Empty;
            var expectedFieldName = metadata.Name ?? nameof(Person);

            var actionContext = GetControllerContext();
            var validator = new DataAnnotationsModelValidator(
                new ValidationAttributeAdapterProvider(),
                attribute,
                stringLocalizer: null);

            var sink = new TestSink();
            var loggerFactory = new TestLoggerFactory(sink, enabled: true);
            var parameterBinder = CreateParameterBinder(metadata, validator, loggerFactory: loggerFactory);
            var modelBindingResult = ModelBindingResult.Success(null);

            // Act
            await parameterBinder.BindModelAsync(
                actionContext,
                CreateMockModelBinder(modelBindingResult),
                CreateMockValueProvider(),
                parameterDescriptor,
                metadata,
                "ignoredvalue");

            // Assert
            Assert.False(actionContext.ModelState.IsValid);
            var modelState = Assert.Single(actionContext.ModelState);
            Assert.Equal(expectedKey, modelState.Key);
            var error = Assert.Single(modelState.Value.Errors);
            Assert.Equal(attribute.FormatErrorMessage(expectedFieldName), error.ErrorMessage);
            Assert.Equal(4, sink.Writes.Count());
        }

        [Fact]
        public async Task BindModelAsync_EnforcesTopLevelDataAnnotationsAttribute()
        {
            // Arrange
            var actionContext = GetControllerContext();
            var mockModelMetadata = CreateMockModelMetadata();
            var validationAttribute = new RangeAttribute(1, 100);
            mockModelMetadata.Setup(o => o.DisplayName).Returns("My Display Name");
            mockModelMetadata.Setup(o => o.ValidatorMetadata).Returns(new[] {
                validationAttribute
            });

            var validator = new DataAnnotationsModelValidator(
                new ValidationAttributeAdapterProvider(),
                validationAttribute,
                stringLocalizer: null);

            var parameterBinder = CreateParameterBinder(mockModelMetadata.Object, validator);
            var modelBindingResult = ModelBindingResult.Success(123);

            // Act
            var result = await parameterBinder.BindModelAsync(
                actionContext,
                CreateMockModelBinder(modelBindingResult),
                CreateMockValueProvider(),
                new ParameterDescriptor { Name = "myParam", ParameterType = typeof(Person) },
                mockModelMetadata.Object,
                50); // This value is ignored, because test explicitly set the ModelBindingResult

            // Assert
            Assert.False(actionContext.ModelState.IsValid);
            Assert.Equal("myParam", actionContext.ModelState.Single().Key);
            Assert.Equal(
                validationAttribute.FormatErrorMessage("My Display Name"),
                actionContext.ModelState.Single().Value.Errors.Single().ErrorMessage);
        }

        [Fact]
        public async Task BindModelAsync_SupportsIObjectModelValidatorForBackCompat()
        {
            // Arrange
            var actionContext = GetControllerContext();

            var mockValidator = new Mock<IObjectModelValidator>(MockBehavior.Strict);
            mockValidator
                .Setup(o => o.Validate(
                    It.IsAny<ActionContext>(),
                    It.IsAny<ValidationStateDictionary>(),
                    It.IsAny<string>(),
                    It.IsAny<object>()))
                .Callback((ActionContext context, ValidationStateDictionary validationState, string prefix, object model) =>
                {
                    context.ModelState.AddModelError(prefix, "Test validation message");
                });

            var modelMetadata = CreateMockModelMetadata().Object;
            var parameterBinder = CreateBackCompatParameterBinder(
                modelMetadata,
                mockValidator.Object);
            var modelBindingResult = ModelBindingResult.Success(123);

            // Act
            var result = await parameterBinder.BindModelAsync(
                actionContext,
                CreateMockModelBinder(modelBindingResult),
                CreateMockValueProvider(),
                new ParameterDescriptor { Name = "myParam", ParameterType = typeof(Person) },
                modelMetadata,
                "ignored");

            // Assert
            Assert.False(actionContext.ModelState.IsValid);
            Assert.Equal("myParam", actionContext.ModelState.Single().Key);
            Assert.Equal(
                "Test validation message",
                actionContext.ModelState.Single().Value.Errors.Single().ErrorMessage);
        }

        [Fact]
        [ReplaceCulture]
        public async Task BindModelAsync_ForParameter_UsesValidationFromActualModel_WhenDerivedModelIsSet()
        {
            // Arrange
            var method = GetType().GetMethod(nameof(TestMethodWithoutAttributes), BindingFlags.NonPublic | BindingFlags.Instance);
            var parameter = method.GetParameters()[0];
            var parameterDescriptor = new ControllerParameterDescriptor
            {
                ParameterInfo = parameter,
                Name = parameter.Name,
            };

            var actionContext = GetControllerContext();
            var modelMetadataProvider = new TestModelMetadataProvider();

            var model = new DerivedPerson();
            var modelBindingResult = ModelBindingResult.Success(model);

            var parameterBinder = new ParameterBinder(
                modelMetadataProvider,
                Mock.Of<IModelBinderFactory>(),
                new DefaultObjectValidator(
                    modelMetadataProvider,
                    new[] { TestModelValidatorProvider.CreateDefaultProvider() },
                    new MvcOptions()),
                _optionsAccessor,
                NullLoggerFactory.Instance);

            var modelMetadata = modelMetadataProvider.GetMetadataForParameter(parameter);
            var modelBinder = CreateMockModelBinder(modelBindingResult);

            // Act
            var result = await parameterBinder.BindModelAsync(
                actionContext,
                modelBinder,
                CreateMockValueProvider(),
                parameterDescriptor,
                modelMetadata,
                value: null);

            // Assert
            Assert.True(result.IsModelSet);
            Assert.Same(model, result.Model);

            Assert.False(actionContext.ModelState.IsValid);
            Assert.Collection(
                actionContext.ModelState,
                kvp =>
                {
                    Assert.Equal($"{parameter.Name}.{nameof(DerivedPerson.DerivedProperty)}", kvp.Key);
                    var error = Assert.Single(kvp.Value.Errors);
                    Assert.Equal("The DerivedProperty field is required.", error.ErrorMessage);
                });
        }

        [Fact]
        public async Task BindModelAsync_ForParameter_UsesValidationFromParameter_WhenDerivedModelIsSet()
        {
            // Arrange
            var method = GetType().GetMethod(nameof(TestMethodWithAttributes), BindingFlags.NonPublic | BindingFlags.Instance);
            var parameter = method.GetParameters()[0];
            var parameterDescriptor = new ControllerParameterDescriptor
            {
                ParameterInfo = parameter,
                Name = parameter.Name,
            };

            var actionContext = GetControllerContext();
            var modelMetadataProvider = new TestModelMetadataProvider();

            var model = new DerivedPerson { DerivedProperty = "SomeValue" };
            var modelBindingResult = ModelBindingResult.Success(model);

            var parameterBinder = new ParameterBinder(
                modelMetadataProvider,
                Mock.Of<IModelBinderFactory>(),
                new DefaultObjectValidator(
                    modelMetadataProvider,
                    new[] { TestModelValidatorProvider.CreateDefaultProvider() },
                    new MvcOptions()),
                _optionsAccessor,
                NullLoggerFactory.Instance);

            var modelMetadata = modelMetadataProvider.GetMetadataForParameter(parameter);
            var modelBinder = CreateMockModelBinder(modelBindingResult);

            // Act
            var result = await parameterBinder.BindModelAsync(
                actionContext,
                modelBinder,
                CreateMockValueProvider(),
                parameterDescriptor,
                modelMetadata,
                value: null);

            // Assert
            Assert.True(result.IsModelSet);
            Assert.Same(model, result.Model);

            Assert.False(actionContext.ModelState.IsValid);
            Assert.Collection(
                actionContext.ModelState,
                kvp =>
                {
                    Assert.Equal(parameter.Name, kvp.Key);
                    var error = Assert.Single(kvp.Value.Errors);
                    Assert.Equal("Always Invalid", error.ErrorMessage);
                });
        }

        [Fact]
        [ReplaceCulture]
        public async Task BindModelAsync_ForProperty_UsesValidationFromActualModel_WhenDerivedModelIsSet()
        {
            // Arrange
            var property = typeof(TestController).GetProperty(nameof(TestController.Model));
            var parameterDescriptor = new ControllerBoundPropertyDescriptor
            {
                PropertyInfo = property,
                Name = property.Name,
            };

            var actionContext = GetControllerContext();
            var modelMetadataProvider = new TestModelMetadataProvider();

            var model = new DerivedModel();
            var modelBindingResult = ModelBindingResult.Success(model);

            var parameterBinder = new ParameterBinder(
                modelMetadataProvider,
                Mock.Of<IModelBinderFactory>(),
                new DefaultObjectValidator(
                    modelMetadataProvider,
                    new[] { TestModelValidatorProvider.CreateDefaultProvider() },
                    new MvcOptions()),
                _optionsAccessor,
                NullLoggerFactory.Instance);

            var modelMetadata = modelMetadataProvider.GetMetadataForProperty(property.DeclaringType, property.Name);
            var modelBinder = CreateMockModelBinder(modelBindingResult);

            // Act
            var result = await parameterBinder.BindModelAsync(
                actionContext,
                modelBinder,
                CreateMockValueProvider(),
                parameterDescriptor,
                modelMetadata,
                value: null);

            // Assert
            Assert.True(result.IsModelSet);
            Assert.Same(model, result.Model);

            Assert.False(actionContext.ModelState.IsValid);
            Assert.Collection(
                actionContext.ModelState,
                kvp =>
                {
                    Assert.Equal($"{property.Name}.{nameof(DerivedPerson.DerivedProperty)}", kvp.Key);
                    var error = Assert.Single(kvp.Value.Errors);
                    Assert.Equal("The DerivedProperty field is required.", error.ErrorMessage);
                });
        }

        [Fact]
        public async Task BindModelAsync_ForProperty_UsesValidationOnProperty_WhenDerivedModelIsSet()
        {
            // Arrange
            var property = typeof(TestControllerWithValidatedProperties).GetProperty(nameof(TestControllerWithValidatedProperties.Model));
            var parameterDescriptor = new ControllerBoundPropertyDescriptor
            {
                PropertyInfo = property,
                Name = property.Name,
            };

            var actionContext = GetControllerContext();
            var modelMetadataProvider = new TestModelMetadataProvider();

            var model = new DerivedModel { DerivedProperty = "some value" };
            var modelBindingResult = ModelBindingResult.Success(model);

            var parameterBinder = new ParameterBinder(
                modelMetadataProvider,
                Mock.Of<IModelBinderFactory>(),
                new DefaultObjectValidator(
                    modelMetadataProvider,
                    new[] { TestModelValidatorProvider.CreateDefaultProvider() },
                    new MvcOptions()),
                _optionsAccessor,
                NullLoggerFactory.Instance);

            var modelMetadata = modelMetadataProvider.GetMetadataForProperty(property.DeclaringType, property.Name);
            var modelBinder = CreateMockModelBinder(modelBindingResult);

            // Act
            var result = await parameterBinder.BindModelAsync(
                actionContext,
                modelBinder,
                CreateMockValueProvider(),
                parameterDescriptor,
                modelMetadata,
                value: null);

            // Assert
            Assert.True(result.IsModelSet);
            Assert.Same(model, result.Model);

            Assert.False(actionContext.ModelState.IsValid);
            Assert.Collection(
                actionContext.ModelState,
                kvp =>
                {
                    Assert.Equal($"{property.Name}", kvp.Key);
                    var error = Assert.Single(kvp.Value.Errors);
                    Assert.Equal("Always Invalid", error.ErrorMessage);
                });
        }

        // Regression test 1 for aspnet/Mvc#7963. ModelState should never be valid.
        [Fact]
        public async Task BindModelAsync_ForOverlappingParametersWithSuppressions_InValid_WithValidSecondParameter()
        {
            // Arrange
            var parameterDescriptor = new ParameterDescriptor
            {
                Name = "patchDocument",
                ParameterType = typeof(IJsonPatchDocument),
            };

            var actionContext = GetControllerContext();
            var modelState = actionContext.ModelState;

            // First ModelState key is not empty to match SimpleTypeModelBinder.
            modelState.SetModelValue("id", "notAGuid", "notAGuid");
            modelState.AddModelError("id", "This is not valid.");

            var modelMetadataProvider = new TestModelMetadataProvider();
            modelMetadataProvider.ForType<IJsonPatchDocument>().ValidationDetails(v => v.ValidateChildren = false);
            var modelMetadata = modelMetadataProvider.GetMetadataForType(typeof(IJsonPatchDocument));

            var parameterBinder = new ParameterBinder(
                modelMetadataProvider,
                Mock.Of<IModelBinderFactory>(),
                new DefaultObjectValidator(
                    modelMetadataProvider,
                    new[] { TestModelValidatorProvider.CreateDefaultProvider() },
                    new MvcOptions()),
                _optionsAccessor,
                NullLoggerFactory.Instance);

            // BodyModelBinder does not update ModelState in success case.
            var modelBindingResult = ModelBindingResult.Success(new JsonPatchDocument());
            var modelBinder = CreateMockModelBinder(modelBindingResult);

            // Act
            var result = await parameterBinder.BindModelAsync(
                actionContext,
                modelBinder,
                new SimpleValueProvider(),
                parameterDescriptor,
                modelMetadata,
                value: null);

            // Assert
            Assert.True(result.IsModelSet);
            Assert.False(modelState.IsValid);
            Assert.Collection(
                modelState,
                kvp =>
                {
                    Assert.Equal("id", kvp.Key);
                    Assert.Equal(ModelValidationState.Invalid, kvp.Value.ValidationState);
                    var error = Assert.Single(kvp.Value.Errors);
                    Assert.Equal("This is not valid.", error.ErrorMessage);
                });
        }

        // Regression test 2 for aspnet/Mvc#7963. ModelState should never be valid.
        [Fact]
        public async Task BindModelAsync_ForOverlappingParametersWithSuppressions_InValid_WithInValidSecondParameter()
        {
            // Arrange
            var parameterDescriptor = new ParameterDescriptor
            {
                Name = "patchDocument",
                ParameterType = typeof(IJsonPatchDocument),
            };

            var actionContext = GetControllerContext();
            var modelState = actionContext.ModelState;

            // First ModelState key is not empty to match SimpleTypeModelBinder.
            modelState.SetModelValue("id", "notAGuid", "notAGuid");
            modelState.AddModelError("id", "This is not valid.");

            // Second ModelState key is empty to match BodyModelBinder.
            modelState.AddModelError(string.Empty, "This is also not valid.");

            var modelMetadataProvider = new TestModelMetadataProvider();
            modelMetadataProvider.ForType<IJsonPatchDocument>().ValidationDetails(v => v.ValidateChildren = false);
            var modelMetadata = modelMetadataProvider.GetMetadataForType(typeof(IJsonPatchDocument));

            var parameterBinder = new ParameterBinder(
                modelMetadataProvider,
                Mock.Of<IModelBinderFactory>(),
                new DefaultObjectValidator(
                    modelMetadataProvider,
                    new[] { TestModelValidatorProvider.CreateDefaultProvider() },
                    new MvcOptions()),
                _optionsAccessor,
                NullLoggerFactory.Instance);

            var modelBindingResult = ModelBindingResult.Failed();
            var modelBinder = CreateMockModelBinder(modelBindingResult);

            // Act
            var result = await parameterBinder.BindModelAsync(
                actionContext,
                modelBinder,
                new SimpleValueProvider(),
                parameterDescriptor,
                modelMetadata,
                value: null);

            // Assert
            Assert.False(result.IsModelSet);
            Assert.False(modelState.IsValid);
            Assert.Collection(
                modelState,
                kvp =>
                {
                    Assert.Empty(kvp.Key);
                    Assert.Equal(ModelValidationState.Invalid, kvp.Value.ValidationState);
                    var error = Assert.Single(kvp.Value.Errors);
                    Assert.Equal("This is also not valid.", error.ErrorMessage);
                },
                kvp =>
                {
                    Assert.Equal("id", kvp.Key);
                    Assert.Equal(ModelValidationState.Invalid, kvp.Value.ValidationState);
                    var error = Assert.Single(kvp.Value.Errors);
                    Assert.Equal("This is not valid.", error.ErrorMessage);
                });
        }

        // Regression test for aspnet/Mvc#8078. Later parameter should not mark entry as valid.
        [Fact]
        public async Task BindModelAsync_ForOverlappingParameters_InValid_WithInValidFirstParameterAndSecondNull()
        {
            // Arrange
            var parameterDescriptor = new ParameterDescriptor
            {
                BindingInfo = new BindingInfo
                {
                    BinderModelName = "id",
                },
                Name = "identifier",
                ParameterType = typeof(string),
            };

            var actionContext = GetControllerContext();
            var modelState = actionContext.ModelState;

            // Mimic ModelStateEntry when first parameter is [FromRoute] int id and request URI is /api/values/notAnInt
            modelState.SetModelValue("id", "notAnInt", "notAnInt");
            modelState.AddModelError("id", "This is not valid.");

            var modelMetadataProvider = new TestModelMetadataProvider();
            var modelMetadata = modelMetadataProvider.GetMetadataForType(typeof(string));
            var parameterBinder = new ParameterBinder(
                modelMetadataProvider,
                Mock.Of<IModelBinderFactory>(),
                new DefaultObjectValidator(
                    modelMetadataProvider,
                    new[] { TestModelValidatorProvider.CreateDefaultProvider() },
                    new MvcOptions()),
                _optionsAccessor,
                NullLoggerFactory.Instance);

            // Mimic result when second parameter is [FromQuery(Name = "id")] string identifier and query is ?id
            var modelBindingResult = ModelBindingResult.Success(null);
            var modelBinder = CreateMockModelBinder(modelBindingResult);

            // Act
            var result = await parameterBinder.BindModelAsync(
                actionContext,
                modelBinder,
                new SimpleValueProvider(),
                parameterDescriptor,
                modelMetadata,
                value: null);

            // Assert
            Assert.True(result.IsModelSet);
            Assert.False(modelState.IsValid);
            var keyValuePair = Assert.Single(modelState);
            Assert.Equal("id", keyValuePair.Key);
            Assert.Equal(ModelValidationState.Invalid, keyValuePair.Value.ValidationState);
        }

        private static ControllerContext GetControllerContext()
        {
            var services = new ServiceCollection();
            services.AddSingleton<ILoggerFactory>(NullLoggerFactory.Instance);

            return new ControllerContext()
            {
                HttpContext = new DefaultHttpContext()
                {
                    RequestServices = services.BuildServiceProvider()
                }
            };
        }

        private static Mock<FakeModelMetadata> CreateMockModelMetadata()
        {
            var mockModelMetadata = new Mock<FakeModelMetadata>();
            mockModelMetadata
                .Setup(o => o.ModelBindingMessageProvider)
                .Returns(new DefaultModelBindingMessageProvider());
            return mockModelMetadata;
        }

        private static IModelBinder CreateMockModelBinder(ModelBindingResult modelBinderResult)
        {
            var mockBinder = new Mock<IModelBinder>(MockBehavior.Strict);
            mockBinder
                .Setup(o => o.BindModelAsync(It.IsAny<ModelBindingContext>()))
                .Returns<ModelBindingContext>(context =>
                {
                    context.Result = modelBinderResult;
                    return Task.CompletedTask;
                });
            return mockBinder.Object;
        }

        private static ParameterBinder CreateParameterBinder(
            ModelMetadata modelMetadata,
            IModelValidator validator = null,
            IOptions<MvcOptions> optionsAccessor = null,
            ILoggerFactory loggerFactory = null)
        {
            var mockModelMetadataProvider = new Mock<IModelMetadataProvider>(MockBehavior.Strict);
            mockModelMetadataProvider
                .Setup(o => o.GetMetadataForType(typeof(Person)))
                .Returns(modelMetadata);

            var mockModelBinderFactory = new Mock<IModelBinderFactory>(MockBehavior.Strict);
            optionsAccessor = optionsAccessor ?? _optionsAccessor;
            return new ParameterBinder(
                mockModelMetadataProvider.Object,
                mockModelBinderFactory.Object,
                new DefaultObjectValidator(
                    mockModelMetadataProvider.Object,
                    new[] { GetModelValidatorProvider(validator) },
                    new MvcOptions()),
                optionsAccessor,
                loggerFactory ?? NullLoggerFactory.Instance);
        }

        private static IModelValidatorProvider GetModelValidatorProvider(IModelValidator validator = null)
        {
            if (validator == null)
            {
                validator = Mock.Of<IModelValidator>();
            }

            var validatorProvider = new Mock<IModelValidatorProvider>();
            validatorProvider
                .Setup(p => p.CreateValidators(It.IsAny<ModelValidatorProviderContext>()))
                .Callback<ModelValidatorProviderContext>(context =>
                {
                    foreach (var result in context.Results)
                    {
                        result.Validator = validator;
                        result.IsReusable = true;
                    }
                });
            return validatorProvider.Object;
        }

        private static ParameterBinder CreateBackCompatParameterBinder(
            ModelMetadata modelMetadata,
            IObjectModelValidator validator)
        {
            var mockModelMetadataProvider = new Mock<IModelMetadataProvider>(MockBehavior.Strict);
            mockModelMetadataProvider
                .Setup(o => o.GetMetadataForType(typeof(Person)))
                .Returns(modelMetadata);

            var mockModelBinderFactory = new Mock<IModelBinderFactory>(MockBehavior.Strict);
            return new ParameterBinder(
                mockModelMetadataProvider.Object,
                mockModelBinderFactory.Object,
                validator,
                _optionsAccessor,
                NullLoggerFactory.Instance);
        }

        private static IValueProvider CreateMockValueProvider()
        {
            var mockValueProvider = new Mock<IValueProvider>(MockBehavior.Strict);
            mockValueProvider
                .Setup(o => o.ContainsPrefix(It.IsAny<string>()))
                .Returns(true);
            return mockValueProvider.Object;
        }

        private class Person : IEquatable<Person>, IEquatable<object>
        {
            public string Name { get; set; }

            public bool Equals(Person other)
            {
                return other != null && string.Equals(Name, other.Name, StringComparison.Ordinal);
            }

            bool IEquatable<object>.Equals(object obj)
            {
                return Equals(obj as Person);
            }
        }

        private class Family
        {
            public Person Dad { get; set; }

            public Person Mom { get; set; }

            public IList<Person> Kids { get; } = new List<Person>();
        }

        private class DerivedPerson : Person
        {
            [Required]
            public string DerivedProperty { get; set; }
        }

        public abstract class FakeModelMetadata : ModelMetadata
        {
            public FakeModelMetadata()
                : base(ModelMetadataIdentity.ForType(typeof(string)))
            {
            }
        }

        private void TestMethodWithoutAttributes(Person person) { }

        private void TestMethodWithAttributes([Required][AlwaysInvalid] Person person) { }

        private class TestController
        {
            public BaseModel Model { get; set; }
        }

        private class TestControllerWithValidatedProperties
        {
            [AlwaysInvalid]
            [Required]
            public BaseModel Model { get; set; }
        }

        private class BaseModel
        {
        }

        private class DerivedModel
        {
            [Required]
            public string DerivedProperty { get; set; }
        }

        private class AlwaysInvalidAttribute : ValidationAttribute
        {
            public AlwaysInvalidAttribute()
            {
                ErrorMessage = "Always Invalid";
            }

            public override bool IsValid(object value)
            {
                return false;
            }
        }
    }
}
