// 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 System.Collections.Generic;
using System.IO;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.Abstractions;
using Microsoft.AspNetCore.Mvc.DataAnnotations;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.Mvc.ModelBinding.Validation;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.AspNetCore.Mvc.ViewEngines;
using Microsoft.AspNetCore.Routing;
using Microsoft.AspNetCore.Testing;
using Microsoft.Extensions.Options;
using Moq;
using Xunit;

namespace Microsoft.AspNetCore.Mvc.ViewFeatures
{
    public class DefaultValidationHtmlAttributeProviderTest
    {
        [Fact]
        [ReplaceCulture]
        public void AddValidationAttributes_AddsAttributes()
        {
            // Arrange
            var expectedMessage = $"The field {nameof(Model.HasValidatorsProperty)} must be a number.";
            var metadataProvider = new EmptyModelMetadataProvider();
            var attributeProvider = GetAttributeProvider(metadataProvider);
            var viewContext = GetViewContext<Model>(model: null, metadataProvider: metadataProvider);
            var attributes = new SortedDictionary<string, string>(StringComparer.Ordinal);
            var modelExplorer = metadataProvider
                .GetModelExplorerForType(typeof(Model), model: null)
                .GetExplorerForProperty(nameof(Model.HasValidatorsProperty));

            // Act
            attributeProvider.AddValidationAttributes(
                viewContext,
                modelExplorer,
                attributes);

            // Assert
            Assert.Collection(
                attributes,
                kvp =>
                {
                    Assert.Equal("data-val", kvp.Key);
                    Assert.Equal("true", kvp.Value);
                },
                kvp =>
                {
                    Assert.Equal("data-val-number", kvp.Key);
                    Assert.Equal(expectedMessage, kvp.Value);
                });
        }

        [Fact]
        [ReplaceCulture]
        public void AddAndTrackValidationAttributes_AddsAttributes()
        {
            // Arrange
            var expectedMessage = $"The field {nameof(Model.HasValidatorsProperty)} must be a number.";
            var metadataProvider = new EmptyModelMetadataProvider();
            var attributeProvider = GetAttributeProvider(metadataProvider);
            var viewContext = GetViewContext<Model>(model: null, metadataProvider: metadataProvider);
            var attributes = new SortedDictionary<string, string>(StringComparer.Ordinal);
            var modelExplorer = metadataProvider
                .GetModelExplorerForType(typeof(Model), model: null)
                .GetExplorerForProperty(nameof(Model.HasValidatorsProperty));

            // Act
            attributeProvider.AddAndTrackValidationAttributes(
                viewContext,
                modelExplorer,
                nameof(Model.HasValidatorsProperty),
                attributes);

            // Assert
            Assert.Collection(
                attributes,
                kvp =>
                {
                    Assert.Equal("data-val", kvp.Key);
                    Assert.Equal("true", kvp.Value);
                },
                kvp =>
                {
                    Assert.Equal("data-val-number", kvp.Key);
                    Assert.Equal(expectedMessage, kvp.Value);
                });
        }

        [Fact]
        public void AddValidationAttributes_AddsNothing_IfClientSideValidationDisabled()
        {
            // Arrange
            var metadataProvider = new EmptyModelMetadataProvider();
            var attributeProvider = GetAttributeProvider(metadataProvider);
            var viewContext = GetViewContext<Model>(model: null, metadataProvider: metadataProvider);
            viewContext.ClientValidationEnabled = false;

            var attributes = new SortedDictionary<string, string>(StringComparer.Ordinal);
            var modelExplorer = metadataProvider
                .GetModelExplorerForType(typeof(Model), model: null)
                .GetExplorerForProperty(nameof(Model.HasValidatorsProperty));

            // Act
            attributeProvider.AddValidationAttributes(
                viewContext,
                modelExplorer,
                attributes);

            // Assert
            Assert.Empty(attributes);
        }

        [Fact]
        public void AddAndTrackValidationAttributes_DoesNotCallAddMethod_IfClientSideValidationDisabled()
        {
            // Arrange
            var metadataProvider = new EmptyModelMetadataProvider();
            var viewContext = GetViewContext<Model>(model: null, metadataProvider: metadataProvider);
            viewContext.ClientValidationEnabled = false;

            var attributes = new SortedDictionary<string, string>(StringComparer.Ordinal);
            var modelExplorer = metadataProvider
                .GetModelExplorerForType(typeof(Model), model: null)
                .GetExplorerForProperty(nameof(Model.HasValidatorsProperty));

            var attributeProviderMock = new Mock<ValidationHtmlAttributeProvider>() { CallBase = true };
            attributeProviderMock
                .Setup(p => p.AddValidationAttributes(
                    It.IsAny<ViewContext>(),
                    It.IsAny<ModelExplorer>(),
                    It.IsAny<IDictionary<string, string>>()))
                .Verifiable();
            var attributeProvider = attributeProviderMock.Object;

            // Act
            attributeProvider.AddAndTrackValidationAttributes(
                viewContext,
                modelExplorer,
                nameof(Model.HasValidatorsProperty),
                attributes);

            // Assert
            Assert.Empty(attributes);
            attributeProviderMock.Verify(
                p => p.AddValidationAttributes(
                    It.IsAny<ViewContext>(),
                    It.IsAny<ModelExplorer>(),
                    It.IsAny<IDictionary<string, string>>()),
                Times.Never);
        }

        [Fact]
        public void AddValidationAttributes_AddsAttributes_EvenIfPropertyAlreadyRendered()
        {
            // Arrange
            var expectedMessage = $"The field {nameof(Model.HasValidatorsProperty)} must be a number.";
            var metadataProvider = new EmptyModelMetadataProvider();
            var attributeProvider = GetAttributeProvider(metadataProvider);
            var viewContext = GetViewContext<Model>(model: null, metadataProvider: metadataProvider);
            viewContext.FormContext.RenderedField(nameof(Model.HasValidatorsProperty), value: true);

            var attributes = new SortedDictionary<string, string>(StringComparer.Ordinal);
            var modelExplorer = metadataProvider
                .GetModelExplorerForType(typeof(Model), model: null)
                .GetExplorerForProperty(nameof(Model.HasValidatorsProperty));

            // Act
            attributeProvider.AddValidationAttributes(
                viewContext,
                modelExplorer,
                attributes);

            // Assert
            Assert.Collection(
                attributes,
                kvp =>
                {
                    Assert.Equal("data-val", kvp.Key);
                    Assert.Equal("true", kvp.Value);
                },
                kvp =>
                {
                    Assert.Equal("data-val-number", kvp.Key);
                    Assert.Equal(expectedMessage, kvp.Value);
                });
        }

        [Fact]
        public void AddAndTrackValidationAttributes_DoesNotCallAddMethod_IfPropertyAlreadyRendered()
        {
            // Arrange
            var metadataProvider = new EmptyModelMetadataProvider();
            var viewContext = GetViewContext<Model>(model: null, metadataProvider: metadataProvider);
            viewContext.FormContext.RenderedField(nameof(Model.HasValidatorsProperty), value: true);

            var attributes = new SortedDictionary<string, string>(StringComparer.Ordinal);
            var modelExplorer = metadataProvider
                .GetModelExplorerForType(typeof(Model), model: null)
                .GetExplorerForProperty(nameof(Model.HasValidatorsProperty));

            var attributeProviderMock = new Mock<ValidationHtmlAttributeProvider>() { CallBase = true };
            attributeProviderMock
                .Setup(p => p.AddValidationAttributes(
                    It.IsAny<ViewContext>(),
                    It.IsAny<ModelExplorer>(),
                    It.IsAny<IDictionary<string, string>>()))
                .Verifiable();
            var attributeProvider = attributeProviderMock.Object;

            // Act
            attributeProvider.AddAndTrackValidationAttributes(
                viewContext,
                modelExplorer,
                nameof(Model.HasValidatorsProperty),
                attributes);

            // Assert
            Assert.Empty(attributes);
            attributeProviderMock.Verify(
                p => p.AddValidationAttributes(
                    It.IsAny<ViewContext>(),
                    It.IsAny<ModelExplorer>(),
                    It.IsAny<IDictionary<string, string>>()),
                Times.Never);
        }

        [Fact]
        public void AddValidationAttributes_AddsNothing_IfPropertyHasNoValidators()
        {
            // Arrange
            var metadataProvider = new EmptyModelMetadataProvider();
            var attributeProvider = GetAttributeProvider(metadataProvider);
            var viewContext = GetViewContext<Model>(model: null, metadataProvider: metadataProvider);
            var attributes = new SortedDictionary<string, string>(StringComparer.Ordinal);
            var modelExplorer = metadataProvider
                .GetModelExplorerForType(typeof(Model), model: null)
                .GetExplorerForProperty(nameof(Model.Property));

            // Act
            attributeProvider.AddValidationAttributes(
                viewContext,
                modelExplorer,
                attributes);

            // Assert
            Assert.Empty(attributes);
        }

        private static ViewContext GetViewContext<TModel>(TModel model, IModelMetadataProvider metadataProvider)
        {
            var actionContext = new ActionContext(new DefaultHttpContext(), new RouteData(), new ActionDescriptor());
            var viewData = new ViewDataDictionary<TModel>(metadataProvider, actionContext.ModelState)
            {
                Model = model,
            };

            return new ViewContext(
                actionContext,
                Mock.Of<IView>(),
                viewData,
                Mock.Of<ITempDataDictionary>(),
                TextWriter.Null,
                new HtmlHelperOptions());
        }

        private static ValidationHtmlAttributeProvider GetAttributeProvider(IModelMetadataProvider metadataProvider)
        {
            // Add validation properties for float, double and decimal properties. Ignore everything else.
            var mvcViewOptions = new MvcViewOptions();
            mvcViewOptions.ClientModelValidatorProviders.Add(new NumericClientModelValidatorProvider());

            var mvcViewOptionsAccessor = new Mock<IOptions<MvcViewOptions>>();
            mvcViewOptionsAccessor.SetupGet(accessor => accessor.Value).Returns(mvcViewOptions);

            return new DefaultValidationHtmlAttributeProvider(
                mvcViewOptionsAccessor.Object,
                metadataProvider,
                new ClientValidatorCache());
        }

        private class Model
        {
            public double HasValidatorsProperty { get; set; }

            public string Property { get; set; }
        }
    }
}
