// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.ComponentModel.DataAnnotations;
using System.Reflection;
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;

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;
        }
    }
}
