// 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.ComponentModel.DataAnnotations;
using System.Linq.Expressions;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.Mvc.ViewFeatures;
using Microsoft.AspNetCore.Testing;
using Xunit;

namespace Microsoft.AspNetCore.Mvc.Rendering
{
    public class HtmlHelperPasswordTest
    {
        public static TheoryData<object> HtmlAttributeData
        {
            get
            {
                return new TheoryData<object>
                {
                    new Dictionary<string, object>
                    {
                        { "name", "-expression-" }, // overridden
                        { "test-key", "test-value" },
                        { "value", "attribute-value" },
                    },
                    new
                    {
                        name = "-expression-", // overridden
                        test_key = "test-value",
                        value = "attribute-value",
                    },
                };
            }
        }

        public static TheoryData<ViewDataDictionary<PasswordModel>, object> PasswordWithViewDataAndAttributesData
        {
            get
            {
                var nullModelViewData = GetViewDataWithNullModelAndNonEmptyViewData();
                var viewData = GetViewDataWithModelStateAndModelAndViewDataValues();
                viewData.Model.Property1 = "does-not-get-used";

                var data = new TheoryData<ViewDataDictionary<PasswordModel>, object>();
                foreach (var items in HtmlAttributeData)
                {
                    data.Add(viewData, items[0]);
                    data.Add(nullModelViewData, items[0]);
                }

                return data;
            }
        }

        [Theory]
        [MemberData(nameof(PasswordWithViewDataAndAttributesData))]
        public void Password_UsesAttributeValueWhenValueArgumentIsNull(
            ViewDataDictionary<PasswordModel> viewData,
            object attributes)
        {
            // Arrange
            var expected = @"<input id=""HtmlEncode[[Property1]]"" name=""HtmlEncode[[Property1]]"" " +
                @"test-key=""HtmlEncode[[test-value]]"" type=""HtmlEncode[[password]]"" " +
                @"value=""HtmlEncode[[attribute-value]]"" />";
            var helper = DefaultTemplatesUtilities.GetHtmlHelper(viewData);

            // Act
            var result = helper.Password("Property1", value: null, htmlAttributes: attributes);

            // Assert
            Assert.Equal(expected, HtmlContentUtilities.HtmlContentToString(result));
        }

        [Theory]
        [MemberData(nameof(PasswordWithViewDataAndAttributesData))]
        public void Password_UsesExplicitValue_IfSpecified(
            ViewDataDictionary<PasswordModel> viewData,
            object attributes)
        {
            // Arrange
            var expected = @"<input id=""HtmlEncode[[Property1]]"" name=""HtmlEncode[[Property1]]"" " +
                @"test-key=""HtmlEncode[[test-value]]"" type=""HtmlEncode[[password]]"" " +
                @"value=""HtmlEncode[[explicit-value]]"" />";
            var helper = DefaultTemplatesUtilities.GetHtmlHelper(viewData);

            // Act
            var result = helper.Password("Property1", "explicit-value", attributes);

            // Assert
            Assert.Equal(expected, HtmlContentUtilities.HtmlContentToString(result));
        }

        [Fact]
        public void PasswordWithPrefix_GeneratesExpectedValue()
        {
            // Arrange
            var expected = @"<input id=""HtmlEncode[[MyPrefix_Property1]]"" name=""HtmlEncode[[MyPrefix.Property1]]"" type=""HtmlEncode[[password]]"" " +
                           @"value=""HtmlEncode[[explicit-value]]"" />";
            var helper = DefaultTemplatesUtilities.GetHtmlHelper(GetViewDataWithModelStateAndModelAndViewDataValues());
            helper.ViewContext.ViewData.TemplateInfo.HtmlFieldPrefix = "MyPrefix";

            // Act
            var result = helper.Password("Property1", "explicit-value", htmlAttributes: null);

            // Assert
            Assert.Equal(expected, HtmlContentUtilities.HtmlContentToString(result));
        }

        [Fact]
        public void PasswordWithPrefix_UsesIdDotReplacementToken()
        {
            // Arrange
            var expected = @"<input id=""HtmlEncode[[MyPrefix$Property1]]"" name=""HtmlEncode[[MyPrefix.Property1]]"" type=""HtmlEncode[[password]]"" " +
                           @"value=""HtmlEncode[[explicit-value]]"" />";
            var helper = DefaultTemplatesUtilities.GetHtmlHelper(
                GetViewDataWithModelStateAndModelAndViewDataValues(),
                idAttributeDotReplacement: "$");
            helper.ViewContext.ViewData.TemplateInfo.HtmlFieldPrefix = "MyPrefix";

            // Act
            var result = helper.Password("Property1", "explicit-value", htmlAttributes: null);

            // Assert
            Assert.Equal(expected, HtmlContentUtilities.HtmlContentToString(result));
        }

        [Fact]
        public void PasswordWithPrefixAndEmptyName_GeneratesExpectedValue()
        {
            // Arrange
            var expected = @"<input id=""HtmlEncode[[MyPrefix]]"" name=""HtmlEncode[[MyPrefix]]"" type=""HtmlEncode[[password]]"" value=""HtmlEncode[[explicit-value]]"" />";
            var helper = DefaultTemplatesUtilities.GetHtmlHelper(GetViewDataWithModelStateAndModelAndViewDataValues());
            helper.ViewContext.ViewData.TemplateInfo.HtmlFieldPrefix = "MyPrefix";
            var name = string.Empty;

            // Act
            var result = helper.Password(name, "explicit-value", htmlAttributes: null);

            // Assert
            Assert.Equal(expected, HtmlContentUtilities.HtmlContentToString(result));
        }

        [Fact]
        public void PasswordWithEmptyNameAndPrefixThrows()
        {
            // Arrange
            var helper = DefaultTemplatesUtilities.GetHtmlHelper("model-value");
            var expression = string.Empty;
            var expectedMessage = "The name of an HTML field cannot be null or empty. Instead use methods " +
                "Microsoft.AspNetCore.Mvc.Rendering.IHtmlHelper.Editor or Microsoft.AspNetCore.Mvc.Rendering." +
                "IHtmlHelper`1.EditorFor with a non-empty htmlFieldName argument value.";

            // Act and Assert
            ExceptionAssert.ThrowsArgument(
                () => helper.Password(expression, value: null, htmlAttributes: null),
                "expression",
                expectedMessage);
        }

        [Fact]
        public void PasswordWithEmptyNameAndPrefix_DoesNotThrow_WithNameAttribute()
        {
            // Arrange
            var expected = @"<input name=""HtmlEncode[[-expression-]]"" type=""HtmlEncode[[password]]"" />";
            var helper = DefaultTemplatesUtilities.GetHtmlHelper("model-value");
            var expression = string.Empty;
            var htmlAttributes = new { name = "-expression-" };

            // Act
            var result = helper.Password(expression, value: null, htmlAttributes: htmlAttributes);

            // Assert
            Assert.Equal(expected, HtmlContentUtilities.HtmlContentToString(result));
        }

        [Fact]
        public void Password_UsesModelStateErrors_ButDoesNotUseModelOrViewDataOrModelStateForValueAttribute()
        {
            // Arrange
            var expected = @"<input class=""HtmlEncode[[some-class input-validation-error]]"" id=""HtmlEncode[[Property1]]""" +
                           @" name=""HtmlEncode[[Property1]]"" test-key=""HtmlEncode[[test-value]]"" type=""HtmlEncode[[password]]"" />";
            var vdd = GetViewDataWithErrors();
            vdd.Model.Property1 = "property-value";
            var helper = DefaultTemplatesUtilities.GetHtmlHelper(vdd);
            var attributes = new Dictionary<string, object>
            {
                { "test-key", "test-value" },
                { "class", "some-class"}
            };

            // Act
            var result = helper.Password("Property1", value: null, htmlAttributes: attributes);

            // Assert
            Assert.Equal(expected, HtmlContentUtilities.HtmlContentToString(result));
        }

        [Fact]
        public void PasswordGeneratesUnobtrusiveValidation()
        {
            // Arrange
            var requiredMessage = ValidationAttributeUtil.GetRequiredErrorMessage("Property2");
            var expected =
                $@"<input data-val=""HtmlEncode[[true]]"" data-val-required=""HtmlEncode[[{requiredMessage}]]"" " +
                @"id=""HtmlEncode[[Property2]]"" name=""HtmlEncode[[Property2]]"" type=""HtmlEncode[[password]]"" />";
            var helper = DefaultTemplatesUtilities.GetHtmlHelper(GetViewDataWithModelStateAndModelAndViewDataValues());

            // Act
            var result = helper.Password("Property2", value: null, htmlAttributes: null);

            // Assert
            Assert.Equal(expected, HtmlContentUtilities.HtmlContentToString(result));
        }

        public static IEnumerable<object[]> PasswordWithComplexExpressions_UsesIdDotSeparatorData
        {
            get
            {
                yield return new object[]
                {
                    "Property4.Property5",
                    @"<input data-test=""HtmlEncode[[val]]"" id=""HtmlEncode[[Property4$$Property5]]"" name=""HtmlEncode[[Property4.Property5]]"" " +
                    @"type=""HtmlEncode[[password]]"" />",
                };

                yield return new object[]
               {
                    "Property4.Property6[0]",
                    @"<input data-test=""HtmlEncode[[val]]"" id=""HtmlEncode[[Property4$$Property6$$0$$]]"" name=""HtmlEncode[[Property4.Property6[0]]]"" " +
                    @"type=""HtmlEncode[[password]]"" />",
               };
            }
        }

        [Theory]
        [MemberData(nameof(PasswordWithComplexExpressions_UsesIdDotSeparatorData))]
        public void PasswordWithComplexExpressions_UsesIdDotSeparator(string expression, string expected)
        {
            // Arrange
            var viewData = GetViewDataWithModelStateAndModelAndViewDataValues();
            var helper = DefaultTemplatesUtilities.GetHtmlHelper(viewData, idAttributeDotReplacement: "$$");
            var attributes = new Dictionary<string, object> { { "data-test", "val" } };

            // Act
            var result = helper.Password(expression, value: null, htmlAttributes: attributes);

            // Assert
            Assert.Equal(expected, HtmlContentUtilities.HtmlContentToString(result));
        }

        [Theory]
        [MemberData(nameof(HtmlAttributeData))]
        public void PasswordForWithAttributes_GeneratesExpectedValue(object htmlAttributes)
        {
            // Arrange
            var expected = @"<input id=""HtmlEncode[[Property1]]"" name=""HtmlEncode[[Property1]]"" " +
                @"test-key=""HtmlEncode[[test-value]]"" type=""HtmlEncode[[password]]"" " +
                @"value=""HtmlEncode[[attribute-value]]"" />";
            var helper = DefaultTemplatesUtilities.GetHtmlHelper(GetViewDataWithModelStateAndModelAndViewDataValues());
            helper.ViewData.Model.Property1 = "test";

            // Act
            var result = helper.PasswordFor(m => m.Property1, htmlAttributes);

            // Assert
            Assert.Equal(expected, HtmlContentUtilities.HtmlContentToString(result));
        }

        [Fact]
        public void PasswordForWithPrefix_GeneratesExpectedValue()
        {
            // Arrange
            var expected = @"<input id=""HtmlEncode[[MyPrefix_Property1]]"" name=""HtmlEncode[[MyPrefix.Property1]]"" type=""HtmlEncode[[password]]"" />";
            var helper = DefaultTemplatesUtilities.GetHtmlHelper(GetViewDataWithModelStateAndModelAndViewDataValues());
            helper.ViewContext.ViewData.TemplateInfo.HtmlFieldPrefix = "MyPrefix";

            // Act
            var result = helper.PasswordFor(m => m.Property1, htmlAttributes: null);

            // Assert
            Assert.Equal(expected, HtmlContentUtilities.HtmlContentToString(result));
        }

        [Fact]
        public void PasswordFor_UsesModelStateErrors_ButDoesNotUseModelOrViewDataOrModelStateForValueAttribute()
        {
            // Arrange
            var expected = @"<input baz=""HtmlEncode[[BazValue]]"" class=""HtmlEncode[[some-class input-validation-error]]"" id=""HtmlEncode[[Property1]]"" " +
                           @"name=""HtmlEncode[[Property1]]"" type=""HtmlEncode[[password]]"" />";
            var vdd = GetViewDataWithErrors();
            vdd.Model.Property1 = "prop1-value";
            var helper = DefaultTemplatesUtilities.GetHtmlHelper(vdd);
            var attributes = new Dictionary<string, object>
            {
                { "baz", "BazValue" },
                { "class", "some-class"}
            };

            // Act
            var result = helper.PasswordFor(m => m.Property1, attributes);

            // Assert
            Assert.Equal(expected, HtmlContentUtilities.HtmlContentToString(result));
        }

        [Fact]
        public void PasswordFor_GeneratesUnobtrusiveValidationAttributes()
        {
            // Arrange
            var requiredMessage = ValidationAttributeUtil.GetRequiredErrorMessage("Property2");
            var expected =
                $@"<input data-val=""HtmlEncode[[true]]"" data-val-required=""HtmlEncode[[{requiredMessage}]]"" " +
                @"id=""HtmlEncode[[Property2]]"" name=""HtmlEncode[[Property2]]"" type=""HtmlEncode[[password]]"" />";
            var helper = DefaultTemplatesUtilities.GetHtmlHelper(GetViewDataWithErrors());

            // Act
            var result = helper.PasswordFor(m => m.Property2, htmlAttributes: null);

            // Assert
            Assert.Equal(expected, HtmlContentUtilities.HtmlContentToString(result));
        }

        public static TheoryData PasswordFor_WithComplexExpressionsData
        {
            get
            {
                return new TheoryData<Expression<Func<PasswordModel, string>>, string>
                {
                    {
                        model => model.Property3["key"],
                        @"<input data-val=""HtmlEncode[[true]]"" id=""HtmlEncode[[pre_Property3_key_]]"" name=""HtmlEncode[[pre.Property3[key]]]"" " +
                        @"type=""HtmlEncode[[password]]"" value=""HtmlEncode[[attr-value]]"" />"
                    },
                    {
                        model => model.Property4.Property5,
                        @"<input data-val=""HtmlEncode[[true]]"" id=""HtmlEncode[[pre_Property4_Property5]]"" name=""HtmlEncode[[pre.Property4.Property5]]"" " +
                        @"type=""HtmlEncode[[password]]"" value=""HtmlEncode[[attr-value]]"" />"
                    },
                    {
                        model => model.Property4.Property6[0],
                        @"<input data-val=""HtmlEncode[[true]]"" id=""HtmlEncode[[pre_Property4_Property6_0_]]"" " +
                        @"name=""HtmlEncode[[pre.Property4.Property6[0]]]"" type=""HtmlEncode[[password]]"" value=""HtmlEncode[[attr-value]]"" />"
                    }
                };
            }
        }

        [Theory]
        [MemberData(nameof(PasswordFor_WithComplexExpressionsData))]
        public void PasswordFor_WithComplexExpressionsAndFieldPrefix_UsesAttributeValueIfSpecified(
            Expression<Func<PasswordModel, string>> expression,
            string expected)
        {
            // Arrange
            var viewData = GetViewDataWithModelStateAndModelAndViewDataValues();
            viewData.ModelState.SetModelValue("pre.Property3[key]", "Property3Val", "Property3Val");
            viewData.ModelState.SetModelValue("pre.Property4.Property5", "Property5Val", "Property5Val");
            viewData.ModelState.SetModelValue("pre.Property4.Property6[0]", "Property6Val", "Property6Val");
            viewData["pre.Property3[key]"] = "vdd-value1";
            viewData["pre.Property4.Property5"] = "vdd-value2";
            viewData["pre.Property4.Property6[0]"] = "vdd-value3";
            viewData.Model.Property3["key"] = "prop-value1";
            viewData.Model.Property4.Property5 = "prop-value2";
            viewData.Model.Property4.Property6.Add("prop-value3");

            var helper = DefaultTemplatesUtilities.GetHtmlHelper(viewData);
            viewData.TemplateInfo.HtmlFieldPrefix = "pre";
            var attributes = new { data_val = "true", value = "attr-value" };

            // Act
            var result = helper.PasswordFor(expression, attributes);

            // Assert
            Assert.Equal(expected, HtmlContentUtilities.HtmlContentToString(result));
        }

        [Fact]
        public void Password_UsesSpecifiedExpressionForNames_IgnoresExpressionValue()
        {
            // Arrange
            var metadataProvider = new EmptyModelMetadataProvider();
            var helper = DefaultTemplatesUtilities.GetHtmlHelper(new ViewDataDictionary<TestModel>(metadataProvider));
            helper.ViewContext.ClientValidationEnabled = false;
            helper.ViewData.Model = new TestModel { Property1 = "propValue" };

            // Act
            var passwordResult = helper.Password("Property1");

            // Assert
            Assert.Equal(
                "<input id=\"HtmlEncode[[Property1]]\" name=\"HtmlEncode[[Property1]]\" type=\"HtmlEncode[[password]]\" />",
                HtmlContentUtilities.HtmlContentToString(passwordResult));
        }

        [Fact]
        public void PasswordFor_UsesSpecifiedExpressionForNames_IgnoresExpressionValue()
        {
            // Arrange
            var metadataProvider = new EmptyModelMetadataProvider();
            var helper = DefaultTemplatesUtilities.GetHtmlHelper(new ViewDataDictionary<TestModel>(metadataProvider));
            helper.ViewContext.ClientValidationEnabled = false;
            helper.ViewData.Model = new TestModel { Property1 = "propValue" };

            // Act
            var passwordForResult = helper.PasswordFor(m => m.Property1);

            // Assert
            Assert.Equal(
                "<input id=\"HtmlEncode[[Property1]]\" name=\"HtmlEncode[[Property1]]\" type=\"HtmlEncode[[password]]\" />",
                HtmlContentUtilities.HtmlContentToString(passwordForResult));
        }

        [Fact]
        public void Password_UsesSpecifiedValue()
        {
            // Arrange
            var metadataProvider = new EmptyModelMetadataProvider();
            var helper = DefaultTemplatesUtilities.GetHtmlHelper(new ViewDataDictionary<TestModel>(metadataProvider));
            helper.ViewContext.ClientValidationEnabled = false;
            helper.ViewData.Model = new TestModel { Property1 = "propValue" };

            // Act
            var passwordResult = helper.Password("Property1", value: "myvalue");

            // Assert
            Assert.Equal(
                "<input id=\"HtmlEncode[[Property1]]\" name=\"HtmlEncode[[Property1]]\" type=\"HtmlEncode[[password]]\" value=\"HtmlEncode[[myvalue]]\" />",
                HtmlContentUtilities.HtmlContentToString(passwordResult));
        }

        [Fact]
        public void PasswordFor_GeneratesPlaceholderAttribute_WhenDisplayAttributePromptIsSet()
        {
            // Arrange
            var expected = @"<input id=""HtmlEncode[[Property7]]"" name=""HtmlEncode[[Property7]]"" placeholder=""HtmlEncode[[placeholder]]"" type=""HtmlEncode[[password]]"" />";
            var model = new PasswordModel();
            var helper = DefaultTemplatesUtilities.GetHtmlHelper(model);

            // Act
            var result = helper.PasswordFor(m => m.Property7, htmlAttributes: null);

            // Assert
            Assert.Equal(expected, HtmlContentUtilities.HtmlContentToString(result));
        }

        private static ViewDataDictionary<PasswordModel> GetViewDataWithNullModelAndNonEmptyViewData()
        {
            return new ViewDataDictionary<PasswordModel>(new EmptyModelMetadataProvider())
            {
                ["Property1"] = "view-data-val",
            };
        }

        private static ViewDataDictionary<PasswordModel> GetViewDataWithModelStateAndModelAndViewDataValues()
        {
            var viewData = GetViewDataWithNullModelAndNonEmptyViewData();
            viewData.Model = new PasswordModel();
            viewData.ModelState.SetModelValue("Property1", "ModelStateValue", "ModelStateValue");

            return viewData;
        }

        private static ViewDataDictionary<PasswordModel> GetViewDataWithErrors()
        {
            var viewData = GetViewDataWithModelStateAndModelAndViewDataValues();
            viewData.ModelState.AddModelError("Property1", "error 1");
            viewData.ModelState.AddModelError("Property1", "error 2");
            return viewData;
        }

        public static TheoryData PasswordFor_IgnoresExpressionValueForComplexExpressionsData
        {
            get
            {
                return new TheoryData<Expression<Func<PasswordModel, string>>, string>
                {
                    {
                        model => model.Property3["key"],
                        @"<input id=""HtmlEncode[[pre_Property3_key_]]"" name=""HtmlEncode[[pre.Property3[key]]]"" " +
                        @"type=""HtmlEncode[[password]]"" />"
                    },
                    {
                        model => model.Property4.Property5,
                        @"<input id=""HtmlEncode[[pre_Property4_Property5]]"" name=""HtmlEncode[[pre.Property4.Property5]]"" " +
                        @"type=""HtmlEncode[[password]]"" />"
                    },
                    {
                        model => model.Property4.Property6[0],
                        @"<input id=""HtmlEncode[[pre_Property4_Property6_0_]]"" " +
                        @"name=""HtmlEncode[[pre.Property4.Property6[0]]]"" type=""HtmlEncode[[password]]"" />"
                    }
                };
            }
        }

        [Theory]
        [MemberData(nameof(PasswordFor_IgnoresExpressionValueForComplexExpressionsData))]
        public void PasswordFor_ComplexExpressions_IgnoresValueFromViewDataModelStateAndModel(
            Expression<Func<PasswordModel, string>> expression,
            string expected)
        {
            // Arrange
            var model = new PasswordModel();
            var helper = DefaultTemplatesUtilities.GetHtmlHelper(model);
            helper.ViewData.TemplateInfo.HtmlFieldPrefix = "pre";

            helper.ViewData.ModelState.SetModelValue("pre.Property3[key]", "MProp3Val", "MProp3Val");
            helper.ViewData.ModelState.SetModelValue("pre.Property4.Property5", "MProp5Val", "MProp5Val");
            helper.ViewData.ModelState.SetModelValue("pre.Property4.Property6[0]", "MProp6Val", "MProp6Val");

            helper.ViewData["pre.Property3[key]"] = "VDProp3Val";
            helper.ViewData["pre.Property4.Property5"] = "VDProp5Val";
            helper.ViewData["pre.Property4.Property6"] = "VDProp6Val";

            helper.ViewData.Model.Property3["key"] = "Prop3Val";
            helper.ViewData.Model.Property4.Property5 = "Prop5Val";
            helper.ViewData.Model.Property4.Property6.Add("Prop6Val");

            // Act
            var result = helper.PasswordFor(expression);

            // Assert
            Assert.Equal(expected, HtmlContentUtilities.HtmlContentToString(result));
        }

        public class PasswordModel
        {
            public string Property1 { get; set; }

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

            public Dictionary<string, string> Property3 { get; } = new Dictionary<string, string>();

            public NestedClass Property4 { get; } = new NestedClass();

            [Display(Prompt = "placeholder")]
            public string Property7 { get; set; }
        }

        public class NestedClass
        {
            public string Property5 { get; set; }

            public List<string> Property6 { get; } = new List<string>();
        }

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