// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Reflection;
using Microsoft.AspNetCore.Mvc.ModelBinding.Metadata;
using Microsoft.AspNetCore.Mvc.ModelBinding.Validation;
using Xunit;

namespace Microsoft.AspNetCore.Mvc.ModelBinding
{
    public class ModelMetadataTest
    {
        // IsComplexType
        private readonly struct IsComplexTypeModel
        {
        }

        [Theory]
        [InlineData(typeof(string))]
        [InlineData(typeof(Nullable<int>))]
        [InlineData(typeof(int))]
        public void IsComplexType_ReturnsFalseForSimpleTypes(Type type)
        {
            // Arrange & Act
            var modelMetadata = new TestModelMetadata(type);

            // Assert
            Assert.False(modelMetadata.IsComplexType);
        }

        [Theory]
        [InlineData(typeof(object))]
        [InlineData(typeof(IDisposable))]
        [InlineData(typeof(IsComplexTypeModel))]
        [InlineData(typeof(Nullable<IsComplexTypeModel>))]
        public void IsComplexType_ReturnsTrueForComplexTypes(Type type)
        {
            // Arrange & Act
            var modelMetadata = new TestModelMetadata(type);

            // Assert
            Assert.True(modelMetadata.IsComplexType);
        }

        // IsCollectionType / IsEnumerableType

        private class NonCollectionType
        {
        }

        private class DerivedList : List<int>
        {
        }

        private class JustEnumerable : IEnumerable
        {
            public IEnumerator GetEnumerator()
            {
                throw new NotImplementedException();
            }
        }

        public static TheoryData<Type> NonCollectionNonEnumerableData
        {
            get
            {
                return new TheoryData<Type>
                {
                    typeof(object),
                    typeof(int),
                    typeof(NonCollectionType),
                    typeof(string),
                };
            }
        }

        public static TheoryData<Type> CollectionAndEnumerableData
        {
            get
            {
                return new TheoryData<Type>
                {
                    typeof(int[]),
                    typeof(List<string>),
                    typeof(DerivedList),
                    typeof(Collection<int>),
                    typeof(Dictionary<object, object>),
                    typeof(CollectionImplementation),
                };
            }
        }

        [Theory]
        [MemberData(nameof(NonCollectionNonEnumerableData))]
        [InlineData(typeof(IEnumerable))]
        [InlineData(typeof(IEnumerable<string>))]
        [InlineData(typeof(JustEnumerable))]
        public void IsCollectionType_ReturnsFalseForNonCollectionTypes(Type type)
        {
            // Arrange & Act
            var modelMetadata = new TestModelMetadata(type);

            // Assert
            Assert.False(modelMetadata.IsCollectionType);
        }

        [Theory]
        [MemberData(nameof(CollectionAndEnumerableData))]
        public void IsCollectionType_ReturnsTrueForCollectionTypes(Type type)
        {
            // Arrange & Act
            var modelMetadata = new TestModelMetadata(type);

            // Assert
            Assert.True(modelMetadata.IsCollectionType);
        }

        [Theory]
        [MemberData(nameof(NonCollectionNonEnumerableData))]
        public void IsEnumerableType_ReturnsFalseForNonEnumerableTypes(Type type)
        {
            // Arrange & Act
            var modelMetadata = new TestModelMetadata(type);

            // Assert
            Assert.False(modelMetadata.IsEnumerableType);
        }

        [Theory]
        [MemberData(nameof(CollectionAndEnumerableData))]
        [InlineData(typeof(IEnumerable))]
        [InlineData(typeof(IEnumerable<string>))]
        [InlineData(typeof(JustEnumerable))]
        public void IsEnumerableType_ReturnsTrueForEnumerableTypes(Type type)
        {
            // Arrange & Act
            var modelMetadata = new TestModelMetadata(type);

            // Assert
            Assert.True(modelMetadata.IsEnumerableType);
        }

        // IsNullableValueType

        [Theory]
        [InlineData(typeof(string), false)]
        [InlineData(typeof(IDisposable), false)]
        [InlineData(typeof(Nullable<int>), true)]
        [InlineData(typeof(int), false)]
        [InlineData(typeof(DerivedList), false)]
        [InlineData(typeof(IsComplexTypeModel), false)]
        [InlineData(typeof(Nullable<IsComplexTypeModel>), true)]
        public void IsNullableValueType_ReturnsExpectedValue(Type modelType, bool expected)
        {
            // Arrange & Act
            var modelMetadata = new TestModelMetadata(modelType);

            // Assert
            Assert.Equal(expected, modelMetadata.IsNullableValueType);
        }

        // IsReferenceOrNullableType

        [Theory]
        [InlineData(typeof(string), true)]
        [InlineData(typeof(IDisposable), true)]
        [InlineData(typeof(Nullable<int>), true)]
        [InlineData(typeof(int), false)]
        [InlineData(typeof(DerivedList), true)]
        [InlineData(typeof(IsComplexTypeModel), false)]
        [InlineData(typeof(Nullable<IsComplexTypeModel>), true)]
        public void IsReferenceOrNullableType_ReturnsExpectedValue(Type modelType, bool expected)
        {
            // Arrange & Act
            var modelMetadata = new TestModelMetadata(modelType);

            // Assert
            Assert.Equal(expected, modelMetadata.IsReferenceOrNullableType);
        }

        // UnderlyingOrModelType

        [Theory]
        [InlineData(typeof(string), typeof(string))]
        [InlineData(typeof(IDisposable), typeof(IDisposable))]
        [InlineData(typeof(Nullable<int>), typeof(int))]
        [InlineData(typeof(int), typeof(int))]
        [InlineData(typeof(DerivedList), typeof(DerivedList))]
        [InlineData(typeof(IsComplexTypeModel), typeof(IsComplexTypeModel))]
        [InlineData(typeof(Nullable<IsComplexTypeModel>), typeof(IsComplexTypeModel))]
        public void UnderlyingOrModelType_ReturnsExpectedValue(Type modelType, Type expected)
        {
            // Arrange & Act
            var modelMetadata = new TestModelMetadata(modelType);

            // Assert
            Assert.Equal(expected, modelMetadata.UnderlyingOrModelType);
        }

        // ElementType

        [Theory]
        [InlineData(typeof(object))]
        [InlineData(typeof(int))]
        [InlineData(typeof(NonCollectionType))]
        [InlineData(typeof(string))]
        public void ElementType_ReturnsNull_ForNonCollections(Type modelType)
        {
            // Arrange
            var metadata = new TestModelMetadata(modelType);

            // Act
            var elementType = metadata.ElementType;

            // Assert
            Assert.Null(elementType);
        }

        [Theory]
        [InlineData(typeof(int[]), typeof(int))]
        [InlineData(typeof(List<string>), typeof(string))]
        [InlineData(typeof(DerivedList), typeof(int))]
        [InlineData(typeof(IEnumerable), typeof(object))]
        [InlineData(typeof(IEnumerable<string>), typeof(string))]
        [InlineData(typeof(Collection<int>), typeof(int))]
        [InlineData(typeof(Dictionary<object, object>), typeof(KeyValuePair<object, object>))]
        [InlineData(typeof(DerivedDictionary), typeof(KeyValuePair<string, int>))]
        public void ElementType_ReturnsExpectedMetadata(Type modelType, Type expected)
        {
            // Arrange
            var metadata = new TestModelMetadata(modelType);

            // Act
            var elementType = metadata.ElementType;

            // Assert
            Assert.NotNull(elementType);
            Assert.Equal(expected, elementType);
        }

        // ContainerType

        [Fact]
        public void ContainerType_IsNull_ForType()
        {
            // Arrange & Act
            var metadata = new TestModelMetadata(typeof(int));

            // Assert
            Assert.Null(metadata.ContainerType);
        }

        [Fact]
        public void ContainerType_IsNull_ForParameter()
        {
            // Arrange & Act
            var method = typeof(CollectionImplementation).GetMethod(nameof(CollectionImplementation.Add));
            var parameter = method.GetParameters()[0]; // Add(string item)
            var metadata = new TestModelMetadata(parameter);

            // Assert
            Assert.Null(metadata.ContainerType);
        }

        [Fact]
        public void ContainerType_ReturnExpectedMetadata_ForProperty()
        {
            // Arrange & Act
            var property = typeof(string).GetProperty(nameof(string.Length));
            var metadata = new TestModelMetadata(property, typeof(int), typeof(string));

            // Assert
            Assert.Equal(typeof(string), metadata.ContainerType);
        }

        // Name / ParameterName / PropertyName

        [Fact]
        public void Names_ReturnExpectedMetadata_ForType()
        {
            // Arrange & Act
            var metadata = new TestModelMetadata(typeof(int));

            // Assert
            Assert.Null(metadata.Name);
            Assert.Null(metadata.ParameterName);
            Assert.Null(metadata.PropertyName);
        }

        [Fact]
        public void Names_ReturnExpectedMetadata_ForParameter()
        {
            // Arrange & Act
            var method = typeof(CollectionImplementation).GetMethod(nameof(CollectionImplementation.Add));
            var parameter = method.GetParameters()[0]; // Add(string item)
            var metadata = new TestModelMetadata(parameter);

            // Assert
            Assert.Equal("item", metadata.Name);
            Assert.Equal("item", metadata.ParameterName);
            Assert.Null(metadata.PropertyName);
        }

        [Fact]
        public void Names_ReturnExpectedMetadata_ForProperty()
        {
            // Arrange & Act
            var property = typeof(string).GetProperty(nameof(string.Length));
            var metadata = new TestModelMetadata(property, typeof(int), typeof(string));

            // Assert
            Assert.Equal(nameof(string.Length), metadata.Name);
            Assert.Null(metadata.ParameterName);
            Assert.Equal(nameof(string.Length), metadata.PropertyName);
        }

        // GetDisplayName()

        [Fact]
        public void GetDisplayName_ReturnsDisplayName_IfSet()
        {
            // Arrange
            var property = typeof(string).GetProperty(nameof(string.Length));
            var metadata = new TestModelMetadata(property, typeof(int), typeof(string));
            metadata.SetDisplayName("displayName");

            // Act
            var result = metadata.GetDisplayName();

            // Assert
            Assert.Equal("displayName", result);
        }

        [Fact]
        public void GetDisplayName_ReturnsParameterName_WhenSetAndDisplayNameIsNull()
        {
            // Arrange
            var method = typeof(CollectionImplementation).GetMethod(nameof(CollectionImplementation.Add));
            var parameter = method.GetParameters()[0]; // Add(string item)
            var metadata = new TestModelMetadata(parameter);

            // Act
            var result = metadata.GetDisplayName();

            // Assert
            Assert.Equal("item", result);
        }

        [Fact]
        public void GetDisplayName_ReturnsPropertyName_WhenSetAndDisplayNameIsNull()
        {
            // Arrange
            var property = typeof(string).GetProperty(nameof(string.Length));
            var metadata = new TestModelMetadata(property, typeof(int), typeof(string));

            // Act
            var result = metadata.GetDisplayName();

            // Assert
            Assert.Equal("Length", result);
        }

        [Fact]
        public void GetDisplayName_ReturnsTypeName_WhenPropertyNameAndDisplayNameAreNull()
        {
            // Arrange
            var metadata = new TestModelMetadata(typeof(string));

            // Act
            var result = metadata.GetDisplayName();

            // Assert
            Assert.Equal("String", result);
        }

        // Virtual methods and properties that throw NotImplementedException in the abstract class.

        [Fact]
        public void GetContainerMetadata_ThrowsNotImplementedException_ByDefault()
        {
            // Arrange
            var metadata = new TestModelMetadata(typeof(DerivedList));

            // Act & Assert
            Assert.Throws<NotImplementedException>(() => metadata.ContainerMetadata);
        }

        [Fact]
        public void GetMetadataForType_ByDefaultThrows_NotImplementedException()
        {
            // Arrange
            var metadata = new TestModelMetadata(typeof(string));

            // Act & Assert
            var result = Assert.Throws<NotImplementedException>(() => metadata.GetMetadataForType(typeof(string)));
        }

        [Fact]
        public void GetMetadataForProperties_ByDefaultThrows_NotImplementedException()
        {
            // Arrange
            var metadata = new TestModelMetadata(typeof(string));

            // Act & Assert
            var result = Assert.Throws<NotImplementedException>(() => metadata.GetMetadataForProperties(typeof(string)));
        }

        private class TestModelMetadata : ModelMetadata
        {
            private string _displayName;

            public TestModelMetadata(Type modelType)
                : base(ModelMetadataIdentity.ForType(modelType))
            {
            }

            public TestModelMetadata(ParameterInfo parameter)
                : base(ModelMetadataIdentity.ForParameter(parameter))
            {
            }

            public TestModelMetadata(PropertyInfo propertyInfo, Type modelType, Type containerType)
                : base(ModelMetadataIdentity.ForProperty(propertyInfo, modelType, containerType))
            {
            }

            public override IReadOnlyDictionary<object, object> AdditionalValues
            {
                get
                {
                    throw new NotImplementedException();
                }
            }

            public override string BinderModelName
            {
                get
                {
                    throw new NotImplementedException();
                }
            }

            public override Type BinderType
            {
                get
                {
                    throw new NotImplementedException();
                }
            }

            public override BindingSource BindingSource
            {
                get
                {
                    throw new NotImplementedException();
                }
            }

            public override bool ConvertEmptyStringToNull
            {
                get
                {
                    throw new NotImplementedException();
                }
            }

            public override string DataTypeName
            {
                get
                {
                    throw new NotImplementedException();
                }
            }

            public override string Description
            {
                get
                {
                    throw new NotImplementedException();
                }
            }

            public override string DisplayFormatString
            {
                get
                {
                    throw new NotImplementedException();
                }
            }

            public override string DisplayName
            {
                get
                {
                    return _displayName;
                }
            }

            public void SetDisplayName(string displayName)
            {
                _displayName = displayName;
            }

            public override string EditFormatString
            {
                get
                {
                    throw new NotImplementedException();
                }
            }

            public override ModelMetadata ElementMetadata
            {
                get
                {
                    throw new NotImplementedException();
                }
            }

            public override IEnumerable<KeyValuePair<EnumGroupAndName, string>> EnumGroupedDisplayNamesAndValues
            {
                get
                {
                    throw new NotImplementedException();
                }
            }

            public override IReadOnlyDictionary<string, string> EnumNamesAndValues
            {
                get
                {
                    throw new NotImplementedException();
                }
            }

            public override bool HasNonDefaultEditFormat
            {
                get
                {
                    throw new NotImplementedException();
                }
            }

            public override bool HideSurroundingHtml
            {
                get
                {
                    throw new NotImplementedException();
                }
            }

            public override bool HtmlEncode
            {
                get
                {
                    throw new NotImplementedException();
                }
            }

            public override bool IsBindingAllowed
            {
                get
                {
                    throw new NotImplementedException();
                }
            }

            public override bool IsBindingRequired
            {
                get
                {
                    throw new NotImplementedException();
                }
            }

            public override bool IsEnum
            {
                get
                {
                    throw new NotImplementedException();
                }
            }

            public override bool IsFlagsEnum
            {
                get
                {
                    throw new NotImplementedException();
                }
            }

            public override bool IsReadOnly
            {
                get
                {
                    throw new NotImplementedException();
                }
            }

            public override bool IsRequired
            {
                get
                {
                    throw new NotImplementedException();
                }
            }

            public override ModelBindingMessageProvider ModelBindingMessageProvider
            {
                get
                {
                    throw new NotImplementedException();
                }
            }

            public override string NullDisplayText
            {
                get
                {
                    throw new NotImplementedException();
                }
            }

            public override int Order
            {
                get
                {
                    throw new NotImplementedException();
                }
            }

            public override string Placeholder
            {
                get
                {
                    throw new NotImplementedException();
                }
            }

            public override ModelPropertyCollection Properties
            {
                get
                {
                    throw new NotImplementedException();
                }
            }

            public override IPropertyFilterProvider PropertyFilterProvider
            {
                get
                {
                    throw new NotImplementedException();
                }
            }

            public override bool ShowForDisplay
            {
                get
                {
                    throw new NotImplementedException();
                }
            }

            public override bool ShowForEdit
            {
                get
                {
                    throw new NotImplementedException();
                }
            }

            public override string SimpleDisplayProperty
            {
                get
                {
                    throw new NotImplementedException();
                }
            }

            public override string TemplateHint
            {
                get
                {
                    throw new NotImplementedException();
                }
            }

            public override IPropertyValidationFilter PropertyValidationFilter
            {
                get
                {
                    throw new NotImplementedException();
                }
            }

            public override bool ValidateChildren
            {
                get
                {
                    throw new NotImplementedException();
                }
            }

            public override IReadOnlyList<object> ValidatorMetadata
            {
                get
                {
                    throw new NotImplementedException();
                }
            }

            public override Func<object, object> PropertyGetter
            {
                get
                {
                    throw new NotImplementedException();
                }
            }

            public override Action<object, object> PropertySetter
            {
                get
                {
                    throw new NotImplementedException();
                }
            }
        }

        private class CollectionImplementation : ICollection<string>
        {
            public int Count
            {
                get
                {
                    throw new NotImplementedException();
                }
            }

            public bool IsReadOnly
            {
                get
                {
                    throw new NotImplementedException();
                }
            }

            public void Add(string item)
            {
                throw new NotImplementedException();
            }

            public void Clear()
            {
                throw new NotImplementedException();
            }

            public bool Contains(string item)
            {
                throw new NotImplementedException();
            }

            public void CopyTo(string[] array, int arrayIndex)
            {
                throw new NotImplementedException();
            }

            public IEnumerator<string> GetEnumerator()
            {
                throw new NotImplementedException();
            }

            public bool Remove(string item)
            {
                throw new NotImplementedException();
            }

            IEnumerator IEnumerable.GetEnumerator()
            {
                throw new NotImplementedException();
            }
        }

        private class DerivedDictionary : Dictionary<string, int>
        {
        }
    }
}
