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

using System;
using Microsoft.AspNetCore.Mvc.Formatters;
using Microsoft.AspNetCore.Mvc.ModelBinding.Metadata;
using Microsoft.Extensions.Options;
using Xunit;

namespace Microsoft.AspNetCore.Mvc.ModelBinding
{
    public class ModelStateDictionaryTest
    {
        [Theory]
        [InlineData("")]
        [InlineData("foo")]
        public void ContainsKey_ReturnsFalse_IfNodeHasNotBeenMutated(string key)
        {
            // Arrange
            var dictionary = new ModelStateDictionary();
            dictionary.AddModelError("foo.bar", "some error");

            // Act
            var result = dictionary.ContainsKey(key);

            // Assert
            Assert.False(result);
        }

        [Theory]
        [InlineData("")]
        [InlineData("foo")]
        public void ContainsKey_ReturnsFalse_IfNodeHasBeenRemoved(string key)
        {
            // Arrange
            var dictionary = new ModelStateDictionary();
            dictionary.AddModelError(key, "some error");

            // Act
            var remove = dictionary.Remove(key);
            var containsKey = dictionary.ContainsKey(key);

            // Assert
            Assert.True(remove);
            Assert.False(containsKey);
        }

        [Theory]
        [InlineData("")]
        [InlineData("foo")]
        [InlineData("foo.bar")]
        [InlineData("foo[bar]")]
        public void ContainsKey_ReturnsTrue_IfNodeHasBeenMutated(string key)
        {
            // Arrange
            var dictionary = new ModelStateDictionary();
            dictionary.MarkFieldSkipped(key);

            // Act
            var result = dictionary.ContainsKey(key);

            // Assert
            Assert.True(result);
        }

        [Theory]
        [InlineData("foo")]
        [InlineData("foo.bar")]
        [InlineData("foo.bar[10]")]
        public void IndexerDoesNotReturnIntermediaryNodes(string key)
        {
            // Arrange
            var modelStateDictionary = new ModelStateDictionary();
            modelStateDictionary.AddModelError("foo.bar[10].baz", "error-message");

            // Act
            var result = modelStateDictionary[key];

            // Assert
            Assert.Null(result);
        }

        [Theory]
        [InlineData("prop")]
        [InlineData("first.second")]
        [InlineData("[0].prop")]
        [InlineData("[qux]")]
        [InlineData("[test].prop")]
        [InlineData("first[0].second")]
        [InlineData("first.second[0].third")]
        [InlineData("first[second][0]")]
        [InlineData("first.second.third[0]")]
        [InlineData("first.second.third[0].fourth")]
        [InlineData("first[0][second]")]
        public void Indexer_ReturnsValuesAddedUsingSetModelValue(string key)
        {
            // Arrange
            var value = "Hello world";
            var modelStateDictionary = new ModelStateDictionary();
            modelStateDictionary.SetModelValue(key, value, value);

            // Act
            var result = modelStateDictionary[key];

            // Assert
            Assert.Equal(value, result.RawValue);
        }

        [Fact]
        public void Clear_RemovesAllEntries()
        {
            // Arrange
            var dictionary = new ModelStateDictionary();
            dictionary.AddModelError("a", "a-error");
            dictionary.AddModelError("b", "b-error");
            dictionary.AddModelError("c", "c-error");

            // Act
            dictionary.Clear();

            // Assert
            Assert.Empty(dictionary);
            Assert.Equal(0, dictionary.ErrorCount);
            Assert.Empty(dictionary);
            Assert.Equal(ModelValidationState.Valid, dictionary.ValidationState);
        }

        [Fact]
        public void Clear_RemovesAllEntries_IfDictionaryIsEmpty()
        {
            // Arrange
            var dictionary = new ModelStateDictionary();

            // Act
            dictionary.Clear();

            // Assert
            Assert.Empty(dictionary);
            Assert.Equal(0, dictionary.ErrorCount);
            Assert.Empty(dictionary);
            Assert.Equal(ModelValidationState.Valid, dictionary.ValidationState);
        }

        [Fact]
        public void MarkFieldSkipped_MarksFieldAsSkipped_IfStateIsUnvalidated()
        {
            // Arrange
            var source = new ModelStateDictionary();
            source.SetModelValue("key", "value", "value");

            // Act
            source.MarkFieldSkipped("key");

            // Assert
            Assert.Equal(ModelValidationState.Skipped, source["key"].ValidationState);
        }

        [Fact]
        public void MarkFieldSkipped_MarksFieldAsSkipped_IfStateIsValid()
        {
            // Arrange
            var source = new ModelStateDictionary();
            source.MarkFieldValid("key");

            // Act
            source.MarkFieldSkipped("key");

            // Assert
            Assert.Equal(ModelValidationState.Skipped, source["key"].ValidationState);
        }

        [Fact]
        public void MarkFieldSkipped_MarksFieldAsSkipped_IfKeyIsNotPresent()
        {
            // Arrange
            var source = new ModelStateDictionary();

            // Act
            source.MarkFieldSkipped("key");

            // Assert
            Assert.Equal(0, source.ErrorCount);
            Assert.Single(source);
            Assert.Equal(ModelValidationState.Skipped, source["key"].ValidationState);
        }

        [Fact]
        public void MarkFieldSkipped_Throws_IfStateIsInvalid()
        {
            // Arrange
            var source = new ModelStateDictionary();
            source.AddModelError("key", "some error");

            // Act
            var exception = Assert.Throws<InvalidOperationException>(() => source.MarkFieldSkipped("key"));

            // Assert
            Assert.Equal(
                "A field previously marked invalid should not be marked skipped.",
                exception.Message);
        }

        [Fact]
        public void MarkFieldValid_MarksFieldAsValid_IfStateIsUnvalidated()
        {
            // Arrange
            var source = new ModelStateDictionary();
            source.SetModelValue("key", "value", "value");

            // Act
            source.MarkFieldValid("key");

            // Assert
            Assert.Equal(ModelValidationState.Valid, source["key"].ValidationState);
        }

        [Fact]
        public void MarkFieldValid_MarksFieldAsValid_IfStateIsSkipped()
        {
            // Arrange
            var source = new ModelStateDictionary();
            source.MarkFieldSkipped("key");

            // Act
            source.MarkFieldValid("key");

            // Assert
            Assert.Equal(ModelValidationState.Valid, source["key"].ValidationState);
        }

        [Fact]
        public void MarkFieldValid_MarksFieldAsValid_IfKeyIsNotPresent()
        {
            // Arrange
            var source = new ModelStateDictionary();

            // Act
            source.MarkFieldValid("key");

            // Assert
            Assert.Equal(0, source.ErrorCount);
            Assert.Single(source);
            Assert.Equal(ModelValidationState.Valid, source["key"].ValidationState);
        }

        [Fact]
        public void MarkFieldValid_Throws_IfStateIsInvalid()
        {
            // Arrange
            var source = new ModelStateDictionary();
            source.AddModelError("key", "some-error");

            // Act
            var exception = Assert.Throws<InvalidOperationException>(() => source.MarkFieldValid("key"));

            // Assert
            Assert.Equal(
                "A field previously marked invalid should not be marked valid.",
                exception.Message);
        }

        [Fact]
        public void CopyConstructor_CopiesModelStateData()
        {
            // Arrange
            var source = new ModelStateDictionary();
            source.SetModelValue("key", "attempted-value", "raw-value");
            var entry = source["key"];
            entry.AttemptedValue = "attempted-value";
            entry.RawValue = "raw-value";
            entry.Errors.Add(new ModelError(new InvalidOperationException()));
            entry.Errors.Add(new ModelError("error-message"));
            entry.ValidationState = ModelValidationState.Skipped;
            // Act
            var target = new ModelStateDictionary(source);

            // Assert
            Assert.Equal(2, target.ErrorCount);
            Assert.Single(target);
            var actual = target["key"];
            Assert.Equal(entry.RawValue, actual.RawValue);
            Assert.Equal(entry.AttemptedValue, actual.AttemptedValue);
            Assert.Equal(entry.Errors, actual.Errors);
            Assert.Equal(entry.ValidationState, actual.ValidationState);
        }

        [Fact]
        public void TryAddModelException_Succeeds()
        {
            // Arrange
            var dictionary = new ModelStateDictionary();
            var exception = new TestException();

            // Act
            dictionary.TryAddModelException("some key", exception);

            // Assert
            var kvp = Assert.Single(dictionary);
            Assert.Equal("some key", kvp.Key);
            var error = Assert.Single(kvp.Value.Errors);
            Assert.Same(exception, error.Exception);
        }

        [Fact]
        public void AddModelErrorCreatesModelStateIfNotPresent()
        {
            // Arrange
            var dictionary = new ModelStateDictionary();

            // Act
            dictionary.AddModelError("some key", "some error");

            // Assert
            Assert.Equal(1, dictionary.ErrorCount);
            var kvp = Assert.Single(dictionary);
            Assert.Equal("some key", kvp.Key);
            var error = Assert.Single(kvp.Value.Errors);
            Assert.Equal("some error", error.ErrorMessage);
        }

        [Fact]
        public void AddModelErrorUsesExistingModelStateIfPresent()
        {
            // Arrange
            var dictionary = new ModelStateDictionary();
            dictionary.AddModelError("some key", "some error");
            var exception = new Exception();
            var provider = new EmptyModelMetadataProvider();
            var metadata = provider.GetMetadataForProperty(typeof(string), nameof(string.Length));

            // Act
            dictionary.AddModelError("some key", exception, metadata);

            // Assert
            Assert.Equal(2, dictionary.ErrorCount);
            var kvp = Assert.Single(dictionary);
            Assert.Equal("some key", kvp.Key);

            Assert.Equal(2, kvp.Value.Errors.Count);
            Assert.Equal("some error", kvp.Value.Errors[0].ErrorMessage);
            Assert.Same(exception, kvp.Value.Errors[1].Exception);
        }

        [Fact]
        public void ConstructorWithDictionaryParameter()
        {
            // Arrange
            var oldDictionary = new ModelStateDictionary();
            oldDictionary.SetModelValue("foo", "bar", "bar");

            // Act
            var newDictionary = new ModelStateDictionary(oldDictionary);

            // Assert
            Assert.Single(newDictionary);
            Assert.Equal("bar", newDictionary["foo"].RawValue);
        }

        [Fact]
        public void GetFieldValidationState_ReturnsUnvalidatedIfDictionaryDoesNotContainKey()
        {
            // Arrange
            var dictionary = new ModelStateDictionary();

            // Act
            var validationState = dictionary.GetFieldValidationState("foo");

            // Assert
            Assert.Equal(ModelValidationState.Unvalidated, validationState);
        }

        [Fact]
        public void GetValidationState_ReturnsValidationStateForKey_IgnoresChildren()
        {
            // Arrange
            var dictionary = new ModelStateDictionary();
            dictionary.AddModelError("foo.bar", "error text");

            // Act
            var validationState = dictionary.GetValidationState("foo");

            // Assert
            Assert.Equal(ModelValidationState.Unvalidated, validationState);
        }

        [Theory]
        [InlineData("foo")]
        [InlineData("foo.bar")]
        [InlineData("foo[bar]")]
        public void GetFieldValidationState_ReturnsInvalidIfKeyChildContainsErrors(string key)
        {
            // Arrange
            var dictionary = new ModelStateDictionary();
            dictionary.AddModelError(key, "error text");

            // Act
            var validationState = dictionary.GetFieldValidationState("foo");

            // Assert
            Assert.Equal(ModelValidationState.Invalid, validationState);
        }

        [Theory]
        [InlineData("foo")]
        [InlineData("foo.bar")]
        [InlineData("foo[bar]")]
        public void GetFieldValidationState_ReturnsValidIfModelStateDoesNotContainErrors(string key)
        {
            // Arrange
            var dictionary = new ModelStateDictionary();
            dictionary.MarkFieldValid(key);

            // Act
            var validationState = dictionary.GetFieldValidationState("foo");

            // Assert
            Assert.Equal(ModelValidationState.Valid, validationState);
        }

        [Theory]
        [InlineData("[0].foo.bar")]
        [InlineData("[0].foo.bar[0]")]
        public void GetFieldValidationState_IndexedPrefix_ReturnsInvalidIfKeyChildContainsErrors(string key)
        {
            // Arrange
            var dictionary = new ModelStateDictionary();
            dictionary.AddModelError(key, "error text");

            // Act
            var validationState = dictionary.GetFieldValidationState("[0].foo");

            // Assert
            Assert.Equal(ModelValidationState.Invalid, validationState);
        }

        [Theory]
        [InlineData("[0].foo.bar")]
        [InlineData("[0].foo.bar[0]")]
        public void GetFieldValidationState_IndexedPrefix_ReturnsValidIfModelStateDoesNotContainErrors(string key)
        {
            // Arrange
            var dictionary = new ModelStateDictionary();
            dictionary.MarkFieldValid(key);

            // Act
            var validationState = dictionary.GetFieldValidationState("[0].foo");

            // Assert
            Assert.Equal(ModelValidationState.Valid, validationState);
        }

        [Fact]
        public void IsValidPropertyReturnsFalseIfErrors()
        {
            // Arrange
            var dictionary = new ModelStateDictionary();
            dictionary.MarkFieldValid("foo");
            dictionary.AddModelError("bar", "some error");

            // Act
            var isValid = dictionary.IsValid;
            var validationState = dictionary.ValidationState;

            // Assert
            Assert.False(isValid);
            Assert.Equal(ModelValidationState.Invalid, validationState);
        }

        [Fact]
        public void IsValidPropertyReturnsTrueIfNoErrors()
        {
            // Arrange
            var dictionary = new ModelStateDictionary();
            dictionary.MarkFieldValid("foo");
            dictionary.MarkFieldSkipped("bar");

            // Act
            var isValid = dictionary.IsValid;
            var validationState = dictionary.ValidationState;

            // Assert
            Assert.True(isValid);
            Assert.Equal(ModelValidationState.Valid, validationState);
        }

        [Fact]
        public void GetFieldValidationState_OfSkippedEntry()
        {
            // Arrange
            var modelState = new ModelStateDictionary();
            modelState.MarkFieldSkipped("foo");

            // Act
            var validationState = modelState.GetValidationState("foo");
            var fieldValidationState = modelState.GetFieldValidationState("foo");

            // Assert
            Assert.Equal(ModelValidationState.Skipped, validationState);
            Assert.Equal(ModelValidationState.Valid, fieldValidationState);
        }

        [Fact]
        public void GetFieldValidationState_WithSkippedProperty()
        {
            // Arrange
            var modelState = new ModelStateDictionary();
            modelState.MarkFieldSkipped("foo.bar.prop1");
            modelState.MarkFieldValid("foo.bar.prop2");

            // Act
            var validationState = modelState.GetFieldValidationState("foo.bar");

            // Assert
            Assert.Equal(ModelValidationState.Valid, validationState);
        }

        [Fact]
        public void GetFieldValidationState_WithAllSkippedProperties()
        {
            // Arrange
            var modelState = new ModelStateDictionary();
            modelState.MarkFieldSkipped("foo.bar.prop1");
            modelState.MarkFieldSkipped("foo.bar.prop2");

            // Act
            var validationState = modelState.GetFieldValidationState("foo.bar");

            // Assert
            Assert.Equal(ModelValidationState.Valid, validationState);
        }

        [Fact]
        public void IsValidPropertyReturnsFalse_IfSomeFieldsAreNotValidated()
        {
            // Arrange
            var dictionary = new ModelStateDictionary();
            dictionary.MarkFieldValid("foo");
            dictionary.SetModelValue("qux", "value", "value");
            dictionary.AddModelError("baz", "some error");

            // Act
            var isValid = dictionary.IsValid;
            var validationState = dictionary.ValidationState;

            // Assert
            Assert.False(isValid);
            Assert.Equal(ModelValidationState.Unvalidated, validationState);
        }

        [Fact]
        public void MergeCopiesDictionaryEntries()
        {
            // Arrange
            var dictionary1 = new ModelStateDictionary();
            dictionary1.SetModelValue("foo", "RawValue1", "AttemptedValue1");
            dictionary1.AddModelError("foo", "value1-Error1");
            dictionary1.AddModelError("foo", "value1-Error2");

            var dictionary2 = new ModelStateDictionary();
            dictionary2.SetModelValue("bar", "RawValue2", "AttemptedValue2");
            dictionary2.AddModelError("bar", "value2-Error1");

            // Act
            dictionary1.Merge(dictionary2);

            // Assert
            Assert.Equal(2, dictionary1.Count);
            var item = dictionary1["foo"];
            Assert.Equal("AttemptedValue1", item.AttemptedValue);
            Assert.Equal("RawValue1", item.RawValue);

            item = dictionary1["bar"];
            Assert.Equal("AttemptedValue2", item.AttemptedValue);
            Assert.Equal("RawValue2", item.RawValue);
            Assert.Collection(item.Errors,
                error => Assert.Equal("value2-Error1", error.ErrorMessage));
        }

        [Theory]
        [InlineData("")]
        [InlineData("key1")]
        public void MergeCopiesDictionaryOverwritesExistingValues(string key)
        {
            // Arrange
            var dictionary1 = new ModelStateDictionary();
            dictionary1.SetModelValue(key, "RawValue1", "AttemptedValue1");
            dictionary1.AddModelError(key, "value1-Error1");
            dictionary1.AddModelError(key, "value1-Error2");
            dictionary1.SetModelValue("other-key", null, null);

            var dictionary2 = new ModelStateDictionary();
            dictionary2.SetModelValue(key, "RawValue2", "AttemptedValue2");
            dictionary2.AddModelError(key, "value2-Error1");

            // Act
            dictionary1.Merge(dictionary2);

            // Assert
            Assert.Equal(2, dictionary1.Count);
            var item = dictionary1["other-key"];
            Assert.Null(item.AttemptedValue);
            Assert.Null(item.RawValue);
            Assert.Empty(item.Errors);

            item = dictionary1[key];
            Assert.Equal("AttemptedValue2", item.AttemptedValue);
            Assert.Equal("RawValue2", item.RawValue);
            Assert.Collection(item.Errors,
                error => Assert.Equal("value2-Error1", error.ErrorMessage));
        }

        [Fact]
        public void MergeDoesNothingIfParameterIsNull()
        {
            // Arrange
            var dictionary = new ModelStateDictionary();
            dictionary.SetModelValue("foo", "value", "value");

            // Act
            dictionary.Merge(null);

            // Assert
            Assert.Single(dictionary);
            Assert.True(dictionary.ContainsKey("foo"));
        }

        [Fact]
        public void SetAttemptedValueCreatesModelStateIfNotPresent()
        {
            // Arrange
            var dictionary = new ModelStateDictionary();

            // Act
            dictionary.SetModelValue("some key", new string[] { "some value" }, "some value");

            // Assert
            Assert.Single(dictionary);
            var modelState = dictionary["some key"];

            Assert.Empty(modelState.Errors);
            Assert.Equal(new string[] { "some value" }, modelState.RawValue);
            Assert.Equal("some value", modelState.AttemptedValue);
        }

        [Fact]
        public void SetAttemptedValueUsesExistingModelStateIfPresent()
        {
            // Arrange
            var dictionary = new ModelStateDictionary();
            dictionary.AddModelError("some key", "some error");
            var ex = new Exception();

            // Act
            dictionary.SetModelValue("some key", new string[] { "some value" }, "some value");

            // Assert
            Assert.Single(dictionary);
            var modelState = dictionary["some key"];

            Assert.Single(modelState.Errors);
            Assert.Equal("some error", modelState.Errors[0].ErrorMessage);
            Assert.Equal(new string[] { "some value" }, modelState.RawValue);
            Assert.Equal("some value", modelState.AttemptedValue);
        }

        [Fact]
        public void GetFieldValidity_ReturnsUnvalidated_IfNoEntryExistsForKey()
        {
            // Arrange
            var dictionary = new ModelStateDictionary();
            dictionary.SetModelValue("user.Name", new string[] { "some value" }, "some value");

            // Act
            var validationState = dictionary.GetFieldValidationState("not-user");

            // Assert
            Assert.Equal(ModelValidationState.Unvalidated, validationState);
        }

        [Fact]
        public void GetFieldValidity_ReturnsUnvalidated_IfAnyItemInSubtreeIsInvalid()
        {
            // Arrange
            var dictionary = new ModelStateDictionary();
            dictionary.MarkFieldValid("user.Address");
            dictionary.SetModelValue("user.Name", new string[] { "some value" }, "some value");
            dictionary.AddModelError("user.Age", "Age is not a valid int");

            // Act
            var validationState = dictionary.GetFieldValidationState("user");

            // Assert
            Assert.Equal(ModelValidationState.Unvalidated, validationState);
        }

        [Theory]
        [InlineData("")]
        [InlineData("user")]
        [InlineData("user.Age")]
        public void GetFieldValidity_ReturnsInvalid_IfAllKeysAreValidatedAndAnyEntryIsInvalid(string key)
        {
            // Arrange
            var dictionary = new ModelStateDictionary();
            dictionary.MarkFieldValid("user.Address");
            dictionary.MarkFieldValid("user.Name");
            dictionary.AddModelError("user.Age", "Age is not a valid int");

            // Act
            var validationState = dictionary.GetFieldValidationState(key);

            // Assert
            Assert.Equal(ModelValidationState.Invalid, validationState);
        }

        [Theory]
        [InlineData("")]
        [InlineData("[0]")]
        [InlineData("[0].product")]
        public void GetFieldValidity_ReturnsInvalid_IfAllKeysAreValidatedAndAnyEntryIsInvalid_Collection(string key)
        {
            // Arrange
            var dictionary = new ModelStateDictionary();
            dictionary.MarkFieldValid("[0].product.Name");
            dictionary.MarkFieldValid("[0].product.Age[0]");
            dictionary.AddModelError("[0].product.Name", "Name is invalid");

            // Act
            var validationState = dictionary.GetFieldValidationState(key);

            // Assert
            Assert.Equal(ModelValidationState.Invalid, validationState);
        }

        [Fact]
        public void GetFieldValidity_ReturnsValid_IfAllKeysAreValid()
        {
            // Arrange
            var dictionary = new ModelStateDictionary();
            dictionary.MarkFieldValid("user.Address");
            dictionary.MarkFieldValid("user.Name");

            // Act
            var validationState = dictionary.GetFieldValidationState("user");

            // Assert
            Assert.Equal(ModelValidationState.Valid, validationState);
        }

        [Fact]
        public void AddModelError_WithErrorString_AddsTooManyModelErrors_WhenMaxErrorsIsReached()
        {
            // Arrange
            var expected = "The maximum number of allowed model errors has been reached.";
            var dictionary = new ModelStateDictionary
            {
                MaxAllowedErrors = 5
            };
            var provider = new EmptyModelMetadataProvider();
            var metadata = provider.GetMetadataForProperty(typeof(string), nameof(string.Length));

            // Act
            dictionary.AddModelError("key1", "error1");
            dictionary.AddModelError("key2", new Exception(), metadata);
            dictionary.AddModelError("key3", new Exception(), metadata);
            dictionary.AddModelError("key4", "error4");
            dictionary.AddModelError("key5", "error5");

            // Assert
            Assert.True(dictionary.HasReachedMaxErrors);
            Assert.Equal(5, dictionary.ErrorCount);
            var error = Assert.Single(dictionary[string.Empty].Errors);
            Assert.IsType<TooManyModelErrorsException>(error.Exception);
            Assert.Equal(expected, error.Exception.Message);

            // TooManyModelErrorsException added instead of key5 error.
            Assert.DoesNotContain("key5", dictionary.Keys);
        }

        [Fact]
        public void TryAddModelException_ReturnsFalse_AndAddsMaxModelErrorMessage()
        {
            // Arrange
            var expected = "The maximum number of allowed model errors has been reached.";
            var dictionary = new ModelStateDictionary
            {
                MaxAllowedErrors = 3
            };

            // Act and Assert
            var result = dictionary.TryAddModelError("key1", "error1");
            Assert.True(result);

            result = dictionary.TryAddModelException("key2", new Exception());
            Assert.True(result);

            result = dictionary.TryAddModelException("key3", new Exception());
            Assert.False(result);

            Assert.Equal(3, dictionary.Count);
            var error = Assert.Single(dictionary[string.Empty].Errors);
            Assert.IsType<TooManyModelErrorsException>(error.Exception);
            Assert.Equal(expected, error.Exception.Message);

            // TooManyModelErrorsException added instead of key3 exception.
            Assert.DoesNotContain("key3", dictionary.Keys);
        }

        [Fact]
        public void TryAddModelError_WithErrorString_ReturnsFalse_AndAddsMaxModelErrorMessage()
        {
            // Arrange
            var expected = "The maximum number of allowed model errors has been reached.";
            var dictionary = new ModelStateDictionary
            {
                MaxAllowedErrors = 3
            };
            var provider = new EmptyModelMetadataProvider();
            var metadata = provider.GetMetadataForProperty(typeof(string), nameof(string.Length));

            // Act and Assert
            Assert.False(dictionary.HasReachedMaxErrors);
            var result = dictionary.TryAddModelError("key1", "error1");
            Assert.True(result);

            Assert.False(dictionary.HasReachedMaxErrors);
            result = dictionary.TryAddModelError("key2", new Exception(), metadata);
            Assert.True(result);

            Assert.False(dictionary.HasReachedMaxErrors); // Still room for TooManyModelErrorsException.
            result = dictionary.TryAddModelError("key3", "error3");
            Assert.False(result);

            Assert.True(dictionary.HasReachedMaxErrors);
            result = dictionary.TryAddModelError("key4", "error4"); // no-op
            Assert.False(result);

            Assert.True(dictionary.HasReachedMaxErrors);
            Assert.Equal(3, dictionary.ErrorCount);
            Assert.Equal(3, dictionary.Count);

            var error = Assert.Single(dictionary[string.Empty].Errors);
            Assert.IsType<TooManyModelErrorsException>(error.Exception);
            Assert.Equal(expected, error.Exception.Message);

            // TooManyModelErrorsException added instead of key3 error.
            Assert.DoesNotContain("key3", dictionary.Keys);

            // Last addition did nothing.
            Assert.DoesNotContain("key4", dictionary.Keys);
        }

        [Fact]
        public void AddModelError_WithException_AddsTooManyModelError_WhenMaxErrorIsReached()
        {
            // Arrange
            var expected = "The maximum number of allowed model errors has been reached.";
            var dictionary = new ModelStateDictionary
            {
                MaxAllowedErrors = 4
            };
            var provider = new EmptyModelMetadataProvider();
            var metadata = provider.GetMetadataForProperty(typeof(string), nameof(string.Length));
            dictionary.AddModelError("key1", new Exception(), metadata);
            dictionary.AddModelError("key2", "error2");
            dictionary.AddModelError("key3", "error3");
            dictionary.AddModelError("key3", new Exception(), metadata);

            // Act and Assert
            Assert.True(dictionary.HasReachedMaxErrors);
            Assert.Equal(4, dictionary.ErrorCount);
            Assert.Equal(4, dictionary.Count);
            var error = Assert.Single(dictionary[string.Empty].Errors);
            Assert.IsType<TooManyModelErrorsException>(error.Exception);
            Assert.Equal(expected, error.Exception.Message);

            // Second key3 model error resulted in TooManyModelErrorsException instead.
            error = Assert.Single(dictionary["key3"].Errors);
            Assert.Null(error.Exception);
            Assert.Equal("error3", error.ErrorMessage);
        }

        [Fact]
        public void TryAddModelError_WithException_ReturnsFalse_AndAddsMaxModelErrorMessage()
        {
            // Arrange
            var expected = "The maximum number of allowed model errors has been reached.";
            var dictionary = new ModelStateDictionary
            {
                MaxAllowedErrors = 3
            };
            var provider = new EmptyModelMetadataProvider();
            var metadata = provider.GetMetadataForProperty(typeof(string), nameof(string.Length));

            // Act and Assert
            var result = dictionary.TryAddModelError("key1", "error1");
            Assert.True(result);

            result = dictionary.TryAddModelError("key2", new Exception(), metadata);
            Assert.True(result);

            result = dictionary.TryAddModelError("key3", new Exception(), metadata);
            Assert.False(result);

            Assert.Equal(3, dictionary.Count);
            var error = Assert.Single(dictionary[string.Empty].Errors);
            Assert.IsType<TooManyModelErrorsException>(error.Exception);
            Assert.Equal(expected, error.Exception.Message);
        }

        [Fact]
        public void ModelStateDictionary_TracksAddedErrorsOverCopyConstructor()
        {
            // Arrange
            var expected = "The maximum number of allowed model errors has been reached.";
            var dictionary = new ModelStateDictionary
            {
                MaxAllowedErrors = 3
            };
            var provider = new EmptyModelMetadataProvider();
            var metadata = provider.GetMetadataForProperty(typeof(string), nameof(string.Length));

            // Act
            dictionary.AddModelError("key1", "error1");
            dictionary.TryAddModelError("key3", new Exception(), metadata);

            var copy = new ModelStateDictionary(dictionary);
            copy.AddModelError("key2", "error2");

            // Assert
            Assert.Equal(3, copy.Count);
            var error = Assert.Single(copy[string.Empty].Errors);
            Assert.IsType<TooManyModelErrorsException>(error.Exception);
            Assert.Equal(expected, error.Exception.Message);
        }

        [Theory]
        [InlineData(2, false)]
        [InlineData(3, true)]
        [InlineData(4, true)]
        public void ModelStateDictionary_HasReachedMaxErrors(int errorCount, bool expected)
        {
            // Arrange
            var dictionary = new ModelStateDictionary()
            {
                MaxAllowedErrors = 3
            };

            for (var i = 0; i < errorCount; i++)
            {
                dictionary.AddModelError("key" + i, "error");
            }

            // Act
            var canAdd = dictionary.HasReachedMaxErrors;

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

        [Fact]
        public void ModelStateDictionary_ReturnExceptionMessage_WhenModelStateNotSet()
        {
            // Arrange
            var dictionary = new ModelStateDictionary();
            var exception = new FormatException("The supplied value is invalid for Length.");

            // Act
            dictionary.TryAddModelException("key", exception);

            // Assert
            var error = Assert.Single(dictionary["key"].Errors);
            Assert.Same(exception, error.Exception);
        }

        [Fact]
        public void ModelStateDictionary_ReturnGenericErrorMessage_WhenModelStateNotSet()
        {
            // Arrange
            var expected = "The supplied value is invalid for Length.";
            var dictionary = new ModelStateDictionary();
            var provider = new EmptyModelMetadataProvider();
            var metadata = provider.GetMetadataForProperty(typeof(string), nameof(string.Length));

            // Act
            dictionary.TryAddModelError("key", new FormatException(), metadata);

            // Assert
            var error = Assert.Single(dictionary["key"].Errors);
            Assert.Equal(expected, error.ErrorMessage);
        }

        [Fact]
        public void ModelStateDictionary_AddsCustomErrorMessage_WhenModelStateNotSet()
        {
            // Arrange
            var expected = "Hmm, the supplied value is not valid for Length.";
            var dictionary = new ModelStateDictionary();

            var bindingMetadataProvider = new DefaultBindingMetadataProvider();
            var compositeProvider = new DefaultCompositeMetadataDetailsProvider(new[] { bindingMetadataProvider });
            var optionsAccessor = new OptionsAccessor();
            optionsAccessor.Value.ModelBindingMessageProvider.SetUnknownValueIsInvalidAccessor(
                name => $"Hmm, the supplied value is not valid for { name }.");

            var provider = new DefaultModelMetadataProvider(compositeProvider, optionsAccessor);
            var metadata = provider.GetMetadataForProperty(typeof(string), nameof(string.Length));

            // Act
            dictionary.TryAddModelError("key", new FormatException(), metadata);

            // Assert
            var entry = Assert.Single(dictionary);
            Assert.Equal("key", entry.Key);
            var error = Assert.Single(entry.Value.Errors);
            Assert.Equal(expected, error.ErrorMessage);
        }

        [Fact]
        public void ModelStateDictionary_AddsCustomErrorMessage_WhenModelStateNotSet_WithParameter()
        {
            // Arrange
            var expected = "Hmm, the supplied value is not valid.";
            var dictionary = new ModelStateDictionary();

            var bindingMetadataProvider = new DefaultBindingMetadataProvider();
            var compositeProvider = new DefaultCompositeMetadataDetailsProvider(new[] { bindingMetadataProvider });
            var optionsAccessor = new OptionsAccessor();
            optionsAccessor.Value.ModelBindingMessageProvider.SetNonPropertyUnknownValueIsInvalidAccessor(
                () => "Hmm, the supplied value is not valid.");

            var method = typeof(string).GetMethod(nameof(string.Copy));
            var parameter = method.GetParameters()[0]; // Copy(string str)
            var provider = new DefaultModelMetadataProvider(compositeProvider, optionsAccessor);
            var metadata = provider.GetMetadataForParameter(parameter);

            // Act
            dictionary.TryAddModelError("key", new FormatException(), metadata);

            // Assert
            var entry = Assert.Single(dictionary);
            Assert.Equal("key", entry.Key);
            var error = Assert.Single(entry.Value.Errors);
            Assert.Equal(expected, error.ErrorMessage);
        }

        [Fact]
        public void ModelStateDictionary_AddsCustomErrorMessage_WhenModelStateNotSet_WithType()
        {
            // Arrange
            var expected = "Hmm, the supplied value is not valid.";
            var dictionary = new ModelStateDictionary();

            var bindingMetadataProvider = new DefaultBindingMetadataProvider();
            var compositeProvider = new DefaultCompositeMetadataDetailsProvider(new[] { bindingMetadataProvider });
            var optionsAccessor = new OptionsAccessor();
            optionsAccessor.Value.ModelBindingMessageProvider.SetNonPropertyUnknownValueIsInvalidAccessor(
                () => "Hmm, the supplied value is not valid.");

            var provider = new DefaultModelMetadataProvider(compositeProvider, optionsAccessor);
            var metadata = provider.GetMetadataForType(typeof(int));

            // Act
            dictionary.TryAddModelError("key", new FormatException(), metadata);

            // Assert
            var entry = Assert.Single(dictionary);
            Assert.Equal("key", entry.Key);
            var error = Assert.Single(entry.Value.Errors);
            Assert.Equal(expected, error.ErrorMessage);
        }

        [Fact]
        public void TryAddModelException_ReturnExceptionMessage_WhenModelStateSet()
        {
            // Arrange
            var dictionary = new ModelStateDictionary();
            dictionary.SetModelValue("key", new string[] { "some value" }, "some value");
            var exception = new FormatException("The value 'some value' is not valid for Length.");

            // Act
            dictionary.TryAddModelException("key", exception);

            // Assert
            var error = Assert.Single(dictionary["key"].Errors);
            Assert.Same(exception, error.Exception);
        }

        [Fact]
        public void ModelStateDictionary_ReturnSpecificErrorMessage_WhenModelStateSet()
        {
            // Arrange
            var expected = "The value 'some value' is not valid for Length.";
            var dictionary = new ModelStateDictionary();
            dictionary.SetModelValue("key", new string[] { "some value" }, "some value");
            var provider = new EmptyModelMetadataProvider();
            var metadata = provider.GetMetadataForProperty(typeof(string), nameof(string.Length));

            // Act
            dictionary.TryAddModelError("key", new FormatException(), metadata);

            // Assert
            var error = Assert.Single(dictionary["key"].Errors);
            Assert.Equal(expected, error.ErrorMessage);
        }

        [Fact]
        public void ModelStateDictionary_AddsCustomErrorMessage_WhenModelStateSet()
        {
            // Arrange
            var expected = "Hmm, the value 'some value' is not valid for Length.";
            var dictionary = new ModelStateDictionary();
            dictionary.SetModelValue("key", new string[] { "some value" }, "some value");

            var bindingMetadataProvider = new DefaultBindingMetadataProvider();
            var compositeProvider = new DefaultCompositeMetadataDetailsProvider(new[] { bindingMetadataProvider });
            var optionsAccessor = new OptionsAccessor();
            optionsAccessor.Value.ModelBindingMessageProvider.SetAttemptedValueIsInvalidAccessor(
                (value, name) => $"Hmm, the value '{ value }' is not valid for { name }.");

            var provider = new DefaultModelMetadataProvider(compositeProvider, optionsAccessor);
            var metadata = provider.GetMetadataForProperty(typeof(string), nameof(string.Length));

            // Act
            dictionary.TryAddModelError("key", new FormatException(), metadata);

            // Assert
            var entry = Assert.Single(dictionary);
            Assert.Equal("key", entry.Key);
            var error = Assert.Single(entry.Value.Errors);
            Assert.Equal(expected, error.ErrorMessage);
        }

        [Fact]
        public void ModelStateDictionary_AddsCustomErrorMessage_WhenModelStateSet_WithParameter()
        {
            // Arrange
            var expected = "Hmm, the value 'some value' is not valid.";
            var dictionary = new ModelStateDictionary();
            dictionary.SetModelValue("key", new string[] { "some value" }, "some value");

            var bindingMetadataProvider = new DefaultBindingMetadataProvider();
            var compositeProvider = new DefaultCompositeMetadataDetailsProvider(new[] { bindingMetadataProvider });
            var optionsAccessor = new OptionsAccessor();
            optionsAccessor.Value.ModelBindingMessageProvider.SetNonPropertyAttemptedValueIsInvalidAccessor(
                value => $"Hmm, the value '{ value }' is not valid.");

            var method = typeof(string).GetMethod(nameof(string.Copy));
            var parameter = method.GetParameters()[0]; // Copy(string str)
            var provider = new DefaultModelMetadataProvider(compositeProvider, optionsAccessor);
            var metadata = provider.GetMetadataForParameter(parameter);

            // Act
            dictionary.TryAddModelError("key", new FormatException(), metadata);

            // Assert
            var entry = Assert.Single(dictionary);
            Assert.Equal("key", entry.Key);
            var error = Assert.Single(entry.Value.Errors);
            Assert.Equal(expected, error.ErrorMessage);
        }

        [Fact]
        public void ModelStateDictionary_AddsCustomErrorMessage_WhenModelStateSet_WithType()
        {
            // Arrange
            var expected = "Hmm, the value 'some value' is not valid.";
            var dictionary = new ModelStateDictionary();
            dictionary.SetModelValue("key", new string[] { "some value" }, "some value");

            var bindingMetadataProvider = new DefaultBindingMetadataProvider();
            var compositeProvider = new DefaultCompositeMetadataDetailsProvider(new[] { bindingMetadataProvider });
            var optionsAccessor = new OptionsAccessor();
            optionsAccessor.Value.ModelBindingMessageProvider.SetNonPropertyAttemptedValueIsInvalidAccessor(
                (value) => $"Hmm, the value '{ value }' is not valid.");

            var provider = new DefaultModelMetadataProvider(compositeProvider, optionsAccessor);
            var metadata = provider.GetMetadataForType(typeof(int));

            // Act
            dictionary.TryAddModelError("key", new FormatException(), metadata);

            // Assert
            var entry = Assert.Single(dictionary);
            Assert.Equal("key", entry.Key);
            var error = Assert.Single(entry.Value.Errors);
            Assert.Equal(expected, error.ErrorMessage);
        }

        [Fact]
        public void ModelStateDictionary_NoErrorMessage_ForUnrecognizedException()
        {
            // Arrange
            var dictionary = new ModelStateDictionary();
            dictionary.SetModelValue("key", new string[] { "some value" }, "some value");
            var provider = new EmptyModelMetadataProvider();
            var metadata = provider.GetMetadataForProperty(typeof(string), nameof(string.Length));

            // Act
            dictionary.TryAddModelError("key", new InvalidOperationException(), metadata);

            // Assert
            var error = Assert.Single(dictionary["key"].Errors);
            Assert.Empty(error.ErrorMessage);
        }

        [Fact]
        public void TryAddModelException_AddsErrorMessage_ForInputFormatterException()
        {
            // Arrange
            var expectedMessage = "This is an InputFormatterException";
            var dictionary = new ModelStateDictionary();
            var exception = new InputFormatterException(expectedMessage);

            // Act
            dictionary.TryAddModelException("key", exception);

            // Assert
            var entry = Assert.Single(dictionary);
            Assert.Equal("key", entry.Key);
            var error = Assert.Single(entry.Value.Errors);
            Assert.Equal(expectedMessage, error.ErrorMessage);
        }

        [Fact]
        public void TryAddModelException_AddsErrorMessage_ForValueProviderException()
        {
            // Arrange
            var expectedMessage = "This is an ValueProviderException";
            var dictionary = new ModelStateDictionary();
            var exception = new ValueProviderException(expectedMessage);

            // Act
            dictionary.TryAddModelException("key", exception);

            // Assert
            var entry = Assert.Single(dictionary);
            Assert.Equal("key", entry.Key);
            var error = Assert.Single(entry.Value.Errors);
            Assert.Equal(expectedMessage, error.ErrorMessage);
        }

        [Fact]
        public void ModelStateDictionary_AddsErrorMessage_ForInputFormatterException()
        {
            // Arrange
            var expectedMessage = "This is an InputFormatterException";
            var dictionary = new ModelStateDictionary();

            var provider = new EmptyModelMetadataProvider();
            var metadata = provider.GetMetadataForType(typeof(int));

            // Act
            dictionary.TryAddModelError("key", new InputFormatterException(expectedMessage), metadata);

            // Assert
            var entry = Assert.Single(dictionary);
            Assert.Equal("key", entry.Key);
            var error = Assert.Single(entry.Value.Errors);
            Assert.Equal(expectedMessage, error.ErrorMessage);
        }

        [Fact]
        public void ModelStateDictionary_AddsErrorMessage_ForValueProviderException()
        {
            // Arrange
            var expectedMessage = "This is an ValueProviderException";
            var dictionary = new ModelStateDictionary();

            var provider = new EmptyModelMetadataProvider();
            var metadata = provider.GetMetadataForType(typeof(int));

            // Act
            dictionary.TryAddModelError("key", new ValueProviderException(expectedMessage), metadata);

            // Assert
            var entry = Assert.Single(dictionary);
            Assert.Equal("key", entry.Key);
            var error = Assert.Single(entry.Value.Errors);
            Assert.Equal(expectedMessage, error.ErrorMessage);
        }

        [Fact]
        public void ModelStateDictionary_ClearEntriesThatMatchWithKey_NonEmptyKey()
        {
            // Arrange
            var dictionary = new ModelStateDictionary();
            dictionary.MarkFieldValid("Property1");
            dictionary.AddModelError("Property2", "Property2 invalid.");
            dictionary.AddModelError("Property3", "Property invalid.");
            dictionary.MarkFieldSkipped("Property4");

            // Act
            dictionary.ClearValidationState("Property1");
            dictionary.ClearValidationState("Property2");
            dictionary.ClearValidationState("Property4");

            // Assert
            Assert.Empty(dictionary["Property1"].Errors);
            Assert.Equal(ModelValidationState.Unvalidated, dictionary["Property1"].ValidationState);
            Assert.Empty(dictionary["Property2"].Errors);
            Assert.Equal(ModelValidationState.Unvalidated, dictionary["Property2"].ValidationState);
            Assert.Single(dictionary["Property3"].Errors);
            Assert.Equal(ModelValidationState.Invalid, dictionary["Property3"].ValidationState);
            Assert.Empty(dictionary["Property4"].Errors);
            Assert.Equal(ModelValidationState.Unvalidated, dictionary["Property4"].ValidationState);
        }

        [Fact]
        public void ModelStateDictionary_ClearEntriesPrefixedWithKey_NonEmptyKey()
        {
            // Arrange
            var dictionary = new ModelStateDictionary();
            dictionary.MarkFieldValid("Product");
            dictionary.AddModelError("Product.Detail1", "Product Detail1 invalid.");
            dictionary.AddModelError("Product.Detail2[0]", "Product Detail2[0] invalid.");
            dictionary.AddModelError("Product.Detail2[1]", "Product Detail2[1] invalid.");
            dictionary.MarkFieldSkipped("Product.Detail2[2]");
            dictionary.MarkFieldSkipped("Product.Detail3");
            dictionary.AddModelError("ProductName", "ProductName invalid.");

            // Act
            dictionary.ClearValidationState("Product");

            // Assert
            Assert.Empty(dictionary["Product"].Errors);
            Assert.Equal(ModelValidationState.Unvalidated, dictionary["Product"].ValidationState);
            Assert.Empty(dictionary["Product.Detail1"].Errors);
            Assert.Equal(ModelValidationState.Unvalidated, dictionary["Product.Detail1"].ValidationState);
            Assert.Empty(dictionary["Product.Detail2[0]"].Errors);
            Assert.Equal(ModelValidationState.Unvalidated, dictionary["Product.Detail2[0]"].ValidationState);
            Assert.Empty(dictionary["Product.Detail2[1]"].Errors);
            Assert.Equal(ModelValidationState.Unvalidated, dictionary["Product.Detail2[1]"].ValidationState);
            Assert.Empty(dictionary["Product.Detail2[2]"].Errors);
            Assert.Equal(ModelValidationState.Unvalidated, dictionary["Product.Detail2[2]"].ValidationState);
            Assert.Empty(dictionary["Product.Detail3"].Errors);
            Assert.Equal(ModelValidationState.Unvalidated, dictionary["Product.Detail3"].ValidationState);
            Assert.Single(dictionary["ProductName"].Errors);
            Assert.Equal(ModelValidationState.Invalid, dictionary["ProductName"].ValidationState);
        }

        [Fact]
        public void ModelStateDictionary_ClearEntries_KeyHasDot_NonEmptyKey()
        {
            // Arrange
            var dictionary = new ModelStateDictionary();
            dictionary.MarkFieldValid("Product");
            dictionary.AddModelError("Product.Detail1", "Product Detail1 invalid.");
            dictionary.AddModelError("Product.Detail1.Name", "Product Detail1 Name invalid.");
            dictionary.MarkFieldSkipped("Product.Detail1Name");

            // Act
            dictionary.ClearValidationState("Product.Detail1");

            // Assert
            Assert.Equal(ModelValidationState.Valid, dictionary["Product"].ValidationState);
            Assert.Empty(dictionary["Product.Detail1"].Errors);
            Assert.Equal(ModelValidationState.Unvalidated, dictionary["Product.Detail1"].ValidationState);
            Assert.Empty(dictionary["Product.Detail1.Name"].Errors);
            Assert.Equal(ModelValidationState.Unvalidated, dictionary["Product.Detail1.Name"].ValidationState);
            Assert.Equal(ModelValidationState.Skipped, dictionary["Product.Detail1Name"].ValidationState);
        }

        [Theory]
        [InlineData("")]
        [InlineData(null)]
        public void ModelStateDictionary_ClearsAllEntries_EmptyKey(string modelKey)
        {
            // Arrange
            var dictionary = new ModelStateDictionary();
            dictionary.MarkFieldValid("Property1");
            dictionary.AddModelError("Property2", "Property2 invalid.");
            dictionary.AddModelError("Property3", "Property invalid.");
            dictionary.MarkFieldSkipped("Property4");

            // Act
            dictionary.ClearValidationState(modelKey);

            // Assert
            Assert.Empty(dictionary["Property1"].Errors);
            Assert.Equal(ModelValidationState.Unvalidated, dictionary["Property1"].ValidationState);
            Assert.Empty(dictionary["Property2"].Errors);
            Assert.Equal(ModelValidationState.Unvalidated, dictionary["Property2"].ValidationState);
            Assert.Empty(dictionary["Property3"].Errors);
            Assert.Equal(ModelValidationState.Unvalidated, dictionary["Property3"].ValidationState);
            Assert.Empty(dictionary["Property4"].Errors);
            Assert.Equal(ModelValidationState.Unvalidated, dictionary["Property4"].ValidationState);
        }

        [Fact]
        public void GetEnumerable_ReturnsEmptySequenceWhenDictionaryIsEmpty()
        {
            // Arrange
            var dictionary = new ModelStateDictionary();

            // Act & Assert
            Assert.Empty(dictionary);
        }

        [Fact]
        public void GetEnumerable_ReturnsAllNonContainerNodes()
        {
            // Arrange
            var dictionary = new ModelStateDictionary();
            dictionary.MarkFieldValid("Property1");
            dictionary.SetModelValue("Property1.Property2", "value", "value");
            dictionary.AddModelError("Property2", "Property invalid.");
            dictionary.AddModelError("Property2[Property3]", "Property2[Property3] invalid.");
            dictionary.MarkFieldSkipped("Property4");
            dictionary.Remove("Property2");

            // Act & Assert
            Assert.Collection(
                dictionary,
                entry =>
                {
                    Assert.Equal("Property1", entry.Key);
                    Assert.Equal(ModelValidationState.Valid, entry.Value.ValidationState);
                    Assert.Null(entry.Value.RawValue);
                    Assert.Null(entry.Value.AttemptedValue);
                    Assert.Empty(entry.Value.Errors);
                },
                entry =>
                {
                    Assert.Equal("Property4", entry.Key);
                    Assert.Equal(ModelValidationState.Skipped, entry.Value.ValidationState);
                    Assert.Null(entry.Value.RawValue);
                    Assert.Null(entry.Value.AttemptedValue);
                    Assert.Empty(entry.Value.Errors);
                },
                entry =>
                {
                    Assert.Equal("Property1.Property2", entry.Key);
                    Assert.Equal(ModelValidationState.Unvalidated, entry.Value.ValidationState);
                    Assert.Equal("value", entry.Value.RawValue);
                    Assert.Equal("value", entry.Value.AttemptedValue);
                    Assert.Empty(entry.Value.Errors);
                },
                entry =>
                {
                    Assert.Equal("Property2[Property3]", entry.Key);
                    Assert.Equal(ModelValidationState.Invalid, entry.Value.ValidationState);
                    Assert.Null(entry.Value.RawValue);
                    Assert.Null(entry.Value.AttemptedValue);
                    Assert.Collection(entry.Value.Errors,
                        error => Assert.Equal("Property2[Property3] invalid.", error.ErrorMessage));
                });
        }

        [Fact]
        public void GetEnumerable_WorksCorrectlyWhenSiblingIsAPrefix()
        {
            // Arrange
            var modelStateDictionary = new ModelStateDictionary();
            modelStateDictionary.SetModelValue("prop", "value1", "value1");
            modelStateDictionary.SetModelValue("property_name", "value3", "value3");
            modelStateDictionary.SetModelValue("property", "value2", "value2");

            // Act & Assert
            Assert.Collection(modelStateDictionary,
                entry =>
                {
                    Assert.Equal("prop", entry.Key);
                    Assert.Equal("value1", entry.Value.RawValue);
                },
                entry =>
                {
                    Assert.Equal("property", entry.Key);
                    Assert.Equal("value2", entry.Value.RawValue);
                },
                entry =>
                {
                    Assert.Equal("property_name", entry.Key);
                    Assert.Equal("value3", entry.Value.RawValue);
                });
        }

        [Fact]
        public void KeysEnumerable_ReturnsEmptySequenceWhenDictionaryIsEmpty()
        {
            // Arrange
            var dictionary = new ModelStateDictionary();

            // Act
            var keys = dictionary.Keys;

            // Assert
            Assert.Empty(keys);
        }

        [Fact]
        public void KeysEnumerable_ReturnsAllKeys()
        {
            // Arrange
            var expected = new[] { "Property1", "Property4", "Property1.Property2", "Property2[Property3]" };
            var dictionary = new ModelStateDictionary();
            dictionary.MarkFieldValid("Property1");
            dictionary.AddModelError("Property1.Property2", "Property2 invalid.");
            dictionary.AddModelError("Property2", "Property invalid.");
            dictionary.AddModelError("Property2[Property3]", "Property2[Property3] invalid.");
            dictionary.MarkFieldSkipped("Property4");
            dictionary.Remove("Property2");

            // Act
            var keys = dictionary.Keys;

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

        [Fact]
        public void ValuesEnumerable_ReturnsEmptySequenceWhenDictionaryIsEmpty()
        {
            // Arrange
            var dictionary = new ModelStateDictionary();

            // Act
            var values = dictionary.Values;

            // Assert
            Assert.Empty(values);
        }

        [Fact]
        public void ValuesEnumerable_ReturnsAllEntries()
        {
            // Arrange
            var dictionary = new ModelStateDictionary();
            dictionary.MarkFieldValid("Property1");
            dictionary.SetModelValue("Property1.Property2", "value", "value");
            dictionary.AddModelError("Property2", "Property invalid.");
            dictionary.AddModelError("Property2[Property3]", "Property2[Property3] invalid.");
            dictionary.MarkFieldSkipped("Property4");
            dictionary.Remove("Property2");

            // Act & Assert
            Assert.Collection(dictionary.Values,
                value =>
                {
                    Assert.Equal(ModelValidationState.Valid, value.ValidationState);
                    Assert.Null(value.RawValue);
                    Assert.Null(value.AttemptedValue);
                    Assert.Empty(value.Errors);
                },
                value =>
                {
                    Assert.Equal(ModelValidationState.Skipped, value.ValidationState);
                    Assert.Null(value.RawValue);
                    Assert.Null(value.AttemptedValue);
                    Assert.Empty(value.Errors);
                },
                value =>
                {
                    Assert.Equal(ModelValidationState.Unvalidated, value.ValidationState);
                    Assert.Equal("value", value.RawValue);
                    Assert.Equal("value", value.AttemptedValue);
                    Assert.Empty(value.Errors);
                },
                value =>
                {
                    Assert.Equal(ModelValidationState.Invalid, value.ValidationState);
                    Assert.Null(value.RawValue);
                    Assert.Null(value.AttemptedValue);
                    Assert.Collection(
                        value.Errors,
                        error => Assert.Equal("Property2[Property3] invalid.", error.ErrorMessage));
                });
        }

        [Fact]
        public void GetModelStateForProperty_ReturnsModelStateForImmediateChildren()
        {
            // Arrange
            var modelStateDictionary = new ModelStateDictionary();
            modelStateDictionary.SetModelValue("property1", "value1", "value1");
            modelStateDictionary.SetModelValue("property1.property2", "value2", "value2");

            // Act 1
            var property1 = modelStateDictionary.Root.GetModelStateForProperty("property1");
            var property2 = modelStateDictionary.Root.GetModelStateForProperty("property1.property2");

            // Assert 1
            Assert.Equal("value1", property1.RawValue);
            Assert.Null(property2);

            // Act 2
            property2 = property1.GetModelStateForProperty("property2");
            Assert.Equal("value2", property2.RawValue);
        }

        [Fact]
        public void GetModelStateForProperty_ReturnsModelStateForIndexedChildren()
        {
            // Arrange
            var modelStateDictionary = new ModelStateDictionary();
            modelStateDictionary.SetModelValue("[property]", "value1", "value1");

            // Act
            var property = modelStateDictionary.Root.GetModelStateForProperty("[property]");

            // Assert
            Assert.Equal("value1", property.RawValue);
        }


        [Fact]
        public void GetFieldValidationState_ReturnsUnvalidated_IfTreeHeightIsGreaterThanLimit()
        {
            // Arrange
            var stackLimit = 5;
            var dictionary = new ModelStateDictionary();
            var key = string.Join(".", Enumerable.Repeat("foo", stackLimit + 1));
            dictionary.MaxValidationDepth = stackLimit;
            dictionary.MaxStateDepth = null;
            dictionary.MarkFieldValid(key);

            // Act
            var validationState = dictionary.GetFieldValidationState("foo");

            // Assert
            Assert.Equal(ModelValidationState.Unvalidated, validationState);
        }

        [Fact]
        public void IsValidProperty_ReturnsTrue_IfTreeHeightIsGreaterThanLimit()
        {
            // Arrange
            var stackLimit = 5;
            var dictionary = new ModelStateDictionary();
            var key = string.Join(".", Enumerable.Repeat("foo", stackLimit + 1));
            dictionary.MaxValidationDepth = stackLimit;
            dictionary.MaxStateDepth = null;
            dictionary.AddModelError(key, "some error");

            // Act
            var isValid = dictionary.IsValid;
            var validationState = dictionary.ValidationState;

            // Assert
            Assert.True(isValid);
            Assert.Equal(ModelValidationState.Valid, validationState);
        }

        [Fact]
        public void TryAddModelException_Throws_IfKeyHasTooManySegments()
        {
            // Arrange
            var exception = new TestException();

            var stateDepth = 5;
            var dictionary = new ModelStateDictionary();
            var key = string.Join(".", Enumerable.Repeat("foo", stateDepth + 1));
            dictionary.MaxStateDepth = stateDepth;

            // Act
            var invalidException = Assert.Throws<InvalidOperationException>(() => dictionary.TryAddModelException(key, exception));

            // Assert
            Assert.Equal(
                $"The specified key exceeded the maximum ModelState depth: {dictionary.MaxStateDepth}",
                invalidException.Message);
        }

        [Fact]
        public void TryAddModelError_Throws_IfKeyHasTooManySegments()
        {
            // Arrange
            var stateDepth = 5;
            var dictionary = new ModelStateDictionary();
            var key = string.Join(".", Enumerable.Repeat("foo", stateDepth + 1));
            dictionary.MaxStateDepth = stateDepth;

            // Act
            var invalidException = Assert.Throws<InvalidOperationException>(() => dictionary.TryAddModelError(key, "errorMessage"));

            // Assert
            Assert.Equal(
                $"The specified key exceeded the maximum ModelState depth: {dictionary.MaxStateDepth}",
                invalidException.Message);
        }

        [Fact]
        public void SetModelValue_Throws_IfKeyHasTooManySegments()
        {
            var stateDepth = 5;
            var dictionary = new ModelStateDictionary();
            var key = string.Join(".", Enumerable.Repeat("foo", stateDepth + 1));
            dictionary.MaxStateDepth = stateDepth;

            // Act
            var invalidException = Assert.Throws<InvalidOperationException>(() => dictionary.SetModelValue(key, string.Empty, string.Empty));

            // Assert
            Assert.Equal(
                $"The specified key exceeded the maximum ModelState depth: {dictionary.MaxStateDepth}",
                invalidException.Message);
        }

        [Fact]
        public void MarkFieldValid_Throws_IfKeyHasTooManySegments()
        {
            // Arrange
            var stateDepth = 5;
            var source = new ModelStateDictionary();
            var key = string.Join(".", Enumerable.Repeat("foo", stateDepth + 1));
            source.MaxStateDepth = stateDepth;

            // Act
            var exception = Assert.Throws<InvalidOperationException>(() => source.MarkFieldValid(key));

            // Assert
            Assert.Equal(
                $"The specified key exceeded the maximum ModelState depth: {source.MaxStateDepth}",
                exception.Message);
        }

        [Fact]
        public void MarkFieldSkipped_Throws_IfKeyHasTooManySegments()
        {
            // Arrange
            var stateDepth = 5;
            var source = new ModelStateDictionary();
            var key = string.Join(".", Enumerable.Repeat("foo", stateDepth + 1));
            source.MaxStateDepth = stateDepth;

            // Act
            var exception = Assert.Throws<InvalidOperationException>(() => source.MarkFieldSkipped(key));

            // Assert
            Assert.Equal(
                $"The specified key exceeded the maximum ModelState depth: {source.MaxStateDepth}",
                exception.Message);
        }

        [Fact]
        public void Constructor_SetsDefaultRecursionDepth()
        {
            // Arrange && Act
            var dictionary = new ModelStateDictionary();

            // Assert
            Assert.Equal(ModelStateDictionary.DefaultMaxRecursionDepth, dictionary.MaxValidationDepth);
            Assert.Equal(ModelStateDictionary.DefaultMaxRecursionDepth, dictionary.MaxStateDepth);
        }

        [Fact]
        public void CopyConstructor_PreservesRecursionDepth()
        {
            // Arrange
            var dictionary = new ModelStateDictionary();
            dictionary.MaxValidationDepth = 5;
            dictionary.MaxStateDepth = 4;

            // Act
            var newDictionary = new ModelStateDictionary(dictionary);

            // Assert
            Assert.Equal(dictionary.MaxValidationDepth, newDictionary.MaxValidationDepth);
            Assert.Equal(dictionary.MaxStateDepth, newDictionary.MaxStateDepth);
        }

        private class OptionsAccessor : IOptions<MvcOptions>
        {
            public MvcOptions Value { get; } = new MvcOptions();
        }
    }

    internal class TestException : Exception
    {
        public TestException()
        {
            Message = "This is a test exception";
        }

        public override string Message { get; }
    }
}
