// 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.Text;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.Infrastructure;
using Microsoft.AspNetCore.Mvc.ModelBinding.Metadata;
using Microsoft.AspNetCore.InternalTesting;
using Microsoft.Extensions.DependencyInjection;
using Moq;

namespace Microsoft.AspNetCore.Mvc.ModelBinding.Validation;

public class DefaultObjectValidatorTests
{
    private readonly MvcOptions _options = new MvcOptions();

    private ModelMetadataProvider MetadataProvider { get; } = TestModelMetadataProvider.CreateDefaultProvider();

    [Fact]
    public void Validate_SimpleValueType_Valid_WithPrefix()
    {
        // Arrange
        var actionContext = new ActionContext();
        var modelState = actionContext.ModelState;
        var validationState = new ValidationStateDictionary();

        var validator = CreateValidator();

        var model = (object)15;

        modelState.SetModelValue("parameter", "15", "15");
        validationState.Add(model, new ValidationStateEntry() { Key = "parameter" });

        // Act
        validator.Validate(actionContext, validationState, "parameter", model);

        // Assert
        AssertKeysEqual(modelState, "parameter");

        var entry = modelState["parameter"];
        Assert.Equal(ModelValidationState.Valid, entry.ValidationState);
        Assert.Empty(entry.Errors);
    }

    [Fact]
    public void Validate_SimpleReferenceType_Valid_WithPrefix()
    {
        // Arrange
        var actionContext = new ActionContext();
        var modelState = actionContext.ModelState;
        var validationState = new ValidationStateDictionary();

        var validator = CreateValidator();

        var model = (object)"test";

        modelState.SetModelValue("parameter", "test", "test");
        validationState.Add(model, new ValidationStateEntry() { Key = "parameter" });

        // Act
        validator.Validate(actionContext, validationState, "parameter", model);

        // Assert
        Assert.True(modelState.IsValid);
        AssertKeysEqual(modelState, "parameter");

        var entry = modelState["parameter"];
        Assert.Equal(ModelValidationState.Valid, entry.ValidationState);
        Assert.Empty(entry.Errors);
    }

    [Fact]
    public void Validate_SimpleType_MaxErrorsReached()
    {
        // Arrange
        var actionContext = new ActionContext();
        var modelState = actionContext.ModelState;
        var validationState = new ValidationStateDictionary();

        var validator = CreateValidator();

        var model = (object)"test";

        modelState.MaxAllowedErrors = 1;
        modelState.AddModelError("other.Model", "error");
        modelState.SetModelValue("parameter", "test", "test");
        validationState.Add(model, new ValidationStateEntry() { Key = "parameter" });

        // Act
        validator.Validate(actionContext, validationState, "parameter", model);

        // Assert
        Assert.False(modelState.IsValid);
        AssertKeysEqual(modelState, string.Empty, "parameter");

        var entry = modelState["parameter"];
        Assert.Equal(ModelValidationState.Skipped, entry.ValidationState);
        Assert.Empty(entry.Errors);
    }

    [Fact]
    public void Validate_SimpleType_SuppressValidation()
    {
        // Arrange
        var actionContext = new ActionContext();
        var modelState = actionContext.ModelState;
        var validationState = new ValidationStateDictionary();

        var validator = CreateValidator();

        var model = (object)"test";

        modelState.SetModelValue("parameter", "test", "test");
        validationState.Add(model, new ValidationStateEntry() { Key = "parameter", SuppressValidation = true });

        // Act
        validator.Validate(actionContext, validationState, "parameter", model);

        // Assert
        Assert.True(modelState.IsValid);
        AssertKeysEqual(modelState, "parameter");

        var entry = modelState["parameter"];
        Assert.Equal(ModelValidationState.Skipped, entry.ValidationState);
        Assert.Empty(entry.Errors);
    }

    // More like how product code does suppressions than Validate_SimpleType_SuppressValidation()
    [Fact]
    public void Validate_SimpleType_SuppressValidationWithNullKey()
    {
        // Arrange
        var actionContext = new ActionContext();
        var modelState = actionContext.ModelState;
        var validator = CreateValidator();
        var model = "test";
        var validationState = new ValidationStateDictionary
            {
                { model, new ValidationStateEntry { SuppressValidation = true } }
            };

        // Act
        validator.Validate(actionContext, validationState, "parameter", model);

        // Assert
        Assert.True(modelState.IsValid);
        Assert.Empty(modelState);
    }

    [Fact]
    public void Validate_ComplexValueType_Valid()
    {
        // Arrange
        var actionContext = new ActionContext();
        var modelState = actionContext.ModelState;
        var validationState = new ValidationStateDictionary();

        var validator = CreateValidator();

        var model = (object)new ValueType() { Reference = "ref", Value = 256 };

        modelState.SetModelValue("parameter.Reference", "ref", "ref");
        modelState.SetModelValue("parameter.Value", "256", "256");
        validationState.Add(model, new ValidationStateEntry() { Key = "parameter" });

        // Act
        validator.Validate(actionContext, validationState, "parameter", model);

        // Assert
        Assert.True(modelState.IsValid);
        AssertKeysEqual(modelState, "parameter.Reference", "parameter.Value");

        var entry = modelState["parameter.Reference"];
        Assert.Equal(ModelValidationState.Valid, entry.ValidationState);
        Assert.Empty(entry.Errors);

        entry = modelState["parameter.Value"];
        Assert.Equal(ModelValidationState.Valid, entry.ValidationState);
        Assert.Empty(entry.Errors);
    }

    [Fact]
    public void Validate_ComplexReferenceType_Valid()
    {
        // Arrange
        var actionContext = new ActionContext();
        var modelState = actionContext.ModelState;
        var validationState = new ValidationStateDictionary();

        var validator = CreateValidator();

        var model = (object)new ReferenceType() { Reference = "ref", Value = 256 };

        modelState.SetModelValue("parameter.Reference", "ref", "ref");
        modelState.SetModelValue("parameter.Value", "256", "256");
        validationState.Add(model, new ValidationStateEntry() { Key = "parameter" });

        // Act
        validator.Validate(actionContext, validationState, "parameter", model);

        // Assert
        Assert.True(modelState.IsValid);
        AssertKeysEqual(modelState, "parameter.Reference", "parameter.Value");

        var entry = modelState["parameter.Reference"];
        Assert.Equal(ModelValidationState.Valid, entry.ValidationState);
        Assert.Empty(entry.Errors);

        entry = modelState["parameter.Value"];
        Assert.Equal(ModelValidationState.Valid, entry.ValidationState);
        Assert.Empty(entry.Errors);
    }

    [Fact]
    public void Validate_ComplexReferenceType_Invalid()
    {
        // Arrange
        var actionContext = new ActionContext();
        var modelState = actionContext.ModelState;
        var validationState = new ValidationStateDictionary();

        var validator = CreateValidator();

        var model = (object)new Person();

        validationState.Add(model, new ValidationStateEntry() { Key = string.Empty });

        // Act
        validator.Validate(actionContext, validationState, string.Empty, model);

        // Assert
        Assert.False(modelState.IsValid);
        AssertKeysEqual(modelState, "Name", "Profession");

        var entry = modelState["Name"];
        Assert.Equal(ModelValidationState.Invalid, entry.ValidationState);
        var error = Assert.Single(entry.Errors);
        Assert.Equal(ValidationAttributeUtil.GetRequiredErrorMessage("Name"), error.ErrorMessage);

        entry = modelState["Profession"];
        Assert.Equal(ModelValidationState.Invalid, entry.ValidationState);
        error = Assert.Single(entry.Errors);
        Assert.Equal(ValidationAttributeUtil.GetRequiredErrorMessage("Profession"), error.ErrorMessage);
    }

    [Fact]
    public void Validate_ComplexType_SuppressValidation()
    {
        // Arrange
        var actionContext = new ActionContext();
        var modelState = actionContext.ModelState;
        var validationState = new ValidationStateDictionary();

        var validator = CreateValidator();

        var model = new Person2()
        {
            Name = "Billy",
            Address = new Address { Street = "GreaterThan5Characters" }
        };

        modelState.SetModelValue("person.Name", "Billy", "Billy");
        modelState.SetModelValue("person.Address.Street", "GreaterThan5Characters", "GreaterThan5Characters");
        validationState.Add(model, new ValidationStateEntry() { Key = "person" });
        validationState.Add(model.Address, new ValidationStateEntry()
        {
            Key = "person.Address",
            SuppressValidation = true
        });

        // Act
        validator.Validate(actionContext, validationState, "person", model);

        // Assert
        Assert.True(modelState.IsValid);
        AssertKeysEqual(modelState, "person.Name", "person.Address.Street");

        var entry = modelState["person.Name"];
        Assert.Equal(ModelValidationState.Valid, entry.ValidationState);
        Assert.Empty(entry.Errors);

        entry = modelState["person.Address.Street"];
        Assert.Equal(ModelValidationState.Skipped, entry.ValidationState);
        Assert.Empty(entry.Errors);
    }

    [Fact]
    [ReplaceCulture]
    public void Validate_ComplexReferenceType_Invalid_MultipleErrorsOnProperty()
    {
        // Arrange
        var actionContext = new ActionContext();
        var modelState = actionContext.ModelState;
        var validationState = new ValidationStateDictionary();

        var validator = CreateValidator();

        var model = (object)new Address() { Street = "Microsoft Way" };

        modelState.SetModelValue("parameter.Street", "Microsoft Way", "Microsoft Way");
        validationState.Add(model, new ValidationStateEntry() { Key = "parameter" });

        // Act
        validator.Validate(actionContext, validationState, "parameter", model);

        // Assert
        Assert.False(modelState.IsValid);
        AssertKeysEqual(modelState, "parameter.Street");

        var entry = modelState["parameter.Street"];
        Assert.Equal(ModelValidationState.Invalid, entry.ValidationState);

        Assert.Equal(2, entry.Errors.Count);
        var errorMessages = entry.Errors.Select(e => e.ErrorMessage);
        Assert.Contains(ValidationAttributeUtil.GetStringLengthErrorMessage(null, 5, "Street"), errorMessages);
        Assert.Contains(ValidationAttributeUtil.GetRegExErrorMessage("hehehe", "Street"), errorMessages);
    }

    [Fact]
    [ReplaceCulture]
    public void Validate_ComplexReferenceType_Invalid_MultipleErrorsOnProperty_EmptyPrefix()
    {
        // Arrange
        var actionContext = new ActionContext();
        var modelState = actionContext.ModelState;
        var validationState = new ValidationStateDictionary();

        var validator = CreateValidator();

        var model = (object)new Address() { Street = "Microsoft Way" };

        modelState.SetModelValue("Street", "Microsoft Way", "Microsoft Way");
        validationState.Add(model, new ValidationStateEntry() { Key = string.Empty });

        // Act
        validator.Validate(actionContext, validationState, string.Empty, model);

        // Assert
        Assert.False(modelState.IsValid);
        AssertKeysEqual(modelState, "Street");

        var entry = modelState["Street"];
        Assert.Equal(ModelValidationState.Invalid, entry.ValidationState);

        Assert.Equal(2, entry.Errors.Count);
        var errorMessages = entry.Errors.Select(e => e.ErrorMessage);
        Assert.Contains(ValidationAttributeUtil.GetStringLengthErrorMessage(null, 5, "Street"), errorMessages);
        Assert.Contains(ValidationAttributeUtil.GetRegExErrorMessage("hehehe", "Street"), errorMessages);
    }

    [Fact]
    [ReplaceCulture]
    public void Validate_NestedComplexReferenceType_Invalid()
    {
        // Arrange
        var actionContext = new ActionContext();
        var modelState = actionContext.ModelState;
        var validationState = new ValidationStateDictionary();

        var validator = CreateValidator();

        var model = (object)new Person() { Name = "Rick", Friend = new Person() };

        modelState.SetModelValue("Name", "Rick", "Rick");
        validationState.Add(model, new ValidationStateEntry() { Key = string.Empty });

        // Act
        validator.Validate(actionContext, validationState, string.Empty, model);

        // Assert
        Assert.False(modelState.IsValid);
        AssertKeysEqual(modelState, "Name", "Profession", "Friend.Name", "Friend.Profession");

        var entry = modelState["Name"];
        Assert.Equal(ModelValidationState.Valid, entry.ValidationState);

        entry = modelState["Profession"];
        Assert.Equal(ModelValidationState.Invalid, entry.ValidationState);
        var error = Assert.Single(entry.Errors);
        Assert.Equal(ValidationAttributeUtil.GetRequiredErrorMessage("Profession"), error.ErrorMessage);

        entry = modelState["Friend.Name"];
        Assert.Equal(ModelValidationState.Invalid, entry.ValidationState);
        error = Assert.Single(entry.Errors);
        Assert.Equal(ValidationAttributeUtil.GetRequiredErrorMessage("Name"), error.ErrorMessage);

        entry = modelState["Friend.Profession"];
        Assert.Equal(ModelValidationState.Invalid, entry.ValidationState);
        error = Assert.Single(entry.Errors);
        Assert.Equal(ValidationAttributeUtil.GetRequiredErrorMessage("Profession"), error.ErrorMessage);
    }

    [Fact]
    public void Validate_ComplexType_MultipleInvalidProperties()
    {
        // Arrange
        var actionContext = new ActionContext();
        var modelState = actionContext.ModelState;
        var model = new InvalidProperties();
        var validationState = new ValidationStateDictionary
            {
                { model, new ValidationStateEntry() },
            };

        var validator = CreateValidator();

        // Act
        validator.Validate(actionContext, validationState, prefix: null, model: model);

        // Assert
        Assert.Collection(
            modelState,
            state =>
            {
                Assert.Equal("FirstName", state.Key);
                var error = Assert.Single(state.Value.Errors);
                Assert.Equal("User object lacks some data.", error.ErrorMessage);
            },
            state =>
            {
                Assert.Equal("Address.City", state.Key);
                var error = Assert.Single(state.Value.Errors);
                Assert.Equal("User object lacks some data.", error.ErrorMessage);
            });
    }

    [Fact]
    public void Validate_ComplexType_MultipleInvalidProperties_WithPrefix()
    {
        // Arrange
        var actionContext = new ActionContext();
        var modelState = actionContext.ModelState;
        var model = new InvalidProperties();
        var validationState = new ValidationStateDictionary
            {
                { model, new ValidationStateEntry { Key = "invalidProperties" } },
            };

        var validator = CreateValidator();

        // Act
        validator.Validate(actionContext, validationState, prefix: "invalidProperties", model: model);

        // Assert
        Assert.Collection(
            modelState,
            state =>
            {
                Assert.Equal("invalidProperties.FirstName", state.Key);
                var error = Assert.Single(state.Value.Errors);
                Assert.Equal("User object lacks some data.", error.ErrorMessage);
            },
            state =>
            {
                Assert.Equal("invalidProperties.Address.City", state.Key);
                var error = Assert.Single(state.Value.Errors);
                Assert.Equal("User object lacks some data.", error.ErrorMessage);
            });
    }

    // IValidatableObject is significant because the validators are on the object
    // itself, not just the properties.
    [Fact]
    [ReplaceCulture]
    public void Validate_ComplexType_IValidatableObject_Invalid()
    {
        // Arrange
        var actionContext = new ActionContext();
        var modelState = actionContext.ModelState;
        var validationState = new ValidationStateDictionary();

        var validator = CreateValidator();

        var model = (object)new ValidatableModel();

        modelState.SetModelValue("parameter", "model", "model");

        validationState.Add(model, new ValidationStateEntry() { Key = "parameter" });

        // Act
        validator.Validate(actionContext, validationState, "parameter", model);

        // Assert
        Assert.False(modelState.IsValid);
        AssertKeysEqual(modelState, "parameter", "parameter.Property1", "parameter.Property2", "parameter.Property3");

        var entry = modelState["parameter"];
        Assert.Equal(ModelValidationState.Invalid, entry.ValidationState);
        var error = Assert.Single(entry.Errors);
        Assert.Equal("Error1 about '' (display: 'ValidatableModel').", error.ErrorMessage);

        entry = modelState["parameter.Property1"];
        Assert.Equal(ModelValidationState.Invalid, entry.ValidationState);
        error = Assert.Single(entry.Errors);
        Assert.Equal("Error2", error.ErrorMessage);

        entry = modelState["parameter.Property2"];
        Assert.Equal(ModelValidationState.Invalid, entry.ValidationState);
        error = Assert.Single(entry.Errors);
        Assert.Equal("Error3", error.ErrorMessage);

        entry = modelState["parameter.Property3"];
        Assert.Equal(ModelValidationState.Invalid, entry.ValidationState);
        error = Assert.Single(entry.Errors);
        Assert.Equal("Error3", error.ErrorMessage);
    }

    [Fact]
    [ReplaceCulture]
    public void Validate_NestedComplexType_IValidatableObject_Invalid()
    {
        // Arrange
        var actionContext = new ActionContext();
        var modelState = actionContext.ModelState;
        var validationState = new ValidationStateDictionary();

        var validator = CreateValidator();

        var model = (object)new ValidatableModelContainer
        {
            ValidatableModelProperty = new ValidatableModel(),
        };

        modelState.SetModelValue("parameter", "model", "model");
        validationState.Add(model, new ValidationStateEntry() { Key = "parameter" });

        // Act
        validator.Validate(actionContext, validationState, "parameter", model);

        // Assert
        Assert.False(modelState.IsValid);
        Assert.Collection(
            modelState,
            entry =>
            {
                Assert.Equal("parameter", entry.Key);
                Assert.Equal(ModelValidationState.Unvalidated, entry.Value.ValidationState);
                Assert.Empty(entry.Value.Errors);
            },
            entry =>
            {
                Assert.Equal("parameter.ValidatableModelProperty", entry.Key);
                Assert.Equal(ModelValidationState.Invalid, entry.Value.ValidationState);
                var error = Assert.Single(entry.Value.Errors);
                Assert.Equal(
                    "Error1 about 'ValidatableModelProperty' (display: 'Never valid').",
                    error.ErrorMessage);
            },
            entry =>
            {
                Assert.Equal("parameter.ValidatableModelProperty.Property1", entry.Key);
                Assert.Equal(ModelValidationState.Invalid, entry.Value.ValidationState);
                var error = Assert.Single(entry.Value.Errors);
                Assert.Equal("Error2", error.ErrorMessage);
            },
            entry =>
            {
                Assert.Equal("parameter.ValidatableModelProperty.Property2", entry.Key);
                Assert.Equal(ModelValidationState.Invalid, entry.Value.ValidationState);
                var error = Assert.Single(entry.Value.Errors);
                Assert.Equal("Error3", error.ErrorMessage);
            },
            entry =>
            {
                Assert.Equal("parameter.ValidatableModelProperty.Property3", entry.Key);
                Assert.Equal(ModelValidationState.Invalid, entry.Value.ValidationState);
                var error = Assert.Single(entry.Value.Errors);
                Assert.Equal("Error3", error.ErrorMessage);
            });
    }

    [ConditionalFact]
    [FrameworkSkipCondition(RuntimeFrameworks.Mono)]
    public void Validate_ComplexType_IValidatableObject_CanUseRequestServices()
    {
        // Arrange
        var service = new Mock<IExampleService>();
        service.Setup(x => x.DoSomething()).Verifiable();

        var provider = new ServiceCollection().AddSingleton(service.Object).BuildServiceProvider();

        var httpContext = new Mock<HttpContext>();
        httpContext.SetupGet(x => x.RequestServices).Returns(provider);

        var actionContext = new ActionContext { HttpContext = httpContext.Object };

        var modelState = actionContext.ModelState;
        var validationState = new ValidationStateDictionary();

        var validator = CreateValidator();

        var model = new Mock<IValidatableObject>();
        model
            .Setup(x => x.Validate(It.IsAny<ValidationContext>()))
            .Callback((ValidationContext context) =>
            {
                var receivedService = context.GetService<IExampleService>();
                Assert.Equal(service.Object, receivedService);
                receivedService.DoSomething();
            })
            .Returns(new List<ValidationResult>());

        // Act
        validator.Validate(actionContext, validationState, prefix: null, model: model.Object);

        // Assert
        service.Verify();
    }

    [Fact]
    [ReplaceCulture]
    public void Validate_ComplexType_FieldsAreIgnored_Valid()
    {
        // Arrange
        var actionContext = new ActionContext();
        var modelState = actionContext.ModelState;
        var validationState = new ValidationStateDictionary();

        var validator = CreateValidator();

        var model = (object)new VariableTest() { test = 5 };

        modelState.SetModelValue("parameter", "5", "5");
        validationState.Add(model, new ValidationStateEntry() { Key = "parameter" });

        // Act
        validator.Validate(actionContext, validationState, "parameter", model);

        // Assert
        Assert.True(modelState.IsValid);
        Assert.Single(modelState);

        var entry = modelState["parameter"];
        Assert.Equal(ModelValidationState.Valid, entry.ValidationState);
        Assert.Empty(entry.Errors);
    }

    [Fact]
    [ReplaceCulture]
    public void Validate_ComplexType_SecondLevelCyclesNotFollowed_Invalid()
    {
        // Arrange
        var actionContext = new ActionContext();
        var modelState = actionContext.ModelState;
        var validationState = new ValidationStateDictionary();

        var validator = CreateValidator();

        var person = new Person() { Name = "Billy" };
        person.Family = new Family { Members = new List<Person> { person } };

        var model = (object)person;

        modelState.SetModelValue("parameter.Name", "Billy", "Billy");
        validationState.Add(model, new ValidationStateEntry() { Key = "parameter" });

        // Act
        validator.Validate(actionContext, validationState, "parameter", model);

        // Assert
        Assert.False(modelState.IsValid);
        AssertKeysEqual(modelState, "parameter.Name", "parameter.Profession");

        var entry = modelState["parameter.Name"];
        Assert.Equal(ModelValidationState.Valid, entry.ValidationState);
        Assert.Empty(entry.Errors);

        entry = modelState["parameter.Profession"];
        Assert.Equal(ModelValidationState.Invalid, entry.ValidationState);
        var error = Assert.Single(entry.Errors);
        Assert.Equal(error.ErrorMessage, ValidationAttributeUtil.GetRequiredErrorMessage("Profession"));
    }

    [Fact]
    [ReplaceCulture]
    public void Validate_ComplexType_CyclesNotFollowed_Invalid()
    {
        // Arrange
        var actionContext = new ActionContext();
        var modelState = actionContext.ModelState;
        var validationState = new ValidationStateDictionary();

        var validator = CreateValidator();

        var person = new Person() { Name = "Billy" };
        person.Friend = person;

        var model = (object)person;

        modelState.SetModelValue("parameter.Name", "Billy", "Billy");
        validationState.Add(model, new ValidationStateEntry() { Key = "parameter" });

        // Act
        validator.Validate(actionContext, validationState, "parameter", model);

        // Assert
        Assert.False(modelState.IsValid);
        AssertKeysEqual(modelState, "parameter.Name", "parameter.Profession");

        var entry = modelState["parameter.Name"];
        Assert.Equal(ModelValidationState.Valid, entry.ValidationState);
        Assert.Empty(entry.Errors);

        entry = modelState["parameter.Profession"];
        Assert.Equal(ModelValidationState.Invalid, entry.ValidationState);
        var error = Assert.Single(entry.Errors);
        Assert.Equal(error.ErrorMessage, ValidationAttributeUtil.GetRequiredErrorMessage("Profession"));
    }

    [Fact]
    public void Validate_ComplexType_ShortCircuit_WhenMaxErrorCountIsSet()
    {
        // Arrange
        var actionContext = new ActionContext();
        var modelState = actionContext.ModelState;
        var validationState = new ValidationStateDictionary();

        var validator = CreateValidator(typeof(string));

        var model = new User()
        {
            Password = "password-val",
            ConfirmPassword = "not-password-val"
        };

        modelState.MaxAllowedErrors = 2;
        modelState.AddModelError("key1", "error1");
        modelState.SetModelValue("user.Password", "password-val", "password-val");
        modelState.SetModelValue("user.ConfirmPassword", "not-password-val", "not-password-val");

        validationState.Add(model, new ValidationStateEntry() { Key = "user", });

        // Act
        validator.Validate(actionContext, validationState, "user", model);

        // Assert
        Assert.False(modelState.IsValid);
        AssertKeysEqual(modelState, string.Empty, "key1", "user.ConfirmPassword", "user.Password");

        var entry = modelState[string.Empty];
        Assert.Equal(ModelValidationState.Invalid, entry.ValidationState);
        var error = Assert.Single(entry.Errors);
        Assert.IsType<TooManyModelErrorsException>(error.Exception);
    }

    [Fact]
    [ReplaceCulture]
    public void Validate_CollectionType_ArrayOfSimpleType_Valid_DefaultKeyPattern()
    {
        // Arrange
        var actionContext = new ActionContext();
        var modelState = actionContext.ModelState;
        var validationState = new ValidationStateDictionary();

        var validator = CreateValidator();

        var model = (object)new int[] { 5, 17 };

        modelState.SetModelValue("parameter[0]", "5", "17");
        modelState.SetModelValue("parameter[1]", "17", "5");
        validationState.Add(model, new ValidationStateEntry() { Key = "parameter" });

        // Act
        validator.Validate(actionContext, validationState, "parameter", model);

        // Assert
        Assert.True(modelState.IsValid);
        AssertKeysEqual(modelState, "parameter[0]", "parameter[1]");

        var entry = modelState["parameter[0]"];
        Assert.Equal(ModelValidationState.Valid, entry.ValidationState);
        Assert.Empty(entry.Errors);

        entry = modelState["parameter[0]"];
        Assert.Equal(ModelValidationState.Valid, entry.ValidationState);
        Assert.Empty(entry.Errors);
    }

    [Fact]
    [ReplaceCulture]
    public void Validate_CollectionType_ArrayOfComplexType_Invalid()
    {
        // Arrange
        var actionContext = new ActionContext();
        var modelState = actionContext.ModelState;
        var validationState = new ValidationStateDictionary();

        var validator = CreateValidator();

        var model = (object)new Person[] { new Person(), new Person() };

        validationState.Add(model, new ValidationStateEntry() { Key = string.Empty });

        // Act
        validator.Validate(actionContext, validationState, string.Empty, model);

        // Assert
        Assert.False(modelState.IsValid);
        AssertKeysEqual(modelState, "[0].Name", "[0].Profession", "[1].Name", "[1].Profession");

        var entry = modelState["[0].Name"];
        Assert.Equal(ModelValidationState.Invalid, entry.ValidationState);
        var error = Assert.Single(entry.Errors);
        Assert.Equal(ValidationAttributeUtil.GetRequiredErrorMessage("Name"), error.ErrorMessage);

        entry = modelState["[0].Profession"];
        Assert.Equal(ModelValidationState.Invalid, entry.ValidationState);
        error = Assert.Single(entry.Errors);
        Assert.Equal(ValidationAttributeUtil.GetRequiredErrorMessage("Profession"), error.ErrorMessage);

        entry = modelState["[1].Name"];
        Assert.Equal(ModelValidationState.Invalid, entry.ValidationState);
        error = Assert.Single(entry.Errors);
        Assert.Equal(ValidationAttributeUtil.GetRequiredErrorMessage("Name"), error.ErrorMessage);

        entry = modelState["[1].Profession"];
        Assert.Equal(ModelValidationState.Invalid, entry.ValidationState);
        error = Assert.Single(entry.Errors);
        Assert.Equal(ValidationAttributeUtil.GetRequiredErrorMessage("Profession"), error.ErrorMessage);
    }

    [Fact]
    [ReplaceCulture]
    public void Validate_CollectionType_ListOfComplexType_Invalid()
    {
        // Arrange
        var actionContext = new ActionContext();
        var modelState = actionContext.ModelState;
        var validationState = new ValidationStateDictionary();

        var validator = CreateValidator();

        var model = (object)new List<Person> { new Person(), new Person() };

        validationState.Add(model, new ValidationStateEntry() { Key = string.Empty });

        // Act
        validator.Validate(actionContext, validationState, string.Empty, model);

        // Assert
        Assert.False(modelState.IsValid);
        AssertKeysEqual(modelState, "[0].Name", "[0].Profession", "[1].Name", "[1].Profession");

        var entry = modelState["[0].Name"];
        Assert.Equal(ModelValidationState.Invalid, entry.ValidationState);
        var error = Assert.Single(entry.Errors);
        Assert.Equal(ValidationAttributeUtil.GetRequiredErrorMessage("Name"), error.ErrorMessage);

        entry = modelState["[0].Profession"];
        Assert.Equal(ModelValidationState.Invalid, entry.ValidationState);
        error = Assert.Single(entry.Errors);
        Assert.Equal(ValidationAttributeUtil.GetRequiredErrorMessage("Profession"), error.ErrorMessage);

        entry = modelState["[1].Name"];
        Assert.Equal(ModelValidationState.Invalid, entry.ValidationState);
        error = Assert.Single(entry.Errors);
        Assert.Equal(ValidationAttributeUtil.GetRequiredErrorMessage("Name"), error.ErrorMessage);

        entry = modelState["[1].Profession"];
        Assert.Equal(ModelValidationState.Invalid, entry.ValidationState);
        error = Assert.Single(entry.Errors);
        Assert.Equal(ValidationAttributeUtil.GetRequiredErrorMessage("Profession"), error.ErrorMessage);
    }

    public static TheoryData<object, Type> ValidCollectionData
    {
        get
        {
            return new TheoryData<object, Type>()
                {
                    { new int[] { 1, 2, 3 }, typeof(int[]) },
                    { new string[] { "Foo", "Bar", "Baz" }, typeof(string[]) },
                    { new List<string> { "Foo", "Bar", "Baz" }, typeof(IList<string>)},
                    { new HashSet<string> { "Foo", "Bar", "Baz" }, typeof(string[]) },
                    {
                        new List<DateTime>
                        {
                            new DateTime(2014, 1, 1),
                            new DateTime(2014, 2, 1),
                            new DateTime(2014, 3, 1),
                        },
                        typeof(ICollection<DateTime>)
                    },
                    {
                        new HashSet<Uri>
                        {
                            new Uri("http://example.com/1"),
                            new Uri("http://example.com/2"),
                            new Uri("http://example.com/3"),
                        },
                        typeof(HashSet<Uri>)
                    },
                };
        }
    }

    [Theory]
    [MemberData(nameof(ValidCollectionData))]
    public void Validate_IndexedCollectionTypes_Valid(object model, Type type)
    {
        // Arrange
        var actionContext = new ActionContext();
        var modelState = actionContext.ModelState;
        var validationState = new ValidationStateDictionary();

        var validator = CreateValidator();

        modelState.SetModelValue("items[0]", "value1", "value1");
        modelState.SetModelValue("items[1]", "value2", "value2");
        modelState.SetModelValue("items[2]", "value3", "value3");
        validationState.Add(model, new ValidationStateEntry()
        {
            Key = "items",

            // Force the validator to treat it as the specified type.
            Metadata = MetadataProvider.GetMetadataForType(type),
        });

        // Act
        validator.Validate(actionContext, validationState, "items", model);

        // Assert
        Assert.True(modelState.IsValid);
        AssertKeysEqual(modelState, "items[0]", "items[1]", "items[2]");

        var entry = modelState["items[0]"];
        Assert.Equal(ModelValidationState.Valid, entry.ValidationState);
        Assert.Empty(entry.Errors);

        entry = modelState["items[1]"];
        Assert.Equal(ModelValidationState.Valid, entry.ValidationState);
        Assert.Empty(entry.Errors);

        entry = modelState["items[2]"];
        Assert.Equal(ModelValidationState.Valid, entry.ValidationState);
        Assert.Empty(entry.Errors);
    }

    [Fact]
    public void Validate_CollectionType_MultipleInvalidItems()
    {
        // Arrange
        var actionContext = new ActionContext();
        var modelState = actionContext.ModelState;
        var model = new InvalidItemsContainer();
        var validationState = new ValidationStateDictionary
            {
                { model, new ValidationStateEntry() },
            };

        var validator = CreateValidator();

        // Act
        validator.Validate(actionContext, validationState, prefix: null, model: model);

        // Assert
        Assert.Collection(
            modelState,
            state =>
            {
                Assert.Equal("Items[0]", state.Key);
                var error = Assert.Single(state.Value.Errors);
                Assert.Equal("Collection contains duplicate value 'Joe'.", error.ErrorMessage);
            },
            state =>
            {
                Assert.Equal("Items[2]", state.Key);
                var error = Assert.Single(state.Value.Errors);
                Assert.Equal("Collection contains duplicate value 'Joe'.", error.ErrorMessage);
            });
    }

    [Fact]
    public void Validate_CollectionType_DictionaryOfSimpleType_Invalid()
    {
        // Arrange
        var actionContext = new ActionContext();
        var modelState = actionContext.ModelState;
        var validationState = new ValidationStateDictionary();

        var validator = CreateValidator();

        var model = new Dictionary<string, string>()
            {
                { "FooKey", "FooValue" },
                { "BarKey", "BarValue" }
            };

        modelState.SetModelValue("items[0].Key", "key0", "key0");
        modelState.SetModelValue("items[0].Value", "value0", "value0");
        modelState.SetModelValue("items[1].Key", "key1", "key1");
        modelState.SetModelValue("items[1].Value", "value1", "value1");
        validationState.Add(model, new ValidationStateEntry() { Key = "items" });

        // Act
        validator.Validate(actionContext, validationState, "items", model);

        // Assert
        Assert.True(modelState.IsValid);
        AssertKeysEqual(modelState, "items[0].Key", "items[0].Value", "items[1].Key", "items[1].Value");

        var entry = modelState["items[0].Key"];
        Assert.Equal(ModelValidationState.Valid, entry.ValidationState);
        Assert.Empty(entry.Errors);

        entry = modelState["items[0].Value"];
        Assert.Equal(ModelValidationState.Valid, entry.ValidationState);
        Assert.Empty(entry.Errors);

        entry = modelState["items[1].Key"];
        Assert.Equal(ModelValidationState.Valid, entry.ValidationState);
        Assert.Empty(entry.Errors);

        entry = modelState["items[1].Value"];
        Assert.Equal(ModelValidationState.Valid, entry.ValidationState);
        Assert.Empty(entry.Errors);
    }

    [Fact]
    [ReplaceCulture]
    public void Validate_CollectionType_DictionaryOfComplexType_Invalid()
    {
        // Arrange
        var actionContext = new ActionContext();
        var modelState = actionContext.ModelState;
        var validationState = new ValidationStateDictionary();

        var validator = CreateValidator();

        var model = (object)new Dictionary<string, Person> { { "Joe", new Person() }, { "Mark", new Person() } };

        modelState.SetModelValue("[0].Key", "Joe", "Joe");
        modelState.SetModelValue("[1].Key", "Mark", "Mark");
        validationState.Add(model, new ValidationStateEntry() { Key = string.Empty });

        // Act
        validator.Validate(actionContext, validationState, string.Empty, model);

        // Assert
        Assert.False(modelState.IsValid);
        AssertKeysEqual(
            modelState,
            "[0].Key",
            "[0].Value.Name",
            "[0].Value.Profession",
            "[1].Key",
            "[1].Value.Name",
            "[1].Value.Profession");

        var entry = modelState["[0].Key"];
        Assert.Equal(ModelValidationState.Valid, entry.ValidationState);
        Assert.Empty(entry.Errors);

        entry = modelState["[1].Key"];
        Assert.Equal(ModelValidationState.Valid, entry.ValidationState);
        Assert.Empty(entry.Errors);

        entry = modelState["[0].Value.Name"];
        Assert.Equal(ModelValidationState.Invalid, entry.ValidationState);
        var error = Assert.Single(entry.Errors);
        Assert.Equal(error.ErrorMessage, ValidationAttributeUtil.GetRequiredErrorMessage("Name"));

        entry = modelState["[0].Value.Profession"];
        Assert.Equal(ModelValidationState.Invalid, entry.ValidationState);
        error = Assert.Single(entry.Errors);
        Assert.Equal(error.ErrorMessage, ValidationAttributeUtil.GetRequiredErrorMessage("Profession"));

        entry = modelState["[1].Value.Name"];
        Assert.Equal(ModelValidationState.Invalid, entry.ValidationState);
        error = Assert.Single(entry.Errors);
        Assert.Equal(error.ErrorMessage, ValidationAttributeUtil.GetRequiredErrorMessage("Name"));

        entry = modelState["[1].Value.Profession"];
        Assert.Equal(ModelValidationState.Invalid, entry.ValidationState);
        error = Assert.Single(entry.Errors);
        Assert.Equal(error.ErrorMessage, ValidationAttributeUtil.GetRequiredErrorMessage("Profession"));
    }

    [Fact]
    [ReplaceCulture]
    public void Validate_DoesntCatchExceptions_FromPropertyAccessors()
    {
        // Arrange
        var actionContext = new ActionContext();
        var modelState = actionContext.ModelState;
        var validationState = new ValidationStateDictionary();

        var validator = CreateValidator();

        var model = new ThrowingProperty();

        // Act & Assert
        Assert.Throws<InvalidTimeZoneException>(
            () =>
            {
                validator.Validate(actionContext, validationState, string.Empty, model);
            });
    }

    // We use the reference equality comparer for breaking cycles
    [Fact]
    public void Validate_DoesNotUseOverridden_GetHashCodeOrEquals()
    {
        // Arrange
        var actionContext = new ActionContext();
        var modelState = actionContext.ModelState;
        var validationState = new ValidationStateDictionary();

        var validator = CreateValidator();

        var model = new TypeThatOverridesEquals[]
        {
                new TypeThatOverridesEquals { Funny = "hehe" },
                new TypeThatOverridesEquals { Funny = "hehe" }
        };

        // Act & Assert (does not throw)
        validator.Validate(actionContext, validationState, string.Empty, model);
    }

    [Fact]
    public void Validate_ForExcludedComplexType_PropertiesMarkedAsSkipped()
    {
        // Arrange
        var actionContext = new ActionContext();
        var modelState = actionContext.ModelState;
        var validationState = new ValidationStateDictionary();

        var validator = CreateValidator(typeof(User));

        var model = new User()
        {
            Password = "password-val",
            ConfirmPassword = "not-password-val"
        };

        // Note that user.ConfirmPassword has no entry in modelstate - we should not
        // create one just to mark it as skipped.
        modelState.SetModelValue("user.Password", "password-val", "password-val");
        validationState.Add(model, new ValidationStateEntry() { Key = "user", });

        // Act
        validator.Validate(actionContext, validationState, "user", model);

        // Assert
        Assert.Equal(ModelValidationState.Valid, modelState.ValidationState);
        AssertKeysEqual(modelState, "user.Password");

        var entry = modelState["user.Password"];
        Assert.Equal(ModelValidationState.Skipped, entry.ValidationState);
        Assert.Empty(entry.Errors);
    }

    [Fact]
    public void Validate_ForExcludedCollectionType_PropertiesMarkedAsSkipped()
    {
        // Arrange
        var actionContext = new ActionContext();
        var modelState = actionContext.ModelState;
        var validationState = new ValidationStateDictionary();

        var validator = CreateValidator(typeof(List<ValidatedModel>));

        var model = new List<ValidatedModel>()
            {
                new ValidatedModel { Value = "15" },
            };

        modelState.SetModelValue("userIds[0]", "15", "15");
        validationState.Add(model, new ValidationStateEntry() { Key = "userIds", });

        // Act
        validator.Validate(actionContext, validationState, "userIds", model);

        // Assert
        Assert.Equal(ModelValidationState.Valid, modelState.ValidationState);
        AssertKeysEqual(modelState, "userIds[0]");

        var entry = modelState["userIds[0]"];
        Assert.Equal(ModelValidationState.Skipped, entry.ValidationState);
        Assert.Empty(entry.Errors);
    }

    private class ValidatedModel
    {
        [Required]
        public string Value { get; set; }
    }

    [Fact]
    public void Validate_SuppressesValidation_ForExcludedType_Stream()
    {
        // Arrange
        var options = new MvcOptions();
        var optionsSetup = new MvcCoreMvcOptionsSetup(Mock.Of<IHttpRequestStreamReaderFactory>());
        optionsSetup.Configure(options);
        var validator = CreateValidator(providers: options.ModelMetadataDetailsProviders.ToArray());
        var model = new MemoryStream(Encoding.UTF8.GetBytes("Hello!"));
        var actionContext = new ActionContext();
        var modelState = actionContext.ModelState;
        modelState.SetModelValue("parameter", rawValue: null, attemptedValue: null);
        var validationState = new ValidationStateDictionary
            {
                { model, new ValidationStateEntry() { Key = "parameter" } }
            };

        // Act
        validator.Validate(actionContext, validationState, "parameter", model);

        // Assert
        Assert.True(modelState.IsValid);
        var entry = Assert.Single(modelState);
        Assert.Equal(ModelValidationState.Valid, entry.Value.ValidationState);
        Assert.Empty(entry.Value.Errors);
    }

    // Regression test for aspnet/Mvc#7992
    [Fact]
    public void Validate_SuppressValidation_AfterHasReachedMaxErrors_Invalid()
    {
        // Arrange
        var actionContext = new ActionContext();
        var modelState = actionContext.ModelState;
        modelState.MaxAllowedErrors = 2;
        modelState.AddModelError(key: "one", errorMessage: "1");
        modelState.AddModelError(key: "two", errorMessage: "2");

        var validator = CreateValidator();
        var model = (object)23; // Box ASAP
        var validationState = new ValidationStateDictionary
            {
                { model, new ValidationStateEntry { SuppressValidation = true } }
            };

        // Act
        validator.Validate(actionContext, validationState, prefix: string.Empty, model);

        // Assert
        Assert.False(modelState.IsValid);
        Assert.True(modelState.HasReachedMaxErrors);
        Assert.Collection(
            modelState,
            kvp =>
            {
                Assert.Empty(kvp.Key);
                Assert.Equal(ModelValidationState.Invalid, kvp.Value.ValidationState);
                var error = Assert.Single(kvp.Value.Errors);
                Assert.IsType<TooManyModelErrorsException>(error.Exception);
            },
            kvp =>
            {
                Assert.Equal("one", kvp.Key);
                Assert.Equal(ModelValidationState.Invalid, kvp.Value.ValidationState);
                var error = Assert.Single(kvp.Value.Errors);
                Assert.Equal("1", error.ErrorMessage);
            });
    }

    [Theory]
    [InlineData(1)]
    [InlineData(5)]
    public void Validate_Throws_IfValidationDepthExceedsMaxDepth(int maxDepth)
    {
        // Arrange
        var expected = $"ValidationVisitor exceeded the maximum configured validation depth '{maxDepth}' when validating property '{nameof(DepthObject.Depth)}' on type '{typeof(DepthObject)}'. " +
            "This may indicate a very deep or infinitely recursive object graph. Consider modifying 'MvcOptions.MaxValidationDepth' or suppressing validation on the model type.";
        _options.MaxValidationDepth = maxDepth;
        var actionContext = new ActionContext();
        var validator = CreateValidator();
        var model = new DepthObject(maxDepth);
        var validationState = new ValidationStateDictionary
            {
                { model, new ValidationStateEntry() }
            };

        // Act & Assert
        var ex = Assert.Throws<InvalidOperationException>(() => validator.Validate(actionContext, validationState, prefix: string.Empty, model));
        Assert.Equal(expected, ex.Message);
        Assert.Equal("https://aka.ms/AA21ue1", ex.HelpLink);
    }

    [Fact]
    public void Validate_WorksIfObjectGraphIsSmallerThanMaxDepth()
    {
        // Arrange
        var maxDepth = 5;
        _options.MaxValidationDepth = maxDepth;
        var actionContext = new ActionContext();
        var validator = CreateValidator();
        var model = new DepthObject(maxDepth - 1);
        var validationState = new ValidationStateDictionary
            {
                { model, new ValidationStateEntry() }
            };

        // Act & Assert
        validator.Validate(actionContext, validationState, prefix: string.Empty, model);
        Assert.True(actionContext.ModelState.IsValid);
    }

    [Fact]
    public void Validate_DoesNotThrow_IfNumberOfErrorsAfterReachingMaxAllowedErrors_GoesOverMaxDepth()
    {
        // Arrange
        var maxDepth = 4;
        _options.MaxValidationDepth = maxDepth;
        var actionContext = new ActionContext();
        actionContext.ModelState.MaxAllowedErrors = 2;
        var validator = CreateValidator();
        var model = new List<ModelWithRequiredProperty>
            {
                new ModelWithRequiredProperty(), new ModelWithRequiredProperty(),
                // After the first 2 items we will reach MaxAllowedErrors
                // If we add items without popping after having reached max validation,
                // with 4 more items (on top of the list) we would go over max depth of 4
                new ModelWithRequiredProperty(), new ModelWithRequiredProperty(),
                new ModelWithRequiredProperty(), new ModelWithRequiredProperty(),
            };

        var validationState = new ValidationStateDictionary
            {
                { model, new ValidationStateEntry() }
            };

        // Act & Assert
        validator.Validate(actionContext, validationState, prefix: string.Empty, model);
        Assert.False(actionContext.ModelState.IsValid);
    }

    [Theory]
    [InlineData(false, ModelValidationState.Unvalidated)]
    [InlineData(true, ModelValidationState.Invalid)]
    public void Validate_RespectsMvcOptionsConfiguration_WhenChildValidationFails(bool optionValue, ModelValidationState expectedParentValidationState)
    {
        // Arrange
        _options.ValidateComplexTypesIfChildValidationFails = optionValue;

        var actionContext = new ActionContext();
        var validationState = new ValidationStateDictionary();
        var validator = CreateValidator();

        var model = (object)new SelfValidatableModelContainer
        {
            IsParentValid = false,
            ValidatableModelProperty = new ValidatableModel()
        };

        // Act
        validator.Validate(actionContext, validationState, prefix: string.Empty, model);

        // Assert
        var modelState = actionContext.ModelState;
        Assert.False(modelState.IsValid);
        Assert.Equal(expectedParentValidationState, modelState.Root.ValidationState);
    }

    [Fact]
    public void Validate_TypeWithoutValidators()
    {
        var actionContext = new ActionContext();
        var validator = CreateValidator();
        var model = new ModelWithoutValidation();
        var validationState = new ValidationStateDictionary
            {
                { model, new ValidationStateEntry() }
            };

        actionContext.ModelState.SetModelValue("Property1", new ValueProviderResult("value1"));
        actionContext.ModelState.SetModelValue("Property2", new ValueProviderResult("value2"));

        // Act
        validator.Validate(actionContext, validationState, string.Empty, model);

        // Assert
        var modelState = actionContext.ModelState;
        Assert.Equal(ModelValidationState.Valid, modelState.ValidationState);
        Assert.True(modelState.IsValid);

        var entry = modelState["Property1"];
        Assert.Equal(ModelValidationState.Valid, entry.ValidationState);

        entry = modelState["Property2"];
        Assert.Equal(ModelValidationState.Valid, entry.ValidationState);
    }

    [Fact]
    public void Validate_TypeWithoutValidators_DoesNotUpdateValidationState()
    {
        var actionContext = new ActionContext();
        var validator = CreateValidator();
        var model = new ModelWithoutValidation();
        var validationState = new ValidationStateDictionary
            {
                { model, new ValidationStateEntry() }
            };

        var modelState = actionContext.ModelState;
        modelState.SetModelValue("Property1", new ValueProviderResult("value1"));
        modelState.SetModelValue("Property2", new ValueProviderResult("value2"));
        modelState["Property2"].ValidationState = ModelValidationState.Skipped;

        // Act
        validator.Validate(actionContext, validationState, string.Empty, model);

        // Assert
        Assert.Equal(ModelValidationState.Valid, modelState.ValidationState);
        Assert.True(modelState.IsValid);

        var entry = modelState["Property1"];
        Assert.Equal(ModelValidationState.Valid, entry.ValidationState);

        entry = modelState["Property2"];
        Assert.Equal(ModelValidationState.Skipped, entry.ValidationState);
    }

    [Fact]
    public void Validate_TypeWithoutValidators_DoesNotResetInvalidState()
    {
        var actionContext = new ActionContext();
        var validator = CreateValidator();
        var model = new ModelWithoutValidation();
        var validationState = new ValidationStateDictionary
            {
                { model, new ValidationStateEntry() }
            };

        var modelState = actionContext.ModelState;
        modelState.SetModelValue("Property1", new ValueProviderResult("value1"));
        modelState.SetModelValue("Property2", new ValueProviderResult("value2"));
        modelState["Property2"].ValidationState = ModelValidationState.Invalid;

        // Act
        validator.Validate(actionContext, validationState, string.Empty, model);

        // Assert
        Assert.Equal(ModelValidationState.Invalid, modelState.ValidationState);
        Assert.False(modelState.IsValid);

        var entry = modelState["Property1"];
        Assert.Equal(ModelValidationState.Valid, entry.ValidationState);

        entry = modelState["Property2"];
        Assert.Equal(ModelValidationState.Invalid, entry.ValidationState);
    }

    public class ModelWithRequiredProperty
    {
        [Required]
        public string MyProperty { get; set; }
    }

    private class ModelWithoutValidation
    {
        public string Property1 { get; set; }

        public string Property2 { get; set; }
    }

    private static DefaultObjectValidator CreateValidator(Type excludedType)
    {
        var excludeFilters = new List<SuppressChildValidationMetadataProvider>();
        if (excludedType != null)
        {
            excludeFilters.Add(new SuppressChildValidationMetadataProvider(excludedType));
        }

        var metadataProvider = TestModelMetadataProvider.CreateDefaultProvider(excludeFilters.ToArray());
        var validatorProviders = TestModelValidatorProvider.CreateDefaultProvider().ValidatorProviders;
        return new DefaultObjectValidator(metadataProvider, validatorProviders, new MvcOptions());
    }

    private DefaultObjectValidator CreateValidator(params IMetadataDetailsProvider[] providers)
    {
        var metadataProvider = TestModelMetadataProvider.CreateDefaultProvider(providers);
        var validatorProviders = TestModelValidatorProvider.CreateDefaultProvider().ValidatorProviders;
        return new DefaultObjectValidator(metadataProvider, validatorProviders, _options);
    }

    private static void AssertKeysEqual(ModelStateDictionary modelState, params string[] keys)
    {
        Assert.Equal<string>(keys.OrderBy(k => k).ToArray(), modelState.Keys.OrderBy(k => k).ToArray());
    }

    private class ThrowingProperty
    {
        [Required]
        public string WatchOut
        {
            get
            {
                throw new InvalidTimeZoneException();
            }
        }
    }

    private class Person
    {
        [Required, StringLength(10)]
        public string Name { get; set; }

        [Required]
        public string Profession { get; set; }

        public Person Friend { get; set; }

        public Family Family { get; set; }
    }

    private class Family
    {
        public List<Person> Members { get; set; }
    }

    private class Person2
    {
        public string Name { get; set; }
        public Address Address { get; set; }
    }

    private class Address
    {
        [StringLength(5)]
        [RegularExpression("hehehe")]
        public string Street { get; set; }
    }

    private struct ValueType
    {
        public int Value { get; set; }
        public string Reference { get; set; }
    }

    private class ReferenceType
    {
        public int Value { get; set; }
        public string Reference { get; set; }
    }

    private class ValidatableModel : IValidatableObject
    {
        public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
        {
            yield return new ValidationResult(
                $"Error1 about '{validationContext.MemberName}' (display: '{validationContext.DisplayName}').",
                new string[] { });
            yield return new ValidationResult("Error2", new[] { "Property1" });
            yield return new ValidationResult("Error3", new[] { "Property2", "Property3" });
        }
    }

    private class ValidatableModelContainer
    {
        [Display(Name = "Never valid")]
        public ValidatableModel ValidatableModelProperty { get; set; }
    }

    private class SelfValidatableModelContainer : IValidatableObject
    {
        public bool IsParentValid { get; set; } = true;

        [Display(Name = "Never valid")]
        public ValidatableModel ValidatableModelProperty { get; set; }

        public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
        {
            if (!IsParentValid)
            {
                yield return new ValidationResult("Parent not valid");
            }
        }
    }

    private class TypeThatOverridesEquals
    {
        [StringLength(2)]
        public string Funny { get; set; }

        public override bool Equals(object obj)
        {
            throw new InvalidOperationException();
        }

        public override int GetHashCode()
        {
            throw new InvalidOperationException();
        }
    }

    private class VariableTest
    {
        [Range(15, 25)]
        public int test;
    }

    private class User : IValidatableObject
    {
        public string Password { get; set; }

        [Compare("Password")]
        public string ConfirmPassword { get; set; }

        public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
        {
            if (Password == "password")
            {
                yield return new ValidationResult("Password does not meet complexity requirements.");
            }
        }
    }

    public interface IExampleService
    {
        void DoSomething();
    }

    private void Validate_Throws_ForTopLevelMetadataData(DepthObject depthObject) { }

    // Custom validation attribute that returns multiple entries in ValidationResult.MemberNames and those member
    // names are indexers. An example scenario is an attribute that confirms all entries in a list are unique.
    private class InvalidItemsAttribute : ValidationAttribute
    {
        protected override ValidationResult IsValid(object value, ValidationContext validationContext)
        {
            return new ValidationResult(
                "Collection contains duplicate value 'Joe'.",
                new[] { "[0]", "[2]" });
        }
    }

    private class InvalidItemsContainer
    {
        [InvalidItems]
        public List<string> Items { get; set; } = new List<string> { "Joe", "Fred", "Joe", "Herman" };
    }

    // Custom validation attribute that returns multiple entries in ValidationResult.MemberNames. An example
    // scenario is an attribute that confirms all properties in a complex type are non-empty.
    private class InvalidPropertiesAttribute : ValidationAttribute
    {
        protected override ValidationResult IsValid(object value, ValidationContext validationContext)
        {
            return new ValidationResult(
                "User object lacks some data.",
                new[] { "FirstName", "Address.City" });
        }
    }

    [InvalidProperties]
    private class InvalidProperties
    {
        public string FirstName { get; set; }

        public string LastName { get; set; } = "IsSet";

        public InvalidAddress Address { get; set; }
    }

    private class InvalidAddress
    {
        public string City { get; set; }
    }

    private class DepthObject
    {
        public DepthObject(int maxAllowedDepth, int depth = 0)
        {
            MaxAllowedDepth = maxAllowedDepth;
            Depth = depth;
        }

        [Range(-10, 400)]
        public int Depth { get; }
        public int MaxAllowedDepth { get; }

        public DepthObject Instance
        {
            get
            {
                if (Depth == MaxAllowedDepth - 1)
                {
                    return this;
                }

                return new DepthObject(MaxAllowedDepth, Depth + 1);
            }
        }
    }
}
