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

using System.ComponentModel.DataAnnotations;
using System.Linq.Expressions;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.Mvc.ViewFeatures;
using Microsoft.AspNetCore.Testing;

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; }
    }
}
