// 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.Generic;
using System.ComponentModel.DataAnnotations;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Text;
using System.Text.Encodings.Web;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Html;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.AspNetCore.Mvc.ViewEngines;
using Microsoft.AspNetCore.Testing;
using Moq;
using Xunit;

namespace Microsoft.AspNetCore.Mvc.ViewFeatures
{
    public class DefaultEditorTemplatesTest
    {
        // Mappings from templateName to expected result when using StubbyHtmlHelper.
        public static TheoryData<string, string> TemplateNameData
        {
            get
            {
                return new TheoryData<string, string>
                {
                    { null, "__TextBox__ class='text-box single-line'" },
                    { string.Empty, "__TextBox__ class='text-box single-line'" },
                    { "EmailAddress", "__TextBox__ class='text-box single-line' type='email'" },
                    { "emailaddress", "__TextBox__ class='text-box single-line' type='email'" },
                    { "HiddenInput", "HtmlEncode[[True]]__Hidden__" }, // Hidden also generates value by default.
                    { "HIDDENINPUT", "HtmlEncode[[True]]__Hidden__" },
                    { "MultilineText", "__TextArea__ class='text-box multi-line'" },
                    { "multilinetext", "__TextArea__ class='text-box multi-line'" },
                    { "Password", "__Password__ class='text-box single-line password'" },
                    { "PASSWORD", "__Password__ class='text-box single-line password'" },
                    { "PhoneNumber", "__TextBox__ class='text-box single-line' type='tel'" },
                    { "phonenumber", "__TextBox__ class='text-box single-line' type='tel'" },
                    { "Text", "__TextBox__ class='text-box single-line'" },
                    { "TEXT", "__TextBox__ class='text-box single-line'" },
                    { "Url", "__TextBox__ class='text-box single-line' type='url'" },
                    { "url", "__TextBox__ class='text-box single-line' type='url'" },
                    { "Date", "__TextBox__ class='text-box single-line' type='date'" },
                    { "DATE", "__TextBox__ class='text-box single-line' type='date'" },
                    { "DateTime", "__TextBox__ class='text-box single-line' type='datetime-local'" },
                    { "datetime", "__TextBox__ class='text-box single-line' type='datetime-local'" },
                    { "DateTime-local", "__TextBox__ class='text-box single-line' type='datetime-local'" },
                    { "DATETIME-LOCAL", "__TextBox__ class='text-box single-line' type='datetime-local'" },
                    { "datetimeoffset", "__TextBox__ class='text-box single-line' type='text'" },
                    { "DateTimeOffset", "__TextBox__ class='text-box single-line' type='text'" },
                    { "Time", "__TextBox__ class='text-box single-line' type='time'" },
                    { "time", "__TextBox__ class='text-box single-line' type='time'" },
                    { "Month", "__TextBox__ class='text-box single-line' type='month'" },
                    { "month", "__TextBox__ class='text-box single-line' type='month'" },
                    { "Week", "__TextBox__ class='text-box single-line' type='week'" },
                    { "week", "__TextBox__ class='text-box single-line' type='week'" },
                    { "Byte", "__TextBox__ class='text-box single-line' type='number'" },
                    { "BYTE", "__TextBox__ class='text-box single-line' type='number'" },
                    { "SByte", "__TextBox__ class='text-box single-line' type='number'" },
                    { "sbyte", "__TextBox__ class='text-box single-line' type='number'" },
                    { "Int16", "__TextBox__ class='text-box single-line' type='number'" },
                    { "INT16", "__TextBox__ class='text-box single-line' type='number'" },
                    { "UInt16", "__TextBox__ class='text-box single-line' type='number'" },
                    { "uint16", "__TextBox__ class='text-box single-line' type='number'" },
                    { "Int32", "__TextBox__ class='text-box single-line' type='number'" },
                    { "INT32", "__TextBox__ class='text-box single-line' type='number'" },
                    { "UInt32", "__TextBox__ class='text-box single-line' type='number'" },
                    { "uint32", "__TextBox__ class='text-box single-line' type='number'" },
                    { "Int64", "__TextBox__ class='text-box single-line' type='number'" },
                    { "INT64", "__TextBox__ class='text-box single-line' type='number'" },
                    { "UInt64", "__TextBox__ class='text-box single-line' type='number'" },
                    { "uint64", "__TextBox__ class='text-box single-line' type='number'" },
                    { "Single", "__TextBox__ class='text-box single-line'" },
                    { "SINGLE", "__TextBox__ class='text-box single-line'" },
                    { "Double", "__TextBox__ class='text-box single-line'" },
                    { "double", "__TextBox__ class='text-box single-line'" },
                    { "Boolean", "__CheckBox__ class='check-box'" }, // Not tri-state b/c string is not a Nullable type.
                    { "BOOLEAN", "__CheckBox__ class='check-box'" },
                    { "Decimal", "__TextBox__ class='text-box single-line'" },
                    { "decimal", "__TextBox__ class='text-box single-line'" },
                    { "String", "__TextBox__ class='text-box single-line'" },
                    { "STRING", "__TextBox__ class='text-box single-line'" },
                    { typeof(IFormFile).Name, "__TextBox__ class='text-box single-line' type='file'" },
                    { TemplateRenderer.IEnumerableOfIFormFileName,
                        "__TextBox__ class='text-box single-line' type='file' multiple='multiple'" },
                };
            }
        }

        // label's IHtmlContent -> expected label text
        public static TheoryData<IHtmlContent, string> ObjectTemplate_ChecksWriteTo_NotToStringData
        {
            get
            {
                // Similar to HtmlString.Empty today.
                var noopContentWithEmptyToString = new Mock<IHtmlContent>(MockBehavior.Strict);
                noopContentWithEmptyToString
                    .Setup(c => c.ToString())
                    .Returns(string.Empty);
                noopContentWithEmptyToString.Setup(c => c.WriteTo(It.IsAny<TextWriter>(), It.IsAny<HtmlEncoder>()));

                // Similar to an empty StringHtmlContent today.
                var noopContentWithNonEmptyToString = new Mock<IHtmlContent>(MockBehavior.Strict);
                noopContentWithNonEmptyToString
                    .Setup(c => c.ToString())
                    .Returns(typeof(StringHtmlContent).FullName);
                noopContentWithNonEmptyToString.Setup(c => c.WriteTo(It.IsAny<TextWriter>(), It.IsAny<HtmlEncoder>()));

                // Makes noop calls on the TextWriter.
                var busyNoopContentWithNonEmptyToString = new Mock<IHtmlContent>(MockBehavior.Strict);
                busyNoopContentWithNonEmptyToString
                    .Setup(c => c.ToString())
                    .Returns(typeof(StringHtmlContent).FullName);
                busyNoopContentWithNonEmptyToString
                    .Setup(c => c.WriteTo(It.IsAny<TextWriter>(), It.IsAny<HtmlEncoder>()))
                    .Callback<TextWriter, HtmlEncoder>((writer, encoder) =>
                    {
                        writer.Write(string.Empty);
                        writer.Write(new char[0]);
                        writer.Write((char[])null);
                        writer.Write((object)null);
                        writer.Write((string)null);
                        writer.Write(format: "{0}", arg0: null);
                        writer.Write(new char[] { 'a', 'b', 'c' }, index: 1, count: 0);
                    });

                // Unrealistic but covers all the bases.
                var writingContentWithEmptyToString = new Mock<IHtmlContent>(MockBehavior.Strict);
                writingContentWithEmptyToString
                    .Setup(c => c.ToString())
                    .Returns(string.Empty);
                writingContentWithEmptyToString
                    .Setup(c => c.WriteTo(It.IsAny<TextWriter>(), It.IsAny<HtmlEncoder>()))
                    .Callback<TextWriter, HtmlEncoder>((writer, encoder) => writer.Write("Some string"));

                // Similar to TagBuilder today.
                var writingContentWithNonEmptyToString = new Mock<IHtmlContent>(MockBehavior.Strict);
                writingContentWithNonEmptyToString
                    .Setup(c => c.ToString())
                    .Returns(typeof(TagBuilder).FullName);
                writingContentWithNonEmptyToString
                    .Setup(c => c.WriteTo(It.IsAny<TextWriter>(), It.IsAny<HtmlEncoder>()))
                    .Callback<TextWriter, HtmlEncoder>((writer, encoder) => writer.Write("Some string"));

                // label's IHtmlContent -> expected label text
                return new TheoryData<IHtmlContent, string>
                {
                    // Types HtmlHelper actually uses.
                    { HtmlString.Empty, string.Empty },
                    {
                        new TagBuilder("label"),
                        "<div class=\"HtmlEncode[[editor-label]]\"><label></label></div>" + Environment.NewLine
                    },

                    // Another IHtmlContent implementation that does not override ToString().
                    { new StringHtmlContent(string.Empty), string.Empty },

                    // Mocks
                    { noopContentWithEmptyToString.Object, string.Empty },
                    { noopContentWithNonEmptyToString.Object, string.Empty },
                    { busyNoopContentWithNonEmptyToString.Object, string.Empty },
                    {
                        writingContentWithEmptyToString.Object,
                        "<div class=\"HtmlEncode[[editor-label]]\">Some string</div>" + Environment.NewLine
                    },
                    {
                        writingContentWithNonEmptyToString.Object,
                        "<div class=\"HtmlEncode[[editor-label]]\">Some string</div>" + Environment.NewLine
                    },
                };
            }
        }

        [Fact]
        public void ObjectTemplateEditsSimplePropertiesOnObjectByDefault()
        {
            // Arrange
            var expected =
                "<div class=\"HtmlEncode[[editor-label]]\"><label for=\"HtmlEncode[[Property1]]\">HtmlEncode[[Property1]]</label></div>" +
                Environment.NewLine +
                "<div class=\"HtmlEncode[[editor-field]]\">Model = p1, ModelType = System.String, PropertyName = Property1, SimpleDisplayText = p1 " +
                "<span class=\"HtmlEncode[[field-validation-valid]]\" data-valmsg-for=\"HtmlEncode[[Property1]]\" data-valmsg-replace=\"HtmlEncode[[true]]\">" +
                "</span></div>" +
                Environment.NewLine +
                "<div class=\"HtmlEncode[[editor-label]]\"><label for=\"HtmlEncode[[Property2]]\">HtmlEncode[[Prop2]]</label></div>" +
                Environment.NewLine +
                "<div class=\"HtmlEncode[[editor-field]]\">Model = (null), ModelType = System.String, PropertyName = Property2, SimpleDisplayText = (null) " +
                "<span class=\"HtmlEncode[[field-validation-valid]]\" data-valmsg-for=\"HtmlEncode[[Property2]]\" data-valmsg-replace=\"HtmlEncode[[true]]\">" +
                "</span></div>" +
                Environment.NewLine;

            var model = new DefaultTemplatesUtilities.ObjectTemplateModel { Property1 = "p1", Property2 = null };
            var html = DefaultTemplatesUtilities.GetHtmlHelper(model);

            // Act
            var result = DefaultEditorTemplates.ObjectTemplate(html);

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

        // Prior to aspnet/Mvc#6638 fix, helper did not generate Property1 <label> or containing <div> with this setup.
        // Expect almost the same HTML as in ObjectTemplateEditsSimplePropertiesOnObjectByDefault(). Only difference is
        // the <div class="editor-label">...</div> is not present for Property1.
        [Fact]
        public void ObjectTemplateSkipsLabel_IfDisplayNameIsEmpty()
        {
            // Arrange
            var expected =
                "<div class=\"HtmlEncode[[editor-label]]\"><label for=\"HtmlEncode[[Property1]]\"></label></div>" +
                Environment.NewLine +
                "<div class=\"HtmlEncode[[editor-field]]\">Model = p1, ModelType = System.String, PropertyName = Property1, SimpleDisplayText = p1 " +
                "<span class=\"HtmlEncode[[field-validation-valid]]\" data-valmsg-for=\"HtmlEncode[[Property1]]\" data-valmsg-replace=\"HtmlEncode[[true]]\">" +
                "</span></div>" +
                Environment.NewLine +
                "<div class=\"HtmlEncode[[editor-label]]\"><label for=\"HtmlEncode[[Property2]]\">HtmlEncode[[Prop2]]</label></div>" +
                Environment.NewLine +
                "<div class=\"HtmlEncode[[editor-field]]\">Model = (null), ModelType = System.String, PropertyName = Property2, SimpleDisplayText = (null) " +
                "<span class=\"HtmlEncode[[field-validation-valid]]\" data-valmsg-for=\"HtmlEncode[[Property2]]\" data-valmsg-replace=\"HtmlEncode[[true]]\">" +
                "</span></div>" +
                Environment.NewLine;

            var provider = new TestModelMetadataProvider();
            provider
                .ForProperty<DefaultTemplatesUtilities.ObjectTemplateModel>(
                    nameof(DefaultTemplatesUtilities.ObjectTemplateModel.Property1))
                .DisplayDetails(dd => dd.DisplayName = () => string.Empty);

            var model = new DefaultTemplatesUtilities.ObjectTemplateModel { Property1 = "p1", Property2 = null };
            var html = DefaultTemplatesUtilities.GetHtmlHelper(model, provider);

            // Act
            var result = DefaultEditorTemplates.ObjectTemplate(html);

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

        [Theory]
        [MemberData(nameof(ObjectTemplate_ChecksWriteTo_NotToStringData))]
        public void ObjectTemplate_ChecksWriteTo_NotToString(IHtmlContent labelContent, string expectedLabel)
        {
            // Arrange
            var expected =
                expectedLabel +
                "<div class=\"HtmlEncode[[editor-field]]\">Model = (null), ModelType = System.String, PropertyName = Property1, SimpleDisplayText = (null) " +
                "</div>" +
                Environment.NewLine +
                expectedLabel +
                "<div class=\"HtmlEncode[[editor-field]]\">Model = (null), ModelType = System.String, PropertyName = Property2, SimpleDisplayText = (null) " +
                "</div>" +
                Environment.NewLine;

            var helperToCopy = DefaultTemplatesUtilities.GetHtmlHelper();
            var helperMock = new Mock<IHtmlHelper>(MockBehavior.Strict);
            helperMock.SetupGet(h => h.ViewContext).Returns(helperToCopy.ViewContext);
            helperMock.SetupGet(h => h.ViewData).Returns(helperToCopy.ViewData);
            helperMock
                .Setup(h => h.Label(
                    It.Is<string>(s => string.Equals("Property1", s, StringComparison.Ordinal) ||
                        string.Equals("Property2", s, StringComparison.Ordinal)),
                    null,   // labelText
                    null))  // htmlAttributes
                .Returns(labelContent);
            helperMock
                .Setup(h => h.ValidationMessage(
                    It.Is<string>(s => string.Equals("Property1", s, StringComparison.Ordinal) ||
                        string.Equals("Property2", s, StringComparison.Ordinal)),
                    null,   // message
                    null,   // htmlAttributes
                    null))  // tag
                .Returns(HtmlString.Empty);

            // Act
            var result = DefaultEditorTemplates.ObjectTemplate(helperMock.Object);

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

        [Fact]
        public void ObjectTemplateDisplaysNullDisplayTextWithNullModelAndTemplateDepthGreaterThanOne()
        {
            // Arrange
            var provider = new TestModelMetadataProvider();
            provider.ForType<DefaultTemplatesUtilities.ObjectTemplateModel>().DisplayDetails(dd =>
            {
                dd.NullDisplayText = "Null Display Text";
                dd.SimpleDisplayProperty = "Property1";
            });

            var html = DefaultTemplatesUtilities.GetHtmlHelper(provider: provider);

            html.ViewData.TemplateInfo.AddVisited("foo");
            html.ViewData.TemplateInfo.AddVisited("bar");

            // Act
            var result = DefaultEditorTemplates.ObjectTemplate(html);

            // Assert
            Assert.Equal(html.ViewData.ModelMetadata.NullDisplayText, HtmlContentUtilities.HtmlContentToString(result));
        }

        [Theory]
        [MemberData(nameof(DefaultDisplayTemplatesTest.HtmlEncodeData), MemberType = typeof(DefaultDisplayTemplatesTest))]
        public void ObjectTemplateDisplaysSimpleDisplayTextWithNonNullModelTemplateDepthGreaterThanOne(
            string simpleDisplayText,
            bool htmlEncode,
            string expectedResult)
        {
            // Arrange
            var model = new DefaultTemplatesUtilities.ObjectTemplateModel()
            {
                Property1 = simpleDisplayText,
            };

            var provider = new TestModelMetadataProvider();
            provider.ForType<DefaultTemplatesUtilities.ObjectTemplateModel>().DisplayDetails(dd =>
            {
                dd.HtmlEncode = htmlEncode;
                dd.NullDisplayText = "Null Display Text";
                dd.SimpleDisplayProperty = "Property1";
            });

            var html = DefaultTemplatesUtilities.GetHtmlHelper(model, provider: provider);

            html.ViewData.TemplateInfo.AddVisited("foo");
            html.ViewData.TemplateInfo.AddVisited("bar");

            // Act
            var result = DefaultEditorTemplates.ObjectTemplate(html);

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

        [Fact]
        public void ObjectTemplate_IgnoresPropertiesWith_ScaffoldColumnFalse()
        {
            // Arrange
            var expected =
@"<div class=""HtmlEncode[[editor-label]]""><label for=""HtmlEncode[[Property1]]"">HtmlEncode[[Property1]]</label></div>" +
Environment.NewLine +
@"<div class=""HtmlEncode[[editor-field]]""><input class=""HtmlEncode[[text-box single-line]]"" id=""HtmlEncode[[Property1]]"" name=""HtmlEncode[[Property1]]"" type=""HtmlEncode[[text]]"" value="""" /> " +
@"<span class=""HtmlEncode[[field-validation-valid]]"" data-valmsg-for=""HtmlEncode[[Property1]]"" data-valmsg-replace=""HtmlEncode[[true]]""></span></div>" +
Environment.NewLine +
@"<div class=""HtmlEncode[[editor-label]]""><label for=""HtmlEncode[[Property3]]"">HtmlEncode[[Property3]]</label></div>" +
Environment.NewLine +
@"<div class=""HtmlEncode[[editor-field]]""><input class=""HtmlEncode[[text-box single-line]]"" id=""HtmlEncode[[Property3]]"" name=""HtmlEncode[[Property3]]"" type=""HtmlEncode[[text]]"" value="""" /> " +
@"<span class=""HtmlEncode[[field-validation-valid]]"" data-valmsg-for=""HtmlEncode[[Property3]]"" data-valmsg-replace=""HtmlEncode[[true]]""></span></div>" +
Environment.NewLine;

            var model = new DefaultTemplatesUtilities.ObjectWithScaffoldColumn();
            var viewEngine = new Mock<ICompositeViewEngine>(MockBehavior.Strict);
            viewEngine
                .Setup(v => v.GetView(/*executingFilePath*/ null, It.IsAny<string>(), /*isMainPage*/ false))
                .Returns(ViewEngineResult.NotFound(string.Empty, Enumerable.Empty<string>()));
            viewEngine
                .Setup(v => v.FindView(It.IsAny<ActionContext>(), It.IsAny<string>(), /*isMainPage*/ false))
                .Returns(ViewEngineResult.NotFound("", Enumerable.Empty<string>()));
            var htmlHelper = DefaultTemplatesUtilities.GetHtmlHelper(model, viewEngine.Object);

            // Act
            var result = DefaultEditorTemplates.ObjectTemplate(htmlHelper);

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

        [Fact]
        public void ObjectTemplate_HonorsHideSurroundingHtml()
        {
            // Arrange
            var expected =
                "Model = p1, ModelType = System.String, PropertyName = Property1, SimpleDisplayText = p1" +
                "<div class=\"HtmlEncode[[editor-label]]\"><label for=\"HtmlEncode[[Property2]]\">HtmlEncode[[Prop2]]</label></div>" +
                Environment.NewLine +
                "<div class=\"HtmlEncode[[editor-field]]\">" +
                    "Model = (null), ModelType = System.String, PropertyName = Property2, SimpleDisplayText = (null) " +
                    "<span class=\"HtmlEncode[[field-validation-valid]]\" data-valmsg-for=\"HtmlEncode[[Property2]]\" data-valmsg-replace=\"HtmlEncode[[true]]\">" +
                    "</span></div>" +
                Environment.NewLine;

            var provider = new TestModelMetadataProvider();
            provider.ForProperty<DefaultTemplatesUtilities.ObjectTemplateModel>("Property1").DisplayDetails(dd =>
            {
                dd.HideSurroundingHtml = true;
            });

            var model = new DefaultTemplatesUtilities.ObjectTemplateModel { Property1 = "p1", Property2 = null };
            var html = DefaultTemplatesUtilities.GetHtmlHelper(model, provider: provider);

            // Act
            var result = DefaultEditorTemplates.ObjectTemplate(html);

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

        [Fact]
        public void ObjectTemplate_OrdersProperties_AsExpected()
        {
            // Arrange
            var model = new OrderedModel();
            var html = DefaultTemplatesUtilities.GetHtmlHelper(model);
            var expectedProperties = new List<string>
            {
                "OrderedProperty3",
                "OrderedProperty2",
                "OrderedProperty1",
                "Property3",
                "Property1",
                "Property2",
                "LastProperty",
            };

            var stringBuilder = new StringBuilder();
            foreach (var property in expectedProperties)
            {
                var label = string.Format(
                    CultureInfo.InvariantCulture,
                    "<div class=\"HtmlEncode[[editor-label]]\"><label for=\"HtmlEncode[[{0}]]\">HtmlEncode[[{0}]]</label></div>",
                    property);
                stringBuilder.AppendLine(label);

                var value = string.Format(
                    CultureInfo.InvariantCulture,
                    "<div class=\"HtmlEncode[[editor-field]]\">Model = (null), ModelType = System.String, PropertyName = {0}, " +
                    "SimpleDisplayText = (null) " +
                    "<span class=\"HtmlEncode[[field-validation-valid]]\" data-valmsg-for=\"HtmlEncode[[{0}]]\" data-valmsg-replace=\"HtmlEncode[[true]]\">" +
                    "</span></div>",
                    property);
                stringBuilder.AppendLine(value);
            }
            var expected = stringBuilder.ToString();

            // Act
            var result = DefaultEditorTemplates.ObjectTemplate(html);

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

        [Fact]
        public void HiddenInputTemplate_ReturnsValueAndHiddenInput()
        {
            // Arrange
            var expected =
                "HtmlEncode[[Formatted string]]<input id=\"HtmlEncode[[FieldPrefix]]\" name=\"HtmlEncode[[FieldPrefix]]\" type=\"HtmlEncode[[hidden]]\" value=\"HtmlEncode[[Model string]]\" />";

            var model = "Model string";
            var html = DefaultTemplatesUtilities.GetHtmlHelper(model);
            var templateInfo = html.ViewData.TemplateInfo;
            templateInfo.HtmlFieldPrefix = "FieldPrefix";

            // TemplateBuilder sets FormattedModelValue before calling TemplateRenderer and it's used below.
            templateInfo.FormattedModelValue = "Formatted string";

            // Act
            var result = DefaultEditorTemplates.HiddenInputTemplate(html);

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

        [Fact]
        public void HiddenInputTemplate_HonorsHideSurroundingHtml()
        {
            // Arrange
            var expected = "<input id=\"HtmlEncode[[FieldPrefix]]\" name=\"HtmlEncode[[FieldPrefix]]\" type=\"HtmlEncode[[hidden]]\" value=\"HtmlEncode[[Model string]]\" />";

            var model = "Model string";

            var provider = new TestModelMetadataProvider();
            provider.ForType<string>().DisplayDetails(dd =>
            {
                dd.HideSurroundingHtml = true;
            });

            var html = DefaultTemplatesUtilities.GetHtmlHelper(model, provider: provider);

            var templateInfo = html.ViewData.TemplateInfo;
            templateInfo.HtmlFieldPrefix = "FieldPrefix";
            templateInfo.FormattedModelValue = "Formatted string";

            // Act
            var result = DefaultEditorTemplates.HiddenInputTemplate(html);

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

        [Fact]
        public void MultilineTextTemplate_ReturnsTextArea()
        {
            // Arrange
            var expected =
                "<textarea class=\"HtmlEncode[[text-box multi-line]]\" id=\"HtmlEncode[[FieldPrefix]]\" name=\"HtmlEncode[[FieldPrefix]]\">" +
                Environment.NewLine +
                "HtmlEncode[[Formatted string]]</textarea>";

            var model = "Model string";
            var html = DefaultTemplatesUtilities.GetHtmlHelper(model);
            var templateInfo = html.ViewData.TemplateInfo;
            templateInfo.HtmlFieldPrefix = "FieldPrefix";

            // TemplateBuilder sets FormattedModelValue before calling TemplateRenderer and it's used below.
            templateInfo.FormattedModelValue = "Formatted string";

            // Act
            var result = DefaultEditorTemplates.MultilineTemplate(html);

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

        [Fact]
        public void PasswordTemplate_ReturnsInputElement_IgnoresValues()
        {
            // Arrange
            var expected = "<input class=\"HtmlEncode[[text-box single-line password]]\" " +
                "id=\"HtmlEncode[[FieldPrefix]]\" name=\"HtmlEncode[[FieldPrefix]]\" " +
                "type=\"HtmlEncode[[password]]\" />";

            // Template ignores Model.
            var model = "Model string";

            var helper = DefaultTemplatesUtilities.GetHtmlHelper(model);
            var viewData = helper.ViewData;
            var templateInfo = viewData.TemplateInfo;
            templateInfo.HtmlFieldPrefix = "FieldPrefix";

            // Template ignores FormattedModelValue, ModelState and ViewData.
            templateInfo.FormattedModelValue = "Formatted string";
            viewData.ModelState.SetModelValue("FieldPrefix", "Raw model string", "Attempted model string");
            viewData["FieldPrefix"] = "ViewData string";

            // Act
            var result = DefaultEditorTemplates.PasswordTemplate(helper);

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

        [Fact]
        public void PasswordTemplate_ReturnsInputElement_UsesHtmlAttributes()
        {
            // Arrange
            var expected = "<input class=\"HtmlEncode[[super text-box single-line password]]\" " +
                "id=\"HtmlEncode[[FieldPrefix]]\" name=\"HtmlEncode[[FieldPrefix]]\" " +
                "type=\"HtmlEncode[[password]]\" value=\"HtmlEncode[[Html attributes string]]\" />";
            var helper = DefaultTemplatesUtilities.GetHtmlHelper<string>(model: null);
            var viewData = helper.ViewData;
            var templateInfo = viewData.TemplateInfo;
            templateInfo.HtmlFieldPrefix = "FieldPrefix";

            viewData["htmlAttributes"] = new { @class = "super", value = "Html attributes string" };

            // Act
            var result = DefaultEditorTemplates.PasswordTemplate(helper);

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

        [Theory]
        [MemberData(nameof(TemplateNameData))]
        public void Editor_CallsExpectedHtmlHelper(string templateName, string expectedResult)
        {
            // Arrange
            var model = new DefaultTemplatesUtilities.ObjectTemplateModel { Property1 = "True" };
            var viewEngine = new Mock<ICompositeViewEngine>(MockBehavior.Strict);
            viewEngine
                .Setup(v => v.GetView(/*executingFilePath*/ null, It.IsAny<string>(), /*isMainPage*/ false))
                .Returns(ViewEngineResult.NotFound(string.Empty, Enumerable.Empty<string>()));
            viewEngine
                .Setup(v => v.FindView(It.IsAny<ActionContext>(), It.IsAny<string>(), /*isMainPage*/ false))
                .Returns(ViewEngineResult.NotFound(string.Empty, Enumerable.Empty<string>()));
            var helper = DefaultTemplatesUtilities.GetHtmlHelper(
                model,
                viewEngine.Object,
                innerHelper => new StubbyHtmlHelper(innerHelper));
            helper.ViewData["Property1"] = "True";

            // TemplateBuilder sets FormattedModelValue before calling TemplateRenderer and it's used in most templates.
            helper.ViewData.TemplateInfo.FormattedModelValue = "Formatted string";

            // Act
            var result = helper.Editor(
                "Property1",
                templateName,
                htmlFieldName: null,
                additionalViewData: null);

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

        [Theory]
        [MemberData(nameof(TemplateNameData))]
        public void EditorFor_CallsExpectedHtmlHelper(string templateName, string expectedResult)
        {
            // Arrange
            var model = new DefaultTemplatesUtilities.ObjectTemplateModel { Property1 = "True" };
            var viewEngine = new Mock<ICompositeViewEngine>(MockBehavior.Strict);
            viewEngine
                .Setup(v => v.GetView(/*executingFilePath*/ null, It.IsAny<string>(), /*isMainPage*/ false))
                .Returns(ViewEngineResult.NotFound(string.Empty, Enumerable.Empty<string>()));
            viewEngine
                .Setup(v => v.FindView(It.IsAny<ActionContext>(), It.IsAny<string>(), /*isMainPage*/ false))
                .Returns(ViewEngineResult.NotFound(string.Empty, Enumerable.Empty<string>()));
            var helper = DefaultTemplatesUtilities.GetHtmlHelper(
                model,
                viewEngine.Object,
                innerHelper => new StubbyHtmlHelper(innerHelper));

            // TemplateBuilder sets FormattedModelValue before calling TemplateRenderer and it's used in most templates.
            helper.ViewData.TemplateInfo.FormattedModelValue = "Formatted string";

            // Act
            var result = helper.EditorFor(
                anotherModel => anotherModel.Property1,
                templateName,
                htmlFieldName: null,
                additionalViewData: null);

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

        [Theory]
        [MemberData(nameof(TemplateNameData))]
        public void Editor_CallsExpectedHtmlHelper_DataTypeName(string templateName, string expectedResult)
        {
            // Arrange
            var model = new DefaultTemplatesUtilities.ObjectTemplateModel { Property1 = "True" };
            var viewEngine = new Mock<ICompositeViewEngine>(MockBehavior.Strict);
            viewEngine
                .Setup(v => v.GetView(/*executingFilePath*/ null, It.IsAny<string>(), /*isMainPage*/ false))
                .Returns(ViewEngineResult.NotFound(string.Empty, Enumerable.Empty<string>()));
            viewEngine
                .Setup(v => v.FindView(It.IsAny<ActionContext>(), It.IsAny<string>(), /*isMainPage*/ false))
                .Returns(ViewEngineResult.NotFound(string.Empty, Enumerable.Empty<string>()));

            var provider = new TestModelMetadataProvider();
            provider.ForProperty<DefaultTemplatesUtilities.ObjectTemplateModel>("Property1").DisplayDetails(dd =>
            {
                dd.DataTypeName = templateName;
            });

            var helper = DefaultTemplatesUtilities.GetHtmlHelper(
                model,
                Mock.Of<IUrlHelper>(),
                viewEngine.Object,
                provider,
                localizerFactory: null,
                innerHelper => new StubbyHtmlHelper(innerHelper));
            helper.ViewData["Property1"] = "True";

            // TemplateBuilder sets FormattedModelValue before calling TemplateRenderer and it's used in most templates.
            helper.ViewData.TemplateInfo.FormattedModelValue = "Formatted string";

            // Act
            var result = helper.Editor(
                "Property1",
                templateName,
                htmlFieldName: null,
                additionalViewData: null);

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

        [Theory]
        [MemberData(nameof(TemplateNameData))]
        public void EditorFor_CallsExpectedHtmlHelper_DataTypeName(string templateName, string expectedResult)
        {
            // Arrange
            var model = new DefaultTemplatesUtilities.ObjectTemplateModel { Property1 = "True" };
            var viewEngine = new Mock<ICompositeViewEngine>(MockBehavior.Strict);
            viewEngine
                .Setup(v => v.GetView(/*executingFilePath*/ null, It.IsAny<string>(), /*isMainPage*/ false))
                .Returns(ViewEngineResult.NotFound(string.Empty, Enumerable.Empty<string>()));
            viewEngine
                .Setup(v => v.FindView(It.IsAny<ActionContext>(), It.IsAny<string>(), /*isMainPage*/ false))
                .Returns(ViewEngineResult.NotFound(string.Empty, Enumerable.Empty<string>()));

            var provider = new TestModelMetadataProvider();
            provider.ForProperty<DefaultTemplatesUtilities.ObjectTemplateModel>("Property1").DisplayDetails(dd =>
            {
                dd.DataTypeName = templateName;
            });

            var helper = DefaultTemplatesUtilities.GetHtmlHelper(
                model,
                Mock.Of<IUrlHelper>(),
                viewEngine.Object,
                provider,
                localizerFactory: null,
                innerHelper => new StubbyHtmlHelper(innerHelper));

            // TemplateBuilder sets FormattedModelValue before calling TemplateRenderer and it's used in most templates.
            helper.ViewData.TemplateInfo.FormattedModelValue = "Formatted string";

            // Act
            var result = helper.EditorFor(
                anotherModel => anotherModel.Property1,
                templateName,
                htmlFieldName: null,
                additionalViewData: null);

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

        [Theory]
        [MemberData(nameof(TemplateNameData))]
        public void Editor_CallsExpectedHtmlHelper_TemplateHint(string templateName, string expectedResult)
        {
            // Arrange
            var model = new DefaultTemplatesUtilities.ObjectTemplateModel { Property1 = "True" };
            var viewEngine = new Mock<ICompositeViewEngine>(MockBehavior.Strict);
            viewEngine
                .Setup(v => v.GetView(/*executingFilePath*/ null, It.IsAny<string>(), /*isMainPage*/ false))
                .Returns(ViewEngineResult.NotFound(string.Empty, Enumerable.Empty<string>()));
            viewEngine
                .Setup(v => v.FindView(It.IsAny<ActionContext>(), It.IsAny<string>(), /*isMainPage*/ false))
                .Returns(ViewEngineResult.NotFound(string.Empty, Enumerable.Empty<string>()));

            var provider = new TestModelMetadataProvider();
            provider.ForProperty<DefaultTemplatesUtilities.ObjectTemplateModel>("Property1").DisplayDetails(dd =>
            {
                dd.TemplateHint = templateName;
            });

            var helper = DefaultTemplatesUtilities.GetHtmlHelper(
                model,
                Mock.Of<IUrlHelper>(),
                viewEngine.Object,
                provider,
                localizerFactory: null,
                innerHelper => new StubbyHtmlHelper(innerHelper));
            helper.ViewData["Property1"] = "True";

            // TemplateBuilder sets FormattedModelValue before calling TemplateRenderer and it's used in most templates.
            helper.ViewData.TemplateInfo.FormattedModelValue = "Formatted string";

            // Act
            var result = helper.Editor(
                "Property1",
                templateName,
                htmlFieldName: null,
                additionalViewData: null);

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

        [Theory]
        [MemberData(nameof(TemplateNameData))]
        public void EditorFor_CallsExpectedHtmlHelper_TemplateHint(string templateName, string expectedResult)
        {
            // Arrange
            var model = new DefaultTemplatesUtilities.ObjectTemplateModel { Property1 = "True" };
            var viewEngine = new Mock<ICompositeViewEngine>(MockBehavior.Strict);
            viewEngine
                .Setup(v => v.GetView(/*executingFilePath*/ null, It.IsAny<string>(), /*isMainPage*/ false))
                .Returns(ViewEngineResult.NotFound(string.Empty, Enumerable.Empty<string>()));
            viewEngine
                .Setup(v => v.FindView(It.IsAny<ActionContext>(), It.IsAny<string>(), /*isMainPage*/ false))
                .Returns(ViewEngineResult.NotFound(string.Empty, Enumerable.Empty<string>()));

            var provider = new TestModelMetadataProvider();
            provider.ForProperty<DefaultTemplatesUtilities.ObjectTemplateModel>("Property1").DisplayDetails(dd =>
            {
                dd.TemplateHint = templateName;
            });

            var helper = DefaultTemplatesUtilities.GetHtmlHelper(
                model,
                Mock.Of<IUrlHelper>(),
                viewEngine.Object,
                provider,
                localizerFactory: null,
                innerHelper => new StubbyHtmlHelper(innerHelper));

            // TemplateBuilder sets FormattedModelValue before calling TemplateRenderer and it's used in most templates.
            helper.ViewData.TemplateInfo.FormattedModelValue = "Formatted string";

            // Act
            var result = helper.EditorFor(
                anotherModel => anotherModel.Property1,
                templateName,
                htmlFieldName: null,
                additionalViewData: null);

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

        [Fact]
        public void Editor_FindsViewDataMember()
        {
            // Arrange
            var model = new DefaultTemplatesUtilities.ObjectTemplateModel { Property1 = "Model string" };
            var viewEngine = new Mock<ICompositeViewEngine>(MockBehavior.Strict);
            viewEngine
                .Setup(v => v.GetView(/*executingFilePath*/ null, It.IsAny<string>(), /*isMainPage*/ false))
                .Returns(ViewEngineResult.NotFound(string.Empty, Enumerable.Empty<string>()));
            viewEngine
                .Setup(v => v.FindView(It.IsAny<ActionContext>(), It.IsAny<string>(), /*isMainPage*/ false))
                .Returns(ViewEngineResult.NotFound(string.Empty, Enumerable.Empty<string>()));
            var helper = DefaultTemplatesUtilities.GetHtmlHelper(model, viewEngine.Object);
            helper.ViewData["Property1"] = "ViewData string";

            // Act
            var result = helper.Editor("Property1");

            // Assert
            Assert.Equal(
                "<input class=\"HtmlEncode[[text-box single-line]]\" id=\"HtmlEncode[[Property1]]\" name=\"HtmlEncode[[Property1]]\" type=\"HtmlEncode[[text]]\" value=\"HtmlEncode[[ViewData string]]\" />",
                HtmlContentUtilities.HtmlContentToString(result));
        }

        [Fact]
        public void Editor_InputTypeDateTime_RendersAsDateTime()
        {
            // Arrange
            var requiredMessage = ValidationAttributeUtil.GetRequiredErrorMessage("DateTimeOffset");
            var expectedInput = "<input class=\"HtmlEncode[[text-box single-line]]\" data-val=\"HtmlEncode[[true]]\" " +
                $"data-val-required=\"HtmlEncode[[{requiredMessage}]]\" id=\"HtmlEncode[[FieldPrefix]]\" " +
                "name=\"HtmlEncode[[FieldPrefix]]\" type=\"HtmlEncode[[datetime]]\" value=\"HtmlEncode[[2000-01-02T03:04:05.060+00:00]]\" />";

            var offset = TimeSpan.FromHours(0);
            var model = new DateTimeOffset(
                year: 2000,
                month: 1,
                day: 2,
                hour: 3,
                minute: 4,
                second: 5,
                millisecond: 60,
                offset: offset);
            var viewEngine = new Mock<ICompositeViewEngine>(MockBehavior.Strict);
            viewEngine
                .Setup(v => v.GetView(/*executingFilePath*/ null, It.IsAny<string>(), /*isMainPage*/ false))
                .Returns(ViewEngineResult.NotFound(string.Empty, Enumerable.Empty<string>()));
            viewEngine
                .Setup(v => v.FindView(It.IsAny<ActionContext>(), It.IsAny<string>(), /*isMainPage*/ false))
                .Returns(ViewEngineResult.NotFound(string.Empty, Enumerable.Empty<string>()));

            var provider = new TestModelMetadataProvider();

            var helper = DefaultTemplatesUtilities.GetHtmlHelper(
                model,
                Mock.Of<IUrlHelper>(),
                viewEngine.Object,
                provider);
            helper.ViewData.TemplateInfo.HtmlFieldPrefix = "FieldPrefix";

            // Act
            var result = helper.Editor(
                string.Empty,
                new { htmlAttributes = new { type = "datetime" } });

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

        // Html5DateRenderingMode.Rfc3339 is enabled by default.
        [Theory]
        [InlineData(null, null, "2000-01-02T03:04:05.060-05:00", "text")]
        [InlineData("date", null, "2000-01-02", "date")]
        [InlineData("date", "{0:d}", "02/01/2000", "date")]
        [InlineData("datetime", null, "2000-01-02T03:04:05.060", "datetime-local")]
        [InlineData("datetime-local", null, "2000-01-02T03:04:05.060", "datetime-local")]
        [InlineData("DateTimeOffset", null, "2000-01-02T03:04:05.060-05:00", "text")]
        [InlineData("DateTimeOffset", "{0:o}", "2000-01-02T03:04:05.0600000-05:00", "text")]
        [InlineData("time", null, "03:04:05.060", "time")]
        [InlineData("time", "{0:t}", "03:04", "time")]
        [InlineData("month", null, "2000-01", "month")]
        [InlineData("month", "{0:yyyy-MM}", "2000-01", "month")]
        [InlineData("week", null, "1999-W52", "week")]
        [InlineData("week", "{0:yyyy-'W1'}", "2000-W1", "week")]
        [ReplaceCulture]
        public void Editor_FindsCorrectDateOrTimeTemplate_WithTimeOffset(
            string dataTypeName,
            string editFormatString,
            string expectedValue,
            string expectedType)
        {
            // Arrange
            var requiredMessage = ValidationAttributeUtil.GetRequiredErrorMessage("DateTimeOffset");
            var expectedInput = "<input class=\"HtmlEncode[[text-box single-line]]\" data-val=\"HtmlEncode[[true]]\" " +
                $"data-val-required=\"HtmlEncode[[{requiredMessage}]]\" id=\"HtmlEncode[[FieldPrefix]]\" " +
                "name=\"HtmlEncode[[FieldPrefix]]\" type=\"HtmlEncode[[" +
                expectedType +
                "]]\" value=\"HtmlEncode[[" + expectedValue + "]]\" />";

            var offset = TimeSpan.FromHours(-5);
            var model = new DateTimeOffset(
                year: 2000,
                month: 1,
                day: 2,
                hour: 3,
                minute: 4,
                second: 5,
                millisecond: 60,
                offset: offset);
            var viewEngine = new Mock<ICompositeViewEngine>(MockBehavior.Strict);
            viewEngine
                .Setup(v => v.GetView(/*executingFilePath*/ null, It.IsAny<string>(), /*isMainPage*/ false))
                .Returns(ViewEngineResult.NotFound(string.Empty, Enumerable.Empty<string>()));
            viewEngine
                .Setup(v => v.FindView(It.IsAny<ActionContext>(), It.IsAny<string>(), /*isMainPage*/ false))
                .Returns(ViewEngineResult.NotFound(string.Empty, Enumerable.Empty<string>()));

            var provider = new TestModelMetadataProvider();
            provider.ForType<DateTimeOffset>().DisplayDetails(dd =>
            {
                dd.DataTypeName = dataTypeName;
                dd.EditFormatString = editFormatString; // What [DataType] does for given type.
                dd.HasNonDefaultEditFormat = true;
            });

            var helper = DefaultTemplatesUtilities.GetHtmlHelper(
                model,
                Mock.Of<IUrlHelper>(),
                viewEngine.Object,
                provider);
            helper.ViewData.TemplateInfo.HtmlFieldPrefix = "FieldPrefix";

            // Act
            var result = helper.Editor(string.Empty);

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

        // Html5DateRenderingMode.Rfc3339 can be disabled.
        [Theory]
        [InlineData(null, null, "02/01/2000 03:04:05 -05:00", "text")]
        [InlineData("date", null, "02/01/2000 03:04:05 -05:00", "date")]
        [InlineData("date", "{0:d}", "02/01/2000", "date")]
        [InlineData("datetime", null, "02/01/2000 03:04:05 -05:00", "datetime-local")]
        [InlineData("datetime-local", null, "02/01/2000 03:04:05 -05:00", "datetime-local")]
        [InlineData("DateTimeOffset", null, "02/01/2000 03:04:05 -05:00", "text")]
        [InlineData("DateTimeOffset", "{0:o}", "2000-01-02T03:04:05.0600000-05:00", "text")]
        [InlineData("time", null, "02/01/2000 03:04:05 -05:00", "time")]
        [InlineData("time", "{0:t}", "03:04", "time")]
        [InlineData("month", null, "2000-01", "month")]
        [InlineData("month", "{0:yyyy-MM}", "2000-01", "month")]
        [InlineData("week", null, "1999-W52", "week")]
        [InlineData("week", "{0:yyyy-'W1'}", "2000-W1", "week")]
        [ReplaceCulture]
        public void Editor_FindsCorrectDateOrTimeTemplate_WithTimeOffset_NotRfc3339(
            string dataTypeName,
            string editFormatString,
            string expectedValue,
            string expectedType)
        {
            // Arrange
            var requiredMessage = ValidationAttributeUtil.GetRequiredErrorMessage("DateTimeOffset");
            var expectedInput =
                "<input class=\"HtmlEncode[[text-box single-line]]\" data-val=\"HtmlEncode[[true]]\" " +
                $"data-val-required=\"HtmlEncode[[{requiredMessage}]]\" id=\"HtmlEncode[[FieldPrefix]]\" " +
                "name=\"HtmlEncode[[FieldPrefix]]\" type=\"HtmlEncode[[" +
                expectedType +
                "]]\" value=\"HtmlEncode[[" + expectedValue + "]]\" />";

            var offset = TimeSpan.FromHours(-5);
            var model = new DateTimeOffset(
                year: 2000,
                month: 1,
                day: 2,
                hour: 3,
                minute: 4,
                second: 5,
                millisecond: 60,
                offset: offset);
            var viewEngine = new Mock<ICompositeViewEngine>(MockBehavior.Strict);
            viewEngine
                .Setup(v => v.GetView(/*executingFilePath*/ null, It.IsAny<string>(), /*isMainPage*/ false))
                .Returns(ViewEngineResult.NotFound(string.Empty, Enumerable.Empty<string>()));
            viewEngine
                .Setup(v => v.FindView(It.IsAny<ActionContext>(), It.IsAny<string>(), /*isMainPage*/ false))
                .Returns(ViewEngineResult.NotFound(string.Empty, Enumerable.Empty<string>()));

            var provider = new TestModelMetadataProvider();
            provider.ForType<DateTimeOffset>().DisplayDetails(dd =>
            {
                dd.DataTypeName = dataTypeName;
                dd.EditFormatString = editFormatString; // What [DataType] does for given type.
                dd.HasNonDefaultEditFormat = true;
            });

            var helper = DefaultTemplatesUtilities.GetHtmlHelper(
                model,
                Mock.Of<IUrlHelper>(),
                viewEngine.Object,
                provider);
            helper.Html5DateRenderingMode = Html5DateRenderingMode.CurrentCulture;
            helper.ViewData.TemplateInfo.HtmlFieldPrefix = "FieldPrefix";

            // Act
            var result = helper.Editor(string.Empty);

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

        // Html5DateRenderingMode.Rfc3339 is enabled by default.
        [Theory]
        [InlineData(null, null, "2000-01-02T03:04:05.060", "datetime-local")]
        [InlineData("date", null, "2000-01-02", "date")]
        [InlineData("date", "{0:d}", "02/01/2000", "date")]
        [InlineData("datetime", null, "2000-01-02T03:04:05.060", "datetime-local")]
        [InlineData("datetime-local", null, "2000-01-02T03:04:05.060", "datetime-local")]
        [InlineData("DateTimeOffset", null, "2000-01-02T03:04:05.060Z", "text")]
        [InlineData("DateTimeOffset", "{0:o}", "2000-01-02T03:04:05.0600000Z", "text")]
        [InlineData("time", null, "03:04:05.060", "time")]
        [InlineData("time", "{0:t}", "03:04", "time")]
        [InlineData("month", null, "2000-01", "month")]
        [InlineData("month", "{0:yyyy/MM}", "2000/01", "month")]
        [InlineData("week", null, "1999-W52", "week")]
        [InlineData("Week", "{0:yyyy/'W1'}", "2000/W1", "week")]
        [ReplaceCulture]
        public void Editor_FindsCorrectDateOrTimeTemplate_ForDateTime(
            string dataTypeName,
            string editFormatString,
            string expectedValue,
            string expectedType)
        {
            // Arrange
            var requiredMessage = ValidationAttributeUtil.GetRequiredErrorMessage("DateTime");
            var expectedInput = "<input class=\"HtmlEncode[[text-box single-line]]\" data-val=\"HtmlEncode[[true]]\" " +
                $"data-val-required=\"HtmlEncode[[{requiredMessage}]]\" id=\"HtmlEncode[[FieldPrefix]]\" " +
                "name=\"HtmlEncode[[FieldPrefix]]\" type=\"HtmlEncode[[" +
                expectedType +
                "]]\" value=\"HtmlEncode[[" + expectedValue + "]]\" />";

            var model = new DateTime(
                year: 2000,
                month: 1,
                day: 2,
                hour: 3,
                minute: 4,
                second: 5,
                millisecond: 60,
                kind: DateTimeKind.Utc);
            var viewEngine = new Mock<ICompositeViewEngine>(MockBehavior.Strict);
            viewEngine
                .Setup(v => v.GetView(/*executingFilePath*/ null, It.IsAny<string>(), /*isMainPage*/ false))
                .Returns(ViewEngineResult.NotFound(string.Empty, Enumerable.Empty<string>()));
            viewEngine
                .Setup(v => v.FindView(It.IsAny<ActionContext>(), It.IsAny<string>(), /*isMainPage*/ false))
                .Returns(ViewEngineResult.NotFound(string.Empty, Enumerable.Empty<string>()));

            var provider = new TestModelMetadataProvider();
            provider.ForType<DateTime>().DisplayDetails(dd =>
            {
                dd.DataTypeName = dataTypeName;
                dd.EditFormatString = editFormatString; // What [DataType] does for given type.
                dd.HasNonDefaultEditFormat = true;
            });

            var helper = DefaultTemplatesUtilities.GetHtmlHelper(
                model,
                Mock.Of<IUrlHelper>(),
                viewEngine.Object,
                provider);
            helper.ViewData.TemplateInfo.HtmlFieldPrefix = "FieldPrefix";

            // Act
            var result = helper.Editor(string.Empty);

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

        // Html5DateRenderingMode.Rfc3339 can be disabled.
        [Theory]
        [InlineData(null, null, "02/01/2000 03:04:05", "datetime-local")]
        [InlineData("date", null, "02/01/2000 03:04:05", "date")]
        [InlineData("date", "{0:d}", "02/01/2000", "date")]
        [InlineData("datetime", null, "02/01/2000 03:04:05", "datetime-local")]
        [InlineData("datetime-local", null, "02/01/2000 03:04:05", "datetime-local")]
        [InlineData("DateTimeOffset", null, "02/01/2000 03:04:05", "text")]
        [InlineData("DateTimeOffset", "{0:o}", "2000-01-02T03:04:05.0600000Z", "text")]
        [InlineData("time", "{0:t}", "03:04", "time")]
        [InlineData("month", null, "2000-01", "month")]
        [InlineData("month", "{0:yyyy/MM}", "2000/01", "month")]
        [InlineData("week", null, "1999-W52", "week")]
        [InlineData("Week", "{0:yyyy/'W1'}", "2000/W1", "week")]
        [ReplaceCulture]
        public void Editor_FindsCorrectDateOrTimeTemplate_ForDateTimeNotRfc3339(
            string dataTypeName,
            string editFormatString,
            string expectedValue,
            string expectedType)
        {
            // Arrange
            var requiredMessage = ValidationAttributeUtil.GetRequiredErrorMessage("DateTime");
            var expectedInput =
                "<input class=\"HtmlEncode[[text-box single-line]]\" data-val=\"HtmlEncode[[true]]\" " +
                $"data-val-required=\"HtmlEncode[[{requiredMessage}]]\" id=\"HtmlEncode[[FieldPrefix]]\" " +
                "name=\"HtmlEncode[[FieldPrefix]]\" type=\"HtmlEncode[[" +
                expectedType +
                "]]\" value=\"HtmlEncode[[" + expectedValue + "]]\" />";

            var model = new DateTime(
                year: 2000,
                month: 1,
                day: 2,
                hour: 3,
                minute: 4,
                second: 5,
                millisecond: 60,
                kind: DateTimeKind.Utc);
            var viewEngine = new Mock<ICompositeViewEngine>(MockBehavior.Strict);
            viewEngine
                .Setup(v => v.GetView(/*executingFilePath*/ null, It.IsAny<string>(), /*isMainPage*/ false))
                .Returns(ViewEngineResult.NotFound(string.Empty, Enumerable.Empty<string>()));
            viewEngine
                .Setup(v => v.FindView(It.IsAny<ActionContext>(), It.IsAny<string>(), /*isMainPage*/ false))
                .Returns(ViewEngineResult.NotFound(string.Empty, Enumerable.Empty<string>()));

            var provider = new TestModelMetadataProvider();
            provider.ForType<DateTime>().DisplayDetails(dd =>
            {
                dd.DataTypeName = dataTypeName;
                dd.EditFormatString = editFormatString; // What [DataType] does for given type.
                dd.HasNonDefaultEditFormat = true;
            });

            var helper = DefaultTemplatesUtilities.GetHtmlHelper(
                model,
                Mock.Of<IUrlHelper>(),
                viewEngine.Object,
                provider);
            helper.Html5DateRenderingMode = Html5DateRenderingMode.CurrentCulture;
            helper.ViewData.TemplateInfo.HtmlFieldPrefix = "FieldPrefix";

            // Act
            var result = helper.Editor(string.Empty);

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

        [Theory]
        [InlineData(null, Html5DateRenderingMode.CurrentCulture, "text")]
        [InlineData(null, Html5DateRenderingMode.Rfc3339, "text")]
        [InlineData("date", Html5DateRenderingMode.CurrentCulture, "date")]
        [InlineData("date", Html5DateRenderingMode.Rfc3339, "date")]
        [InlineData("datetime", Html5DateRenderingMode.CurrentCulture, "datetime-local")]
        [InlineData("datetime", Html5DateRenderingMode.Rfc3339, "datetime-local")]
        [InlineData("datetime-local", Html5DateRenderingMode.CurrentCulture, "datetime-local")]
        [InlineData("datetime-local", Html5DateRenderingMode.Rfc3339, "datetime-local")]
        [InlineData("time", Html5DateRenderingMode.CurrentCulture, "time")]
        [InlineData("time", Html5DateRenderingMode.Rfc3339, "time")]
        [InlineData("month", Html5DateRenderingMode.CurrentCulture, "month")]
        [InlineData("month", Html5DateRenderingMode.Rfc3339, "month")]
        [InlineData("week", Html5DateRenderingMode.CurrentCulture, "week")]
        [InlineData("week", Html5DateRenderingMode.Rfc3339, "week")]
        public void Editor_AppliesNonDefaultEditFormat(string dataTypeName, Html5DateRenderingMode renderingMode, string expectedType)
        {
            // Arrange
            var requiredMessage = ValidationAttributeUtil.GetRequiredErrorMessage("DateTimeOffset");
            var expectedInput = "<input class=\"HtmlEncode[[text-box single-line]]\" data-val=\"HtmlEncode[[true]]\" " +
                $"data-val-required=\"HtmlEncode[[{requiredMessage}]]\" id=\"HtmlEncode[[FieldPrefix]]\" " +
                "name=\"HtmlEncode[[FieldPrefix]]\" type=\"HtmlEncode[[" +
                expectedType +
                "]]\" value=\"HtmlEncode[[Formatted as 2000-01-02T03:04:05.0600000+00:00]]\" />";

            var offset = TimeSpan.FromHours(0);
            var model = new DateTimeOffset(
                year: 2000,
                month: 1,
                day: 2,
                hour: 3,
                minute: 4,
                second: 5,
                millisecond: 60,
                offset: offset);
            var viewEngine = new Mock<ICompositeViewEngine>(MockBehavior.Strict);
            viewEngine
                .Setup(v => v.GetView(/*executingFilePath*/ null, It.IsAny<string>(), /*isMainPage*/ false))
                .Returns(ViewEngineResult.NotFound(string.Empty, Enumerable.Empty<string>()));
            viewEngine
                .Setup(v => v.FindView(It.IsAny<ActionContext>(), It.IsAny<string>(), /*isMainPage*/ false))
                .Returns(ViewEngineResult.NotFound(string.Empty, Enumerable.Empty<string>()));

            var provider = new TestModelMetadataProvider();
            provider.ForType<DateTimeOffset>().DisplayDetails(dd =>
            {
                dd.DataTypeName = dataTypeName;
                dd.EditFormatString = "Formatted as {0:O}"; // What [DataType] does for given type.
                dd.HasNonDefaultEditFormat = true;
            });

            var helper = DefaultTemplatesUtilities.GetHtmlHelper(
                model,
                Mock.Of<IUrlHelper>(),
                viewEngine.Object,
                provider);

            helper.Html5DateRenderingMode = renderingMode; // Ignored due to HasNonDefaultEditFormat.
            helper.ViewData.TemplateInfo.HtmlFieldPrefix = "FieldPrefix";

            // Act
            var result = helper.Editor(string.Empty);

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

        [Fact]
        public void EditorFor_FindsModel()
        {
            // Arrange
            var model = new DefaultTemplatesUtilities.ObjectTemplateModel { Property1 = "Model string" };
            var viewEngine = new Mock<ICompositeViewEngine>(MockBehavior.Strict);
            viewEngine
                .Setup(v => v.GetView(/*executingFilePath*/ null, It.IsAny<string>(), /*isMainPage*/ false))
                .Returns(ViewEngineResult.NotFound(string.Empty, Enumerable.Empty<string>()));
            viewEngine
                .Setup(v => v.FindView(It.IsAny<ActionContext>(), It.IsAny<string>(), /*isMainPage*/ false))
                .Returns(ViewEngineResult.NotFound(string.Empty, Enumerable.Empty<string>()));
            var helper = DefaultTemplatesUtilities.GetHtmlHelper(model, viewEngine.Object);
            helper.ViewData["Property1"] = "ViewData string";

            // Act
            var result = helper.EditorFor(m => m.Property1);

            // Assert
            Assert.Equal(
                "<input class=\"HtmlEncode[[text-box single-line]]\" id=\"HtmlEncode[[Property1]]\" name=\"HtmlEncode[[Property1]]\" type=\"HtmlEncode[[text]]\" value=\"HtmlEncode[[Model string]]\" />",
                HtmlContentUtilities.HtmlContentToString(result));
        }

        [Fact]
        public void Editor_FindsModel_IfNoViewDataMember()
        {
            // Arrange
            var model = new DefaultTemplatesUtilities.ObjectTemplateModel { Property1 = "Model string" };
            var viewEngine = new Mock<ICompositeViewEngine>(MockBehavior.Strict);
            viewEngine
                .Setup(v => v.GetView(/*executingFilePath*/ null, It.IsAny<string>(), /*isMainPage*/ false))
                .Returns(ViewEngineResult.NotFound(string.Empty, Enumerable.Empty<string>()));
            viewEngine
                .Setup(v => v.FindView(It.IsAny<ActionContext>(), It.IsAny<string>(), /*isMainPage*/ false))
                .Returns(ViewEngineResult.NotFound(string.Empty, Enumerable.Empty<string>()));
            var helper = DefaultTemplatesUtilities.GetHtmlHelper(model, viewEngine.Object);

            // Act
            var result = helper.Editor("Property1");

            // Assert
            Assert.Equal(
                "<input class=\"HtmlEncode[[text-box single-line]]\" id=\"HtmlEncode[[Property1]]\" name=\"HtmlEncode[[Property1]]\" type=\"HtmlEncode[[text]]\" value=\"HtmlEncode[[Model string]]\" />",
                HtmlContentUtilities.HtmlContentToString(result));
        }

        [Theory]
        [InlineData(null)]
        [InlineData("")]
        public void EditorFor_FindsModel_EvenIfNullOrEmpty(string propertyValue)
        {
            // Arrange
            var model = new DefaultTemplatesUtilities.ObjectTemplateModel { Property1 = propertyValue, };
            var viewEngine = new Mock<ICompositeViewEngine>(MockBehavior.Strict);
            viewEngine
                .Setup(v => v.GetView(/*executingFilePath*/ null, It.IsAny<string>(), /*isMainPage*/ false))
                .Returns(ViewEngineResult.NotFound(string.Empty, Enumerable.Empty<string>()));
            viewEngine
                .Setup(v => v.FindView(It.IsAny<ActionContext>(), It.IsAny<string>(), /*isMainPage*/ false))
                .Returns(ViewEngineResult.NotFound(string.Empty, Enumerable.Empty<string>()));
            var helper = DefaultTemplatesUtilities.GetHtmlHelper(model, viewEngine.Object);
            helper.ViewData["Property1"] = "ViewData string";

            // Act
            var result = helper.EditorFor(m => m.Property1);

            // Assert
            Assert.Equal(
                "<input class=\"HtmlEncode[[text-box single-line]]\" id=\"HtmlEncode[[Property1]]\" name=\"HtmlEncode[[Property1]]\" type=\"HtmlEncode[[text]]\" value=\"\" />",
                HtmlContentUtilities.HtmlContentToString(result));
        }

        [Fact]
        public void EditorFor_DoesNotWrapExceptionThrowsDuringViewRendering()
        {
            // Arrange
            var expectedMessage = "my exception message";
            var model = new DefaultTemplatesUtilities.ObjectTemplateModel { Property1 = "Test string", };
            var view = new Mock<IView>();
            view.Setup(v => v.RenderAsync(It.IsAny<ViewContext>()))
                .Returns(Task.Run(() =>
                {
                    throw new FormatException(expectedMessage);
                }));
            var viewEngine = new Mock<ICompositeViewEngine>(MockBehavior.Strict);
            viewEngine
                .Setup(v => v.GetView(/*executingFilePath*/ null, It.IsAny<string>(), /*isMainPage*/ false))
                .Returns(ViewEngineResult.NotFound(string.Empty, Enumerable.Empty<string>()));
            viewEngine
                .Setup(v => v.FindView(It.IsAny<ActionContext>(), It.IsAny<string>(), /*isMainPage*/ false))
                .Returns(ViewEngineResult.Found("test-view", view.Object));
            var helper = DefaultTemplatesUtilities.GetHtmlHelper(model, viewEngine.Object);
            helper.ViewData["Property1"] = "ViewData string";

            // Act and Assert
            var ex = Assert.Throws<FormatException>(() => helper.EditorFor(m => m.Property1));
            Assert.Equal(expectedMessage, ex.Message);
        }

        [Fact]
        public void EditorForModel_CallsFindView_WithExpectedPath()
        {
            // Arrange
            var viewEngine = new Mock<ICompositeViewEngine>(MockBehavior.Strict);
            viewEngine
                .Setup(v => v.GetView(/*executingFilePath*/ null, It.IsAny<string>(), /*isMainPage*/ false))
                .Returns(ViewEngineResult.NotFound(string.Empty, Enumerable.Empty<string>()));
            viewEngine
                .Setup(v => v.FindView(It.IsAny<ActionContext>(), "EditorTemplates/String", /*isMainPage*/ false))
                .Returns(ViewEngineResult.Found(string.Empty, new Mock<IView>().Object))
                .Verifiable();
            var html = DefaultTemplatesUtilities.GetHtmlHelper(new object(), viewEngine: viewEngine.Object);

            // Act & Assert
            html.Editor(expression: string.Empty, templateName: null, htmlFieldName: null, additionalViewData: null);
            viewEngine.Verify();
        }

        private class OrderedModel
        {
            [Display(Order = 10001)]
            public string LastProperty { get; set; }

            public string Property3 { get; set; }
            public string Property1 { get; set; }
            public string Property2 { get; set; }

            [Display(Order = 23)]
            public string OrderedProperty3 { get; set; }
            [Display(Order = 23)]
            public string OrderedProperty2 { get; set; }
            [Display(Order = 23)]
            public string OrderedProperty1 { get; set; }
        }

        private class StubbyHtmlHelper : IHtmlHelper, IViewContextAware
        {
            private readonly IHtmlHelper _innerHelper;

            public StubbyHtmlHelper(IHtmlHelper innerHelper)
            {
                _innerHelper = innerHelper;
            }

            public Html5DateRenderingMode Html5DateRenderingMode
            {
                get { return _innerHelper.Html5DateRenderingMode; }
                set { _innerHelper.Html5DateRenderingMode = value; }
            }

            public string IdAttributeDotReplacement
            {
                get { return _innerHelper.IdAttributeDotReplacement; }
            }

            public IModelMetadataProvider MetadataProvider
            {
                get { return _innerHelper.MetadataProvider; }
            }

            public dynamic ViewBag
            {
                get { return _innerHelper.ViewBag; }
            }

            public ViewContext ViewContext
            {
                get { return _innerHelper.ViewContext; }
            }

            public ViewDataDictionary ViewData
            {
                get { return _innerHelper.ViewData; }
            }

            public ITempDataDictionary TempData
            {
                get { return _innerHelper.TempData; }
            }

            public UrlEncoder UrlEncoder
            {
                get { return _innerHelper.UrlEncoder; }
            }

            public void Contextualize(ViewContext viewContext)
            {
                (_innerHelper as IViewContextAware)?.Contextualize(viewContext);
            }

            public IHtmlContent ActionLink(
                string linkText,
                string actionName,
                string controllerName,
                string protocol,
                string hostname,
                string fragment,
                object routeValues,
                object htmlAttributes)
            {
                throw new NotImplementedException();
            }

            public IHtmlContent AntiForgeryToken()
            {
                throw new NotImplementedException();
            }

            public MvcForm BeginForm(
                string actionName,
                string controllerName,
                object routeValues,
                FormMethod method,
                bool? antiforgery,
                object htmlAttributes)
            {
                throw new NotImplementedException();
            }

            public MvcForm BeginRouteForm(
                string routeName,
                object routeValues,
                FormMethod method,
                bool? antiforgery,
                object htmlAttributes)
            {
                throw new NotImplementedException();
            }

            public IHtmlContent CheckBox(string name, bool? isChecked, object htmlAttributes)
            {
                return HelperName("__CheckBox__", htmlAttributes);
            }

            public IHtmlContent Display(
                string expression,
                string templateName,
                string htmlFieldName,
                object additionalViewData)
            {
                throw new NotImplementedException();
            }

            public string DisplayName(string expression)
            {
                throw new NotImplementedException();
            }

            public string DisplayText(string name)
            {
                throw new NotImplementedException();
            }

            public IHtmlContent DropDownList(
                string name,
                IEnumerable<SelectListItem> selectList,
                string optionLabel,
                object htmlAttributes)
            {
                return HelperName("__DropDownList__", htmlAttributes);
            }

            public IHtmlContent Editor(
                string expression,
                string templateName,
                string htmlFieldName,
                object additionalViewData)
            {
                return _innerHelper.Editor(expression, templateName, htmlFieldName, additionalViewData);
            }

            public string Encode(string value)
            {
                throw new NotImplementedException();
            }

            public string Encode(object value)
            {
                return _innerHelper.Encode(value);
            }

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

            public string FormatValue(object value, string format)
            {
                throw new NotImplementedException();
            }

            public string GenerateIdFromName(string name)
            {
                throw new NotImplementedException();
            }

            public IEnumerable<SelectListItem> GetEnumSelectList<TEnum>() where TEnum : struct
            {
                throw new NotImplementedException();
            }

            public IEnumerable<SelectListItem> GetEnumSelectList(Type enumType)
            {
                throw new NotImplementedException();
            }

            public IHtmlContent Hidden(string name, object value, object htmlAttributes)
            {
                return HelperName("__Hidden__", htmlAttributes);
            }

            public string Id(string name)
            {
                throw new NotImplementedException();
            }

            public IHtmlContent Label(string expression, string labelText, object htmlAttributes)
            {
                return HelperName("__Label__", htmlAttributes);
            }

            public IHtmlContent ListBox(string name, IEnumerable<SelectListItem> selectList, object htmlAttributes)
            {
                throw new NotImplementedException();
            }

            public string Name(string name)
            {
                throw new NotImplementedException();
            }

            public Task<IHtmlContent> PartialAsync(
                string partialViewName,
                object model,
                ViewDataDictionary viewData)
            {
                throw new NotImplementedException();
            }

            public IHtmlContent Password(string name, object value, object htmlAttributes)
            {
                return HelperName("__Password__", htmlAttributes);
            }

            public IHtmlContent RadioButton(string name, object value, bool? isChecked, object htmlAttributes)
            {
                return HelperName("__RadioButton__", htmlAttributes);
            }

            public IHtmlContent Raw(object value)
            {
                throw new NotImplementedException();
            }

            public IHtmlContent Raw(string value)
            {
                throw new NotImplementedException();
            }

            public Task RenderPartialAsync(string partialViewName, object model, ViewDataDictionary viewData)
            {
                throw new NotImplementedException();
            }

            public IHtmlContent RouteLink(
                string linkText,
                string routeName,
                string protocol,
                string hostName,
                string fragment,
                object routeValues,
                object htmlAttributes)
            {
                throw new NotImplementedException();
            }

            public IHtmlContent TextArea(string name, string value, int rows, int columns, object htmlAttributes)
            {
                return HelperName("__TextArea__", htmlAttributes);
            }

            public IHtmlContent TextBox(string name, object value, string format, object htmlAttributes)
            {
                return HelperName("__TextBox__", htmlAttributes);
            }

            public IHtmlContent ValidationMessage(string modelName, string message, object htmlAttributes, string tag)
            {
                return HelperName("__ValidationMessage__", htmlAttributes);
            }

            public IHtmlContent ValidationSummary(
                bool excludePropertyErrors,
                string message,
                object htmlAttributes,
                string tag)
            {
                throw new NotImplementedException();
            }

            public string Value(string name, string format)
            {
                throw new NotImplementedException();
            }

            private IHtmlContent HelperName(string name, object htmlAttributes)
            {
                var htmlAttributesDictionary = HtmlHelper.AnonymousObjectToHtmlAttributes(htmlAttributes);
                var htmlAttributesString =
                    string.Join(" ", htmlAttributesDictionary.Select(entry => $"{ entry.Key }='{ entry.Value }'"));
                var helperName = $"{ name } { htmlAttributesString }";

                return new HtmlString(helperName.TrimEnd());
            }
        }
    }
}
