// 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 Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.AspNetCore.Mvc.ViewFeatures;
using Microsoft.AspNetCore.Razor.TagHelpers;
using Microsoft.AspNetCore.Razor.TagHelpers.Testing;
using Microsoft.AspNetCore.InternalTesting;
using Moq;

namespace Microsoft.AspNetCore.Mvc.TagHelpers;

public class InputTagHelperTest
{
    public static TheoryData<TagHelperAttributeList, string> MultiAttributeCheckBoxData
    {
        get
        {
            // outputAttributes, expectedAttributeString
            return new TheoryData<TagHelperAttributeList, string>
                {
                    {
                        new TagHelperAttributeList
                        {
                            { "hello", "world" },
                            { "hello", "world2" }
                        },
                        "hello=\"HtmlEncode[[world]]\" hello=\"HtmlEncode[[world2]]\""
                    },
                    {
                        new TagHelperAttributeList
                        {
                            { "hello", "world" },
                            { "hello", "world2" },
                            { "hello", "world3" }
                        },
                        "hello=\"HtmlEncode[[world]]\" hello=\"HtmlEncode[[world2]]\" hello=\"HtmlEncode[[world3]]\""
                    },
                    {
                        new TagHelperAttributeList
                        {
                            { "HelLO", "world" },
                            { "HELLO", "world2" }
                        },
                        "HelLO=\"HtmlEncode[[world]]\" HELLO=\"HtmlEncode[[world2]]\""
                    },
                    {
                        new TagHelperAttributeList
                        {
                            { "Hello", "world" },
                            { "HELLO", "world2" },
                            { "hello", "world3" }
                        },
                        "Hello=\"HtmlEncode[[world]]\" HELLO=\"HtmlEncode[[world2]]\" hello=\"HtmlEncode[[world3]]\""
                    },
                    {
                        new TagHelperAttributeList
                        {
                            { "HeLlO", "world" },
                            { "hello", "world2" }
                        },
                        "HeLlO=\"HtmlEncode[[world]]\" hello=\"HtmlEncode[[world2]]\""
                    },
                };
        }
    }

    [Theory]
    [MemberData(nameof(MultiAttributeCheckBoxData))]
    public async Task CheckBoxHandlesMultipleAttributesSameNameArePreserved(
        TagHelperAttributeList outputAttributes,
        string expectedAttributeString)
    {
        // Arrange
        var originalContent = "original content";
        var expectedContent = $"<input {expectedAttributeString} type=\"HtmlEncode[[checkbox]]\" id=\"HtmlEncode[[IsACar]]\" " +
            $"name=\"HtmlEncode[[IsACar]]\" value=\"HtmlEncode[[true]]\" />" +
            "<input name=\"HtmlEncode[[IsACar]]\" type=\"HtmlEncode[[hidden]]\" value=\"HtmlEncode[[false]]\" />";

        var context = new TagHelperContext(
            tagName: "input",
            allAttributes: new TagHelperAttributeList(
                Enumerable.Empty<TagHelperAttribute>()),
            items: new Dictionary<object, object>(),
            uniqueId: "test");
        var output = new TagHelperOutput(
            "input",
            outputAttributes,
            getChildContentAsync: (useCachedResult, encoder) => Task.FromResult<TagHelperContent>(result: null))
        {
            TagMode = TagMode.SelfClosing,
        };

        output.Content.AppendHtml(originalContent);
        var htmlGenerator = new TestableHtmlGenerator(new EmptyModelMetadataProvider());
        var tagHelper = GetTagHelper(htmlGenerator, model: false, propertyName: nameof(Model.IsACar));

        // Act
        await tagHelper.ProcessAsync(context, output);

        // Assert
        Assert.NotNull(output.PostElement);
        Assert.Equal(originalContent, HtmlContentUtilities.HtmlContentToString(output.Content));
        Assert.Equal(expectedContent, HtmlContentUtilities.HtmlContentToString(output));
    }

    [Theory]
    [InlineData("bad")]
    [InlineData("notbool")]
    public void CheckBoxHandlesNonParsableStringsAsBoolsCorrectly(
        string possibleBool)
    {
        // Arrange
        const string content = "original content";
        const string tagName = "input";
        const string forAttributeName = "asp-for";

        var expected = Resources.FormatInputTagHelper_InvalidStringResult(
            forAttributeName,
            possibleBool,
            typeof(bool).FullName);

        var attributes = new TagHelperAttributeList
            {
                { "class", "form-control" },
            };

        var context = new TagHelperContext(
            tagName: "input",
            allAttributes: new TagHelperAttributeList(
                Enumerable.Empty<TagHelperAttribute>()),
            items: new Dictionary<object, object>(),
            uniqueId: "test");
        var output = new TagHelperOutput(
            tagName,
            attributes,
            getChildContentAsync: (useCachedResult, encoder) => Task.FromResult<TagHelperContent>(result: null))
        {
            TagMode = TagMode.SelfClosing,
        };
        output.Content.AppendHtml(content);
        var htmlGenerator = new TestableHtmlGenerator(new EmptyModelMetadataProvider());
        var tagHelper = GetTagHelper(htmlGenerator, model: possibleBool, propertyName: nameof(Model.IsACar));

        // Act and Assert
        var ex = Assert.Throws<InvalidOperationException>(() => tagHelper.Process(context, output));
        Assert.Equal(expected, ex.Message);
    }

    [Theory]
    [InlineData(10)]
    [InlineData(1337)]
    public void CheckBoxHandlesInvalidDataTypesCorrectly(
        int possibleBool)
    {
        // Arrange
        const string content = "original content";
        const string tagName = "input";
        const string forAttributeName = "asp-for";

        var expected = Resources.FormatInputTagHelper_InvalidExpressionResult(
            "<input>",
            forAttributeName,
            possibleBool.GetType().FullName,
            typeof(bool).FullName,
            typeof(string).FullName,
            "type",
            "checkbox");

        var attributes = new TagHelperAttributeList
            {
                { "class", "form-control" },
            };

        var context = new TagHelperContext(
            tagName: "input",
            allAttributes: new TagHelperAttributeList(
                Enumerable.Empty<TagHelperAttribute>()),
            items: new Dictionary<object, object>(),
            uniqueId: "test");
        var output = new TagHelperOutput(
            tagName,
            attributes,
            getChildContentAsync: (useCachedResult, encoder) => Task.FromResult<TagHelperContent>(result: null))
        {
            TagMode = TagMode.SelfClosing,
        };
        output.Content.AppendHtml(content);
        var htmlGenerator = new TestableHtmlGenerator(new EmptyModelMetadataProvider());
        var tagHelper = GetTagHelper(htmlGenerator, model: possibleBool, propertyName: nameof(Model.IsACar));

        // Act and Assert
        var ex = Assert.Throws<InvalidOperationException>(() => tagHelper.Process(context, output));
        Assert.Equal(expected, ex.Message);
    }

    [Theory]
    [InlineData("trUE")]
    [InlineData("FAlse")]
    public void CheckBoxHandlesParsableStringsAsBoolsCorrectly(
        string possibleBool)
    {
        // Arrange
        const string content = "original content";
        const string tagName = "input";
        const string isCheckedAttr = "checked=\"HtmlEncode[[checked]]\" ";
        var isChecked = (bool.Parse(possibleBool) ? isCheckedAttr : string.Empty);
        var expectedContent = $"<input class=\"HtmlEncode[[form-control]]\" type=\"HtmlEncode[[checkbox]]\" " +
            $"{isChecked}id=\"HtmlEncode[[IsACar]]\" name=\"HtmlEncode[[IsACar]]\" " +
            "value=\"HtmlEncode[[true]]\" /><input name=\"HtmlEncode[[IsACar]]\" type=\"HtmlEncode[[hidden]]\" " +
            "value=\"HtmlEncode[[false]]\" />";
        var expectedPostElement = "<input name=\"IsACar\" type=\"hidden\" value=\"false\" />";

        var attributes = new TagHelperAttributeList
            {
                { "class", "form-control" },
            };

        var context = new TagHelperContext(
            tagName: "input",
            allAttributes: new TagHelperAttributeList(
                Enumerable.Empty<TagHelperAttribute>()),
            items: new Dictionary<object, object>(),
            uniqueId: "test");
        var output = new TagHelperOutput(
            tagName,
            attributes,
            getChildContentAsync: (useCachedResult, encoder) => Task.FromResult<TagHelperContent>(result: null))
        {
            TagMode = TagMode.SelfClosing,
        };
        output.Content.AppendHtml(content);
        var htmlGenerator = new TestableHtmlGenerator(new EmptyModelMetadataProvider());
        var tagHelper = GetTagHelper(htmlGenerator, model: possibleBool, propertyName: nameof(Model.IsACar));

        // Act
        tagHelper.Process(context, output);

        // Assert
        Assert.Equal(content, HtmlContentUtilities.HtmlContentToString(output.Content));
        Assert.Equal(expectedContent, HtmlContentUtilities.HtmlContentToString(output));
        Assert.Equal(expectedPostElement, output.PostElement.GetContent());
    }

    [Theory]
    [InlineData("checkbox")]
    [InlineData("hidden")]
    [InlineData("number")]
    [InlineData("password")]
    [InlineData("text")]
    public void Process_WithEmptyForName_Throws(string inputTypeName)
    {
        // Arrange
        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.";

        var metadataProvider = new EmptyModelMetadataProvider();
        var htmlGenerator = new TestableHtmlGenerator(metadataProvider);
        var model = false;
        var modelExplorer = metadataProvider.GetModelExplorerForType(typeof(bool), model);
        var modelExpression = new ModelExpression(name: string.Empty, modelExplorer: modelExplorer);
        var viewContext = TestableHtmlGenerator.GetViewContext(model, htmlGenerator, metadataProvider);
        var tagHelper = new InputTagHelper(htmlGenerator)
        {
            For = modelExpression,
            InputTypeName = inputTypeName,
            ViewContext = viewContext,
        };

        var attributes = new TagHelperAttributeList
            {
                { "type", inputTypeName },
            };

        var context = new TagHelperContext(attributes, new Dictionary<object, object>(), "test");
        var output = new TagHelperOutput(
            "input",
            new TagHelperAttributeList(),
            getChildContentAsync: (useCachedResult, encoder) => Task.FromResult<TagHelperContent>(result: null))
        {
            TagMode = TagMode.SelfClosing,
        };

        // Act & Assert
        ExceptionAssert.ThrowsArgument(
            () => tagHelper.Process(context, output),
            paramName: "expression",
            exceptionMessage: expectedMessage);
    }

    [Fact]
    public void Process_Radio_WithEmptyForName_Throws()
    {
        // Arrange
        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.";

        var inputTypeName = "radio";
        var metadataProvider = new EmptyModelMetadataProvider();
        var htmlGenerator = new TestableHtmlGenerator(metadataProvider);
        var model = 23;
        var modelExplorer = metadataProvider.GetModelExplorerForType(typeof(int), model);
        var modelExpression = new ModelExpression(name: string.Empty, modelExplorer: modelExplorer);
        var viewContext = TestableHtmlGenerator.GetViewContext(model, htmlGenerator, metadataProvider);
        var tagHelper = new InputTagHelper(htmlGenerator)
        {
            For = modelExpression,
            InputTypeName = inputTypeName,
            Value = "24",
            ViewContext = viewContext,
        };

        var attributes = new TagHelperAttributeList
            {
                { "type", inputTypeName },
                { "value", "24" },
            };

        var context = new TagHelperContext(attributes, new Dictionary<object, object>(), "test");
        var output = new TagHelperOutput(
            "input",
            new TagHelperAttributeList(),
            getChildContentAsync: (useCachedResult, encoder) => Task.FromResult<TagHelperContent>(result: null))
        {
            TagMode = TagMode.SelfClosing,
        };

        // Act & Assert
        ExceptionAssert.ThrowsArgument(
            () => tagHelper.Process(context, output),
            paramName: "expression",
            exceptionMessage: expectedMessage);
    }

    [Theory]
    [InlineData("hidden")]
    [InlineData("number")]
    [InlineData("text")]
    public void Process_WithEmptyForName_DoesNotThrow_WithName(string inputTypeName)
    {
        // Arrange
        var expectedAttributeValue = "-expression-";
        var expectedTagName = "input";
        var expectedAttributes = new TagHelperAttributeList
            {
                { "name",  expectedAttributeValue },
                { "type", inputTypeName },
                { "value", "False" },
            };

        var metadataProvider = new EmptyModelMetadataProvider();
        var htmlGenerator = new TestableHtmlGenerator(metadataProvider);
        var model = false;
        var modelExplorer = metadataProvider.GetModelExplorerForType(typeof(bool), model);
        var modelExpression = new ModelExpression(name: string.Empty, modelExplorer: modelExplorer);
        var viewContext = TestableHtmlGenerator.GetViewContext(model, htmlGenerator, metadataProvider);
        viewContext.ClientValidationEnabled = false;

        var tagHelper = new InputTagHelper(htmlGenerator)
        {
            For = modelExpression,
            InputTypeName = inputTypeName,
            Name = expectedAttributeValue,
            ViewContext = viewContext,
        };

        var attributes = new TagHelperAttributeList
            {
                { "name", expectedAttributeValue },
                { "type", inputTypeName },
            };

        var context = new TagHelperContext(attributes, new Dictionary<object, object>(), "test");
        var output = new TagHelperOutput(
            expectedTagName,
            new TagHelperAttributeList(),
            getChildContentAsync: (useCachedResult, encoder) => Task.FromResult<TagHelperContent>(result: null))
        {
            TagMode = TagMode.SelfClosing,
        };

        // Act
        tagHelper.Process(context, output);

        // Assert
        Assert.Equal(expectedAttributes, output.Attributes);
        Assert.False(output.IsContentModified);
        Assert.Equal(expectedTagName, output.TagName);
    }

    [Fact]
    public void Process_Checkbox_WithEmptyForName_DoesNotThrow_WithName()
    {
        // Arrange
        var expectedAttributeValue = "-expression-";
        var expectedPostElementContent = $"<input name=\"HtmlEncode[[{expectedAttributeValue}]]\" " +
            "type=\"HtmlEncode[[hidden]]\" value=\"HtmlEncode[[false]]\" />";
        var expectedTagName = "input";
        var inputTypeName = "checkbox";
        var expectedAttributes = new TagHelperAttributeList
            {
                { "name",  expectedAttributeValue },
                { "type", inputTypeName },
                { "value", "true" },
            };

        var metadataProvider = new EmptyModelMetadataProvider();
        var htmlGenerator = new TestableHtmlGenerator(metadataProvider);
        var model = false;
        var modelExplorer = metadataProvider.GetModelExplorerForType(typeof(bool), model);
        var modelExpression = new ModelExpression(name: string.Empty, modelExplorer: modelExplorer);
        var viewContext = TestableHtmlGenerator.GetViewContext(model, htmlGenerator, metadataProvider);
        viewContext.ClientValidationEnabled = false;

        var tagHelper = new InputTagHelper(htmlGenerator)
        {
            For = modelExpression,
            InputTypeName = inputTypeName,
            Name = expectedAttributeValue,
            ViewContext = viewContext,
        };

        var attributes = new TagHelperAttributeList
            {
                { "name", expectedAttributeValue },
                { "type", inputTypeName },
            };

        var context = new TagHelperContext(attributes, new Dictionary<object, object>(), "test");
        var output = new TagHelperOutput(
            expectedTagName,
            new TagHelperAttributeList(),
            getChildContentAsync: (useCachedResult, encoder) => Task.FromResult<TagHelperContent>(result: null))
        {
            TagMode = TagMode.SelfClosing,
        };

        // Act
        tagHelper.Process(context, output);

        // Assert
        Assert.Equal(expectedAttributes, output.Attributes);
        Assert.False(output.IsContentModified);
        Assert.Equal(expectedTagName, output.TagName);

        Assert.False(viewContext.FormContext.HasEndOfFormContent);
        Assert.Equal(expectedPostElementContent, HtmlContentUtilities.HtmlContentToString(output.PostElement));
    }

    [Fact]
    public void Process_Password_WithEmptyForName_DoesNotThrow_WithName()
    {
        // Arrange
        var expectedAttributeValue = "-expression-";
        var expectedTagName = "input";
        var inputTypeName = "password";
        var expectedAttributes = new TagHelperAttributeList
            {
                { "name",  expectedAttributeValue },
                { "type", inputTypeName },
            };

        var metadataProvider = new EmptyModelMetadataProvider();
        var htmlGenerator = new TestableHtmlGenerator(metadataProvider);
        var model = "password";
        var modelExplorer = metadataProvider.GetModelExplorerForType(typeof(string), model);
        var modelExpression = new ModelExpression(name: string.Empty, modelExplorer: modelExplorer);
        var viewContext = TestableHtmlGenerator.GetViewContext(model, htmlGenerator, metadataProvider);
        viewContext.ClientValidationEnabled = false;

        var tagHelper = new InputTagHelper(htmlGenerator)
        {
            For = modelExpression,
            InputTypeName = inputTypeName,
            Name = expectedAttributeValue,
            ViewContext = viewContext,
        };

        // Expect attributes to just pass through. Tag helper binds all input attributes and doesn't add any.
        var context = new TagHelperContext(expectedAttributes, new Dictionary<object, object>(), "test");
        var output = new TagHelperOutput(
            expectedTagName,
            new TagHelperAttributeList(),
            getChildContentAsync: (useCachedResult, encoder) => Task.FromResult<TagHelperContent>(result: null))
        {
            TagMode = TagMode.SelfClosing,
        };

        // Act
        tagHelper.Process(context, output);

        // Assert
        Assert.Equal(expectedAttributes, output.Attributes);
        Assert.False(output.IsContentModified);
        Assert.Equal(expectedTagName, output.TagName);
    }

    [Fact]
    public void Process_Radio_WithEmptyForName_DoesNotThrow_WithName()
    {
        // Arrange
        var expectedAttributeValue = "-expression-";
        var expectedTagName = "input";
        var inputTypeName = "radio";
        var expectedAttributes = new TagHelperAttributeList
            {
                { "name",  expectedAttributeValue },
                { "type", inputTypeName },
                { "value", "24" },
            };

        var metadataProvider = new EmptyModelMetadataProvider();
        var htmlGenerator = new TestableHtmlGenerator(metadataProvider);
        var model = 23;
        var modelExplorer = metadataProvider.GetModelExplorerForType(typeof(int), model);
        var modelExpression = new ModelExpression(name: string.Empty, modelExplorer: modelExplorer);
        var viewContext = TestableHtmlGenerator.GetViewContext(model, htmlGenerator, metadataProvider);
        viewContext.ClientValidationEnabled = false;

        var tagHelper = new InputTagHelper(htmlGenerator)
        {
            For = modelExpression,
            InputTypeName = inputTypeName,
            Name = expectedAttributeValue,
            Value = "24",
            ViewContext = viewContext,
        };

        var attributes = new TagHelperAttributeList
            {
                { "name", expectedAttributeValue },
                { "type", inputTypeName },
                { "value", "24" },
            };

        var context = new TagHelperContext(attributes, new Dictionary<object, object>(), "test");
        var output = new TagHelperOutput(
            expectedTagName,
            new TagHelperAttributeList(),
            getChildContentAsync: (useCachedResult, encoder) => Task.FromResult<TagHelperContent>(result: null))
        {
            TagMode = TagMode.SelfClosing,
        };

        // Act
        tagHelper.Process(context, output);

        // Assert
        Assert.Equal(expectedAttributes, output.Attributes);
        Assert.False(output.IsContentModified);
        Assert.Equal(expectedTagName, output.TagName);
    }

    // Top-level container (List<Model> or Model instance), immediate container type (Model or NestModel),
    // model accessor, expression path / id, expected value.
    public static TheoryData<object, Type, object, NameAndId, string> TestDataSet
    {
        get
        {
            var modelWithNull = new Model
            {
                NestedModel = new NestedModel
                {
                    Text = null,
                },
                Text = null,
            };
            var modelWithText = new Model
            {
                NestedModel = new NestedModel
                {
                    Text = "inner text",
                },
                Text = "outer text",
            };
            var models = new List<Model>
                {
                    modelWithNull,
                    modelWithText,
                };

            return new TheoryData<object, Type, object, NameAndId, string>
                {
                    { null, typeof(Model), null, new NameAndId("Text", "Text"),
                        string.Empty },

                    { modelWithNull, typeof(Model), modelWithNull.Text, new NameAndId("Text", "Text"),
                        string.Empty },
                    { modelWithText, typeof(Model), modelWithText.Text, new NameAndId("Text", "Text"),
                        "outer text" },

                    { modelWithNull, typeof(NestedModel), modelWithNull.NestedModel.Text,
                        new NameAndId("NestedModel.Text", "NestedModel_Text"), string.Empty },
                    { modelWithText, typeof(NestedModel), modelWithText.NestedModel.Text,
                        new NameAndId("NestedModel.Text", "NestedModel_Text"), "inner text" },

                    { models, typeof(Model), models[0].Text,
                        new NameAndId("[0].Text", "z0__Text"), string.Empty },
                    { models, typeof(Model), models[1].Text,
                        new NameAndId("[1].Text", "z1__Text"), "outer text" },

                    { models, typeof(NestedModel), models[0].NestedModel.Text,
                        new NameAndId("[0].NestedModel.Text", "z0__NestedModel_Text"), string.Empty },
                    { models, typeof(NestedModel), models[1].NestedModel.Text,
                        new NameAndId("[1].NestedModel.Text", "z1__NestedModel_Text"), "inner text" },
                };
        }
    }

    [Theory]
    [MemberData(nameof(TestDataSet))]
    public async Task ProcessAsync_GeneratesExpectedOutput(
        object container,
        Type containerType,
        object model,
        NameAndId nameAndId,
        string expectedValue)
    {
        // Arrange
        var expectedAttributes = new TagHelperAttributeList
            {
                { "class", "form-control" },
                { "type", "text" },
                { "id", nameAndId.Id },
                { "name", nameAndId.Name },
                { "valid", "from validation attributes" },
                { "value", expectedValue },
            };
        var expectedPreContent = "original pre-content";
        var expectedContent = "original content";
        var expectedPostContent = "original post-content";
        var expectedTagName = "not-input";

        var context = new TagHelperContext(
            tagName: "input",
            allAttributes: new TagHelperAttributeList(
                Enumerable.Empty<TagHelperAttribute>()),
            items: new Dictionary<object, object>(),
            uniqueId: "test");
        var originalAttributes = new TagHelperAttributeList
            {
                { "class", "form-control" },
            };
        var output = new TagHelperOutput(
            expectedTagName,
            originalAttributes,
            getChildContentAsync: (useCachedResult, encoder) =>
            {
                var tagHelperContent = new DefaultTagHelperContent();
                tagHelperContent.SetContent("Something");
                return Task.FromResult<TagHelperContent>(tagHelperContent);
            })
        {
            TagMode = TagMode.StartTagOnly,
        };
        output.PreContent.SetContent(expectedPreContent);
        output.Content.SetContent(expectedContent);
        output.PostContent.SetContent(expectedPostContent);

        var htmlGenerator = new TestableHtmlGenerator(new EmptyModelMetadataProvider())
        {
            ValidationAttributes =
                {
                    {  "valid", "from validation attributes" },
                }
        };

        // Property name is either nameof(Model.Text) or nameof(NestedModel.Text).
        var tagHelper = GetTagHelper(
            htmlGenerator,
            container,
            containerType,
            model,
            propertyName: nameof(Model.Text),
            expressionName: nameAndId.Name);

        // Act
        await tagHelper.ProcessAsync(context, output);

        // Assert
        Assert.Equal(expectedAttributes, output.Attributes);
        Assert.Equal(expectedPreContent, output.PreContent.GetContent());
        Assert.Equal(expectedContent, output.Content.GetContent());
        Assert.Equal(expectedPostContent, output.PostContent.GetContent());
        Assert.Equal(TagMode.StartTagOnly, output.TagMode);
        Assert.Equal(expectedTagName, output.TagName);
    }

    [Theory]
    [InlineData("datetime", "datetime")]
    [InlineData(null, "text")]
    [InlineData("hidden", "hidden")]
    public void Process_GeneratesFormattedOutput(string specifiedType, string expectedType)
    {
        // Arrange
        var expectedAttributes = new TagHelperAttributeList
            {
                { "type", expectedType },
                { "id", "DateTimeOffset" },
                { "name", "DateTimeOffset" },
                { "valid", "from validation attributes" },
                { "value", "datetime: 2011-08-31T05:30:45.0000000+03:00" },
            };
        var expectedTagName = "not-input";
        var container = new Model
        {
            DateTimeOffset = new DateTimeOffset(2011, 8, 31, hour: 5, minute: 30, second: 45, offset: TimeSpan.FromHours(3))
        };

        var allAttributes = new TagHelperAttributeList
            {
                { "type", specifiedType },
            };
        var context = new TagHelperContext(
            tagName: "input",
            allAttributes: allAttributes,
            items: new Dictionary<object, object>(),
            uniqueId: "test");
        var output = new TagHelperOutput(
            expectedTagName,
            new TagHelperAttributeList(),
            getChildContentAsync: (useCachedResult, encoder) =>
            {
                throw new Exception("getChildContentAsync should not be called.");
            })
        {
            TagMode = TagMode.StartTagOnly,
        };

        var htmlGenerator = new TestableHtmlGenerator(new EmptyModelMetadataProvider())
        {
            ValidationAttributes =
                {
                    {  "valid", "from validation attributes" },
                }
        };

        var tagHelper = GetTagHelper(
            htmlGenerator,
            container,
            typeof(Model),
            model: container.DateTimeOffset,
            propertyName: nameof(Model.DateTimeOffset),
            expressionName: nameof(Model.DateTimeOffset));
        tagHelper.Format = "datetime: {0:o}";
        tagHelper.InputTypeName = specifiedType;

        // Act
        tagHelper.Process(context, output);

        // Assert
        Assert.Equal(expectedAttributes, output.Attributes);
        Assert.Empty(output.PreContent.GetContent());
        Assert.Empty(output.Content.GetContent());
        Assert.Empty(output.PostContent.GetContent());
        Assert.Equal(TagMode.StartTagOnly, output.TagMode);
        Assert.Equal(expectedTagName, output.TagName);
    }

    [Theory]
    [InlineData("datetime", "datetime")]
    [InlineData(null, "datetime-local")]
    [InlineData("hidden", "hidden")]
    public void Process_GeneratesFormattedOutput_ForDateTime(string specifiedType, string expectedType)
    {
        // Arrange
        var expectedAttributes = new TagHelperAttributeList
            {
                { "type", expectedType },
                { "id", nameof(Model.DateTime) },
                { "name", nameof(Model.DateTime) },
                { "valid", "from validation attributes" },
                { "value", "datetime: 2011-08-31T05:30:45.0000000Z" },
            };
        var expectedTagName = "not-input";
        var container = new Model
        {
            DateTime = new DateTime(2011, 8, 31, hour: 5, minute: 30, second: 45, kind: DateTimeKind.Utc),
        };

        var allAttributes = new TagHelperAttributeList
            {
                { "type", specifiedType },
            };
        var context = new TagHelperContext(
            tagName: "input",
            allAttributes: allAttributes,
            items: new Dictionary<object, object>(),
            uniqueId: "test");
        var output = new TagHelperOutput(
            expectedTagName,
            new TagHelperAttributeList(),
            getChildContentAsync: (useCachedResult, encoder) =>
            {
                throw new Exception("getChildContentAsync should not be called.");
            })
        {
            TagMode = TagMode.StartTagOnly,
        };

        var htmlGenerator = new TestableHtmlGenerator(new EmptyModelMetadataProvider())
        {
            ValidationAttributes =
                {
                    {  "valid", "from validation attributes" },
                }
        };

        var tagHelper = GetTagHelper(
            htmlGenerator,
            container,
            typeof(Model),
            model: container.DateTime,
            propertyName: nameof(Model.DateTime),
            expressionName: nameof(Model.DateTime));
        tagHelper.Format = "datetime: {0:o}";
        tagHelper.InputTypeName = specifiedType;

        // Act
        tagHelper.Process(context, output);

        // Assert
        Assert.Equal(expectedAttributes, output.Attributes);
        Assert.Empty(output.PreContent.GetContent());
        Assert.Empty(output.Content.GetContent());
        Assert.Empty(output.PostContent.GetContent());
        Assert.Equal(TagMode.StartTagOnly, output.TagMode);
        Assert.Equal(expectedTagName, output.TagName);
    }

    [Theory]
    [InlineData("SomeProperty", "SomeProperty", true)]
    [InlineData("SomeProperty", "[0].SomeProperty", true)]
    [InlineData("SomeProperty", "[0].SomeProperty", false)]
    public void Process_GeneratesInvariantCultureMetadataInput_WhenValueUsesInvariantFormatting(string propertyName, string nameAttributeValue, bool usesInvariantFormatting)
    {
        // Arrange
        var metadataProvider = new EmptyModelMetadataProvider();
        var htmlGenerator = new Mock<IHtmlGenerator>(MockBehavior.Strict);
        var model = false;
        var modelExplorer = metadataProvider.GetModelExplorerForType(typeof(bool), model);
        var modelExpression = new ModelExpression(name: string.Empty, modelExplorer: modelExplorer);
        var viewContext = TestableHtmlGenerator.GetViewContext(model, htmlGenerator.Object, metadataProvider);
        var tagHelper = new InputTagHelper(htmlGenerator.Object)
        {
            For = modelExpression,
            InputTypeName = "text",
            Name = propertyName,
            ViewContext = viewContext,
        };

        var tagBuilder = new TagBuilder("input")
        {
            TagRenderMode = TagRenderMode.SelfClosing,
            Attributes =
            {
                { "name", nameAttributeValue },
            },
        };

        htmlGenerator
            .Setup(mock => mock.GenerateTextBox(
                tagHelper.ViewContext,
                tagHelper.For.ModelExplorer,
                tagHelper.For.Name,
                modelExplorer.Model,
                null,                   // format
                It.IsAny<object>()))    // htmlAttributes
            .Returns(tagBuilder)
            .Callback(() => viewContext.FormContext.InvariantField(tagBuilder.Attributes["name"], usesInvariantFormatting))
            .Verifiable();

        var expectedPostElement = usesInvariantFormatting
            ? $"<input name=\"__Invariant\" type=\"hidden\" value=\"{tagBuilder.Attributes["name"]}\" />"
            : string.Empty;

        var attributes = new TagHelperAttributeList
            {
                { "name", propertyName },
                { "type", "text" },
            };
        var context = new TagHelperContext(attributes, new Dictionary<object, object>(), "test");
        var output = new TagHelperOutput(
            "input",
            new TagHelperAttributeList(),
            getChildContentAsync: (useCachedResult, encoder) => Task.FromResult<TagHelperContent>(result: null))
        {
            TagMode = TagMode.SelfClosing,
        };

        // Act
        tagHelper.Process(context, output);

        // Assert
        htmlGenerator.Verify();

        Assert.Equal(expectedPostElement, output.PostElement.GetContent());
    }

    [Fact]
    public async Task ProcessAsync_GenerateCheckBox_WithHiddenInputRenderModeNone()
    {
        var propertyName = "-expression-";
        var expectedTagName = "input";
        var inputTypeName = "checkbox";
        var expectedAttributes = new TagHelperAttributeList
            {
                { "name",  propertyName },
                { "type", inputTypeName },
                { "value", "true" },
            };

        var metadataProvider = new EmptyModelMetadataProvider();
        var htmlGenerator = new TestableHtmlGenerator(metadataProvider);
        var model = false;
        var modelExplorer = metadataProvider.GetModelExplorerForType(typeof(bool), model);
        var modelExpression = new ModelExpression(name: string.Empty, modelExplorer: modelExplorer);
        var viewContext = TestableHtmlGenerator.GetViewContext(model, htmlGenerator, metadataProvider);

        viewContext.CheckBoxHiddenInputRenderMode = CheckBoxHiddenInputRenderMode.None;

        var tagHelper = new InputTagHelper(htmlGenerator)
        {
            For = modelExpression,
            InputTypeName = inputTypeName,
            Name = propertyName,
            ViewContext = viewContext,
        };

        var attributes = new TagHelperAttributeList
            {
                { "name", propertyName },
                { "type", inputTypeName },
            };

        var context = new TagHelperContext(attributes, new Dictionary<object, object>(), "test");
        var output = new TagHelperOutput(
            expectedTagName,
            new TagHelperAttributeList(),
            getChildContentAsync: (useCachedResult, encoder) => Task.FromResult<TagHelperContent>(result: null))
        {
            TagMode = TagMode.SelfClosing,
        };

        // Act
        await tagHelper.ProcessAsync(context, output);

        // Assert
        Assert.Equal(expectedAttributes, output.Attributes);
        Assert.False(output.IsContentModified);
        Assert.Equal(expectedTagName, output.TagName);

        Assert.False(viewContext.FormContext.HasEndOfFormContent);
        Assert.True(string.IsNullOrEmpty(HtmlContentUtilities.HtmlContentToString(output.PostElement)));
    }

    [Fact]
    public async Task ProcessAsync_GenerateCheckBox_WithHiddenInputRenderModeInline()
    {
        var propertyName = "-expression-";
        var expectedTagName = "input";
        var expectedPostElementContent = $"<input name=\"HtmlEncode[[{propertyName}]]\" " +
            "type=\"HtmlEncode[[hidden]]\" value=\"HtmlEncode[[false]]\" />";
        var inputTypeName = "checkbox";
        var expectedAttributes = new TagHelperAttributeList
            {
                { "name",  propertyName },
                { "type", inputTypeName },
                { "value", "true" },
            };

        var metadataProvider = new EmptyModelMetadataProvider();
        var htmlGenerator = new TestableHtmlGenerator(metadataProvider);
        var model = false;
        var modelExplorer = metadataProvider.GetModelExplorerForType(typeof(bool), model);
        var modelExpression = new ModelExpression(name: string.Empty, modelExplorer: modelExplorer);
        var viewContext = TestableHtmlGenerator.GetViewContext(model, htmlGenerator, metadataProvider);

        viewContext.FormContext.CanRenderAtEndOfForm = true;
        viewContext.CheckBoxHiddenInputRenderMode = CheckBoxHiddenInputRenderMode.Inline;

        var tagHelper = new InputTagHelper(htmlGenerator)
        {
            For = modelExpression,
            InputTypeName = inputTypeName,
            Name = propertyName,
            ViewContext = viewContext,
        };

        var attributes = new TagHelperAttributeList
            {
                { "name", propertyName },
                { "type", inputTypeName },
            };

        var context = new TagHelperContext(attributes, new Dictionary<object, object>(), "test");
        var output = new TagHelperOutput(
            expectedTagName,
            new TagHelperAttributeList(),
            getChildContentAsync: (useCachedResult, encoder) => Task.FromResult<TagHelperContent>(result: null))
        {
            TagMode = TagMode.SelfClosing,
        };

        // Act
        await tagHelper.ProcessAsync(context, output);

        // Assert
        Assert.Equal(expectedAttributes, output.Attributes);
        Assert.False(output.IsContentModified);
        Assert.Equal(expectedTagName, output.TagName);

        Assert.False(viewContext.FormContext.HasEndOfFormContent);
        Assert.Equal(expectedPostElementContent, HtmlContentUtilities.HtmlContentToString(output.PostElement));
    }

    [Fact]
    public async Task ProcessAsync_GenerateCheckBox_WithHiddenInputRenderModeEndOfForm()
    {
        var propertyName = "-expression-";
        var expectedTagName = "input";
        var expectedEndOfFormContent = $"<input name=\"HtmlEncode[[{propertyName}]]\" " +
            "type=\"HtmlEncode[[hidden]]\" value=\"HtmlEncode[[false]]\" />";
        var inputTypeName = "checkbox";
        var expectedAttributes = new TagHelperAttributeList
            {
                { "name",  propertyName },
                { "type", inputTypeName },
                { "value", "true" },
            };

        var metadataProvider = new EmptyModelMetadataProvider();
        var htmlGenerator = new TestableHtmlGenerator(metadataProvider);
        var model = false;
        var modelExplorer = metadataProvider.GetModelExplorerForType(typeof(bool), model);
        var modelExpression = new ModelExpression(name: string.Empty, modelExplorer: modelExplorer);
        var viewContext = TestableHtmlGenerator.GetViewContext(model, htmlGenerator, metadataProvider);

        viewContext.FormContext.CanRenderAtEndOfForm = true;
        viewContext.CheckBoxHiddenInputRenderMode = CheckBoxHiddenInputRenderMode.EndOfForm;

        var tagHelper = new InputTagHelper(htmlGenerator)
        {
            For = modelExpression,
            InputTypeName = inputTypeName,
            Name = propertyName,
            ViewContext = viewContext,
        };

        var attributes = new TagHelperAttributeList
            {
                { "name", propertyName },
                { "type", inputTypeName },
            };

        var context = new TagHelperContext(attributes, new Dictionary<object, object>(), "test");
        var output = new TagHelperOutput(
            expectedTagName,
            new TagHelperAttributeList(),
            getChildContentAsync: (useCachedResult, encoder) => Task.FromResult<TagHelperContent>(result: null))
        {
            TagMode = TagMode.SelfClosing,
        };

        // Act
        await tagHelper.ProcessAsync(context, output);

        // Assert
        Assert.Equal(expectedAttributes, output.Attributes);
        Assert.False(output.IsContentModified);
        Assert.Equal(expectedTagName, output.TagName);

        Assert.Equal(expectedEndOfFormContent, string.Join("", viewContext.FormContext.EndOfFormContent.Select(html => HtmlContentUtilities.HtmlContentToString(html))));
        Assert.True(string.IsNullOrEmpty(HtmlContentUtilities.HtmlContentToString(output.PostElement)));
    }

    [Fact]
    public async Task ProcessAsync_GenerateCheckBox_WithHiddenInputRenderModeEndOfForm_AndCanRenderAtEndOfFormNotSet()
    {
        var propertyName = "-expression-";
        var expectedTagName = "input";
        var expectedPostElementContent = $"<input name=\"HtmlEncode[[{propertyName}]]\" " +
            "type=\"HtmlEncode[[hidden]]\" value=\"HtmlEncode[[false]]\" />";
        var inputTypeName = "checkbox";
        var expectedAttributes = new TagHelperAttributeList
            {
                { "name",  propertyName },
                { "type", inputTypeName },
                { "value", "true" },
            };

        var metadataProvider = new EmptyModelMetadataProvider();
        var htmlGenerator = new TestableHtmlGenerator(metadataProvider);
        var model = false;
        var modelExplorer = metadataProvider.GetModelExplorerForType(typeof(bool), model);
        var modelExpression = new ModelExpression(name: string.Empty, modelExplorer: modelExplorer);
        var viewContext = TestableHtmlGenerator.GetViewContext(model, htmlGenerator, metadataProvider);

        viewContext.FormContext.CanRenderAtEndOfForm = false;
        viewContext.CheckBoxHiddenInputRenderMode = CheckBoxHiddenInputRenderMode.EndOfForm;

        var tagHelper = new InputTagHelper(htmlGenerator)
        {
            For = modelExpression,
            InputTypeName = inputTypeName,
            Name = propertyName,
            ViewContext = viewContext,
        };

        var attributes = new TagHelperAttributeList
            {
                { "name", propertyName },
                { "type", inputTypeName },
            };

        var context = new TagHelperContext(attributes, new Dictionary<object, object>(), "test");
        var output = new TagHelperOutput(
            expectedTagName,
            new TagHelperAttributeList(),
            getChildContentAsync: (useCachedResult, encoder) => Task.FromResult<TagHelperContent>(result: null))
        {
            TagMode = TagMode.SelfClosing,
        };

        // Act
        await tagHelper.ProcessAsync(context, output);

        // Assert
        Assert.Equal(expectedAttributes, output.Attributes);
        Assert.False(output.IsContentModified);
        Assert.Equal(expectedTagName, output.TagName);

        Assert.False(viewContext.FormContext.HasEndOfFormContent);
        Assert.Equal(expectedPostElementContent, HtmlContentUtilities.HtmlContentToString(output.PostElement));
    }

    [Fact]
    public async Task ProcessAsync_CallsGenerateCheckBox_WithExpectedParameters()
    {
        // Arrange
        var originalContent = "original content";
        var expectedPreContent = "original pre-content";
        var expectedContent = "<input class=\"HtmlEncode[[form-control]]\" type=\"HtmlEncode[[checkbox]]\" /><hidden />";
        var expectedPostContent = "original post-content";
        var expectedPostElement = "<hidden />";

        var context = new TagHelperContext(
            tagName: "input",
            allAttributes: new TagHelperAttributeList(
                Enumerable.Empty<TagHelperAttribute>()),
            items: new Dictionary<object, object>(),
            uniqueId: "test");
        var originalAttributes = new TagHelperAttributeList
            {
                { "class", "form-control" },
            };
        var output = new TagHelperOutput(
            "input",
            originalAttributes,
            getChildContentAsync: (useCachedResult, encoder) =>
            {
                var tagHelperContent = new DefaultTagHelperContent();
                tagHelperContent.SetContent("Something");
                return Task.FromResult<TagHelperContent>(tagHelperContent);
            })
        {
            TagMode = TagMode.SelfClosing,
        };
        output.PreContent.AppendHtml(expectedPreContent);
        output.Content.AppendHtml(originalContent);
        output.PostContent.AppendHtml(expectedPostContent);

        var htmlGenerator = new Mock<IHtmlGenerator>(MockBehavior.Strict);
        var tagHelper = GetTagHelper(htmlGenerator.Object, model: false, propertyName: nameof(Model.IsACar));
        tagHelper.Format = "somewhat-less-null"; // ignored

        var tagBuilder = new TagBuilder("input")
        {
            TagRenderMode = TagRenderMode.SelfClosing
        };
        htmlGenerator
            .Setup(mock => mock.GenerateCheckBox(
                tagHelper.ViewContext,
                tagHelper.For.ModelExplorer,
                tagHelper.For.Name,
                null,                   // isChecked
                It.IsAny<object>()))    // htmlAttributes
            .Returns(tagBuilder)
            .Verifiable();
        htmlGenerator
            .Setup(mock => mock.GenerateHiddenForCheckbox(
                tagHelper.ViewContext,
                tagHelper.For.ModelExplorer,
                tagHelper.For.Name))
            .Returns(new TagBuilder("hidden") { TagRenderMode = TagRenderMode.SelfClosing })
            .Verifiable();

        // Act
        await tagHelper.ProcessAsync(context, output);

        // Assert
        htmlGenerator.Verify();

        Assert.NotEmpty(output.Attributes);
        Assert.Equal(expectedPreContent, output.PreContent.GetContent());
        Assert.Equal(originalContent, HtmlContentUtilities.HtmlContentToString(output.Content));
        Assert.Equal(expectedContent, HtmlContentUtilities.HtmlContentToString(output));
        Assert.Equal(expectedPostContent, output.PostContent.GetContent());
        Assert.Equal(expectedPostElement, output.PostElement.GetContent());
        Assert.Equal(TagMode.SelfClosing, output.TagMode);
    }

    [Theory]
    [InlineData(null, "hidden", null, null)]
    [InlineData(null, "Hidden", "not-null", "somewhat-less-null")]
    [InlineData(null, "HIDden", null, "somewhat-less-null")]
    [InlineData(null, "HIDDEN", "not-null", null)]
    [InlineData("hiddeninput", null, null, null)]
    [InlineData("HiddenInput", null, "not-null", null)]
    [InlineData("hidDENinPUT", null, null, "somewhat-less-null")]
    [InlineData("HIDDENINPUT", null, "not-null", "somewhat-less-null")]
    public async Task ProcessAsync_CallsGenerateTextBox_WithExpectedParametersForHidden(
        string dataTypeName,
        string inputTypeName,
        string model,
        string format)
    {
        // Arrange
        var contextAttributes = new TagHelperAttributeList
            {
                { "class", "form-control" },
            };
        if (!string.IsNullOrEmpty(inputTypeName))
        {
            contextAttributes.SetAttribute("type", inputTypeName);  // Support restoration of type attribute, if any.
        }

        var expectedAttributes = new TagHelperAttributeList
            {
                { "class", "form-control hidden-control" },
                { "type", inputTypeName ?? "hidden" },      // Generator restores type attribute; adds "hidden" if none.
            };
        var expectedPreContent = "original pre-content";
        var expectedContent = "original content";
        var expectedPostContent = "original post-content";
        var expectedTagName = "not-input";

        var context = new TagHelperContext(
            tagName: "input",
            allAttributes: contextAttributes,
            items: new Dictionary<object, object>(),
            uniqueId: "test");
        var originalAttributes = new TagHelperAttributeList
            {
                { "class", "form-control" },
            };
        var output = new TagHelperOutput(
            expectedTagName,
            originalAttributes,
            getChildContentAsync: (useCachedResult, encoder) =>
            {
                var tagHelperContent = new DefaultTagHelperContent();
                tagHelperContent.SetContent("Something");
                return Task.FromResult<TagHelperContent>(tagHelperContent);
            })
        {
            TagMode = TagMode.StartTagOnly,
        };
        output.PreContent.SetContent(expectedPreContent);
        output.Content.SetContent(expectedContent);
        output.PostContent.SetContent(expectedPostContent);

        var metadataProvider = new TestModelMetadataProvider();
        metadataProvider.ForProperty<Model>("Text").DisplayDetails(dd => dd.DataTypeName = dataTypeName);

        var htmlGenerator = new Mock<IHtmlGenerator>(MockBehavior.Strict);
        var tagHelper = GetTagHelper(
            htmlGenerator.Object,
            model,
            nameof(Model.Text),
            metadataProvider: metadataProvider);
        tagHelper.Format = format;
        tagHelper.InputTypeName = inputTypeName;

        var tagBuilder = new TagBuilder("input")
        {
            Attributes =
                {
                    { "class", "hidden-control" },
                },
        };
        htmlGenerator
            .Setup(mock => mock.GenerateTextBox(
                tagHelper.ViewContext,
                tagHelper.For.ModelExplorer,
                tagHelper.For.Name,
                model,  // value
                format,
                new Dictionary<string, object> { { "type", "hidden" } }))   // htmlAttributes
            .Returns(tagBuilder)
            .Verifiable();

        // Act
        await tagHelper.ProcessAsync(context, output);

        // Assert
        htmlGenerator.Verify();

        Assert.Equal(TagMode.StartTagOnly, output.TagMode);
        Assert.Equal(expectedAttributes, output.Attributes, CaseSensitiveTagHelperAttributeComparer.Default);
        Assert.Equal(expectedPreContent, output.PreContent.GetContent());
        Assert.Equal(expectedContent, output.Content.GetContent());
        Assert.Equal(expectedPostContent, output.PostContent.GetContent());
        Assert.Equal(expectedTagName, output.TagName);
    }

    [Fact]
    public async Task ProcessAsync_GenerateCheckBox_WithHiddenInputExpectedFormAttribute()
    {
        var propertyName = "-expression-";
        var expectedTagName = "input";
        var formName = "form-name";
        var expectedEndOfFormContent = $"<input form=\"HtmlEncode[[{formName}]]\" name=\"HtmlEncode[[{propertyName}]]\" " +
            "type=\"HtmlEncode[[hidden]]\" value=\"HtmlEncode[[false]]\" />";
        var inputTypeName = "checkbox";
        var expectedAttributes = new TagHelperAttributeList
            {
                { "name",  propertyName },
                { "type", inputTypeName },
                { "form", formName },
                { "value", "true" },
            };

        var metadataProvider = new EmptyModelMetadataProvider();
        var htmlGenerator = new TestableHtmlGenerator(metadataProvider);
        var model = false;
        var modelExplorer = metadataProvider.GetModelExplorerForType(typeof(bool), model);
        var modelExpression = new ModelExpression(name: string.Empty, modelExplorer: modelExplorer);
        var viewContext = TestableHtmlGenerator.GetViewContext(model, htmlGenerator, metadataProvider);
        viewContext.FormContext.CanRenderAtEndOfForm = true;
        viewContext.CheckBoxHiddenInputRenderMode = CheckBoxHiddenInputRenderMode.EndOfForm;

        var tagHelper = new InputTagHelper(htmlGenerator)
        {
            For = modelExpression,
            InputTypeName = inputTypeName,
            Name = propertyName,
            FormName = formName,
            ViewContext = viewContext,
        };

        var attributes = new TagHelperAttributeList
            {
                { "name", propertyName },
                { "type", inputTypeName },
                { "form", formName}
            };

        var context = new TagHelperContext(attributes, new Dictionary<object, object>(), "test");
        var output = new TagHelperOutput(
            expectedTagName,
            new TagHelperAttributeList(),
            getChildContentAsync: (useCachedResult, encoder) => Task.FromResult<TagHelperContent>(result: null))
        {
            TagMode = TagMode.SelfClosing,
        };

        // Act
        await tagHelper.ProcessAsync(context, output);

        // Assert
        Assert.Equal(expectedAttributes, output.Attributes);
        Assert.False(output.IsContentModified);
        Assert.Equal(expectedTagName, output.TagName);

        Assert.Equal(expectedEndOfFormContent, string.Join("", viewContext.FormContext.EndOfFormContent.Select(html => HtmlContentUtilities.HtmlContentToString(html))));
        Assert.True(string.IsNullOrEmpty(HtmlContentUtilities.HtmlContentToString(output.PostElement)));
    }

    [Theory]
    [InlineData(null, "password", null)]
    [InlineData(null, "Password", "not-null")]
    [InlineData(null, "PASSword", null)]
    [InlineData(null, "PASSWORD", "not-null")]
    [InlineData("password", null, null)]
    [InlineData("Password", null, "not-null")]
    [InlineData("PASSword", null, null)]
    [InlineData("PASSWORD", null, "not-null")]
    public async Task ProcessAsync_CallsGeneratePassword_WithExpectedParameters(
        string dataTypeName,
        string inputTypeName,
        string model)
    {
        // Arrange
        var contextAttributes = new TagHelperAttributeList
            {
                { "class", "form-control" },
            };
        if (!string.IsNullOrEmpty(inputTypeName))
        {
            contextAttributes.SetAttribute("type", inputTypeName);  // Support restoration of type attribute, if any.
        }

        var expectedAttributes = new TagHelperAttributeList
            {
                { "class", "form-control password-control" },
                { "type", inputTypeName ?? "password" },    // Generator restores type attribute; adds "password" if none.
            };
        var expectedPreContent = "original pre-content";
        var expectedContent = "original content";
        var expectedPostContent = "original post-content";
        var expectedTagName = "not-input";

        var context = new TagHelperContext(
            tagName: "input",
            allAttributes: contextAttributes,
            items: new Dictionary<object, object>(),
            uniqueId: "test");
        var originalAttributes = new TagHelperAttributeList
            {
                { "class", "form-control" },
            };
        var output = new TagHelperOutput(
            expectedTagName,
            originalAttributes,
            getChildContentAsync: (useCachedResult, encoder) =>
            {
                var tagHelperContent = new DefaultTagHelperContent();
                tagHelperContent.SetContent("Something");
                return Task.FromResult<TagHelperContent>(tagHelperContent);
            })
        {
            TagMode = TagMode.StartTagOnly,
        };
        output.PreContent.SetContent(expectedPreContent);
        output.Content.SetContent(expectedContent);
        output.PostContent.SetContent(expectedPostContent);

        var metadataProvider = new TestModelMetadataProvider();
        metadataProvider.ForProperty<Model>("Text").DisplayDetails(dd => dd.DataTypeName = dataTypeName);

        var htmlGenerator = new Mock<IHtmlGenerator>(MockBehavior.Strict);
        var tagHelper = GetTagHelper(
            htmlGenerator.Object,
            model,
            nameof(Model.Text),
            metadataProvider: metadataProvider);
        tagHelper.Format = "somewhat-less-null"; // ignored
        tagHelper.InputTypeName = inputTypeName;

        var tagBuilder = new TagBuilder("input")
        {
            Attributes =
                {
                    { "class", "password-control" },
                },
        };
        htmlGenerator
            .Setup(mock => mock.GeneratePassword(
                tagHelper.ViewContext,
                tagHelper.For.ModelExplorer,
                tagHelper.For.Name,
                null,       // value
                null))      // htmlAttributes
            .Returns(tagBuilder)
            .Verifiable();

        // Act
        await tagHelper.ProcessAsync(context, output);

        // Assert
        htmlGenerator.Verify();

        Assert.Equal(TagMode.StartTagOnly, output.TagMode);
        Assert.Equal(expectedAttributes, output.Attributes, CaseSensitiveTagHelperAttributeComparer.Default);
        Assert.Equal(expectedPreContent, output.PreContent.GetContent());
        Assert.Equal(expectedContent, output.Content.GetContent());
        Assert.Equal(expectedPostContent, output.PostContent.GetContent());
        Assert.Equal(expectedTagName, output.TagName);
    }

    [Theory]
    [InlineData("radio", null)]
    [InlineData("Radio", "not-null")]
    [InlineData("RADio", null)]
    [InlineData("RADIO", "not-null")]
    public async Task ProcessAsync_CallsGenerateRadioButton_WithExpectedParameters(
        string inputTypeName,
        string model)
    {
        // Arrange
        var value = "match";            // Real generator would use this for comparison with For.Metadata.Model.
        var contextAttributes = new TagHelperAttributeList
            {
                { "class", "form-control" },
                { "value", value },
            };
        if (!string.IsNullOrEmpty(inputTypeName))
        {
            contextAttributes.SetAttribute("type", inputTypeName);  // Support restoration of type attribute, if any.
        }

        var expectedAttributes = new TagHelperAttributeList
            {
                { "class", "form-control radio-control" },
                { "value", value },
                { "type", inputTypeName ?? "radio" },       // Generator restores type attribute; adds "radio" if none.
            };
        var expectedPreContent = "original pre-content";
        var expectedContent = "original content";
        var expectedPostContent = "original post-content";
        var expectedTagName = "not-input";

        var context = new TagHelperContext(
            tagName: "input",
            allAttributes: contextAttributes,
            items: new Dictionary<object, object>(),
            uniqueId: "test");
        var originalAttributes = new TagHelperAttributeList
            {
                { "class", "form-control" },
            };
        var output = new TagHelperOutput(
            expectedTagName,
            originalAttributes,
            getChildContentAsync: (useCachedResult, encoder) =>
            {
                var tagHelperContent = new DefaultTagHelperContent();
                tagHelperContent.SetContent("Something");
                return Task.FromResult<TagHelperContent>(tagHelperContent);
            })
        {
            TagMode = TagMode.StartTagOnly,
        };
        output.PreContent.SetContent(expectedPreContent);
        output.Content.SetContent(expectedContent);
        output.PostContent.SetContent(expectedPostContent);

        var htmlGenerator = new Mock<IHtmlGenerator>(MockBehavior.Strict);
        var tagHelper = GetTagHelper(htmlGenerator.Object, model, nameof(Model.Text));
        tagHelper.Format = "somewhat-less-null"; // ignored
        tagHelper.InputTypeName = inputTypeName;
        tagHelper.Value = value;

        var tagBuilder = new TagBuilder("input")
        {
            Attributes =
                {
                    { "class", "radio-control" },
                },
        };
        htmlGenerator
            .Setup(mock => mock.GenerateRadioButton(
                tagHelper.ViewContext,
                tagHelper.For.ModelExplorer,
                tagHelper.For.Name,
                value,
                null,       // isChecked
                null))      // htmlAttributes
            .Returns(tagBuilder)
            .Verifiable();

        // Act
        await tagHelper.ProcessAsync(context, output);

        // Assert
        htmlGenerator.Verify();

        Assert.Equal(TagMode.StartTagOnly, output.TagMode);
        Assert.Equal(expectedAttributes, output.Attributes, CaseSensitiveTagHelperAttributeComparer.Default);
        Assert.Equal(expectedPreContent, output.PreContent.GetContent());
        Assert.Equal(expectedContent, output.Content.GetContent());
        Assert.Equal(expectedPostContent, output.PostContent.GetContent());
        Assert.Equal(expectedTagName, output.TagName);
    }

    [Theory]
    [InlineData(null, null, null, "somewhat-less-null")]
    [InlineData(null, null, "not-null", null)]
    [InlineData(null, "string", null, null)]
    [InlineData(null, "String", "not-null", null)]
    [InlineData(null, "STRing", null, "somewhat-less-null")]
    [InlineData(null, "STRING", "not-null", null)]
    [InlineData(null, "text", null, null)]
    [InlineData(null, "Text", "not-null", "somewhat-less-null")]
    [InlineData(null, "TExt", null, null)]
    [InlineData(null, "TEXT", "not-null", null)]
    [InlineData("string", null, null, null)]
    [InlineData("String", null, "not-null", null)]
    [InlineData("STRing", null, null, null)]
    [InlineData("STRING", null, "not-null", null)]
    [InlineData("text", null, null, null)]
    [InlineData("Text", null, "not-null", null)]
    [InlineData("TExt", null, null, null)]
    [InlineData("TEXT", null, "not-null", null)]
    [InlineData("custom-datatype", null, null, null)]
    [InlineData(null, "unknown-input-type", "not-null", null)]
    [InlineData("Image", null, "not-null", "somewhat-less-null")]
    [InlineData(null, "image", "not-null", null)]
    public async Task ProcessAsync_CallsGenerateTextBox_WithExpectedParameters(
        string dataTypeName,
        string inputTypeName,
        string model,
        string format)
    {
        // Arrange
        var contextAttributes = new TagHelperAttributeList
            {
                { "class", "form-control" },
            };
        if (!string.IsNullOrEmpty(inputTypeName))
        {
            contextAttributes.SetAttribute("type", inputTypeName);  // Support restoration of type attribute, if any.
        }

        var expectedAttributes = new TagHelperAttributeList
            {
                { "class", "form-control text-control" },
                { "type", inputTypeName ?? "text" },        // Generator restores type attribute; adds "text" if none.
            };
        var expectedPreContent = "original pre-content";
        var expectedContent = "original content";
        var expectedPostContent = "original post-content";
        var expectedTagName = "not-input";

        var context = new TagHelperContext(
            tagName: "input",
            allAttributes: contextAttributes,
            items: new Dictionary<object, object>(),
            uniqueId: "test");
        var originalAttributes = new TagHelperAttributeList
            {
                { "class", "form-control" },
            };
        var output = new TagHelperOutput(
            expectedTagName,
            originalAttributes,
            getChildContentAsync: (useCachedResult, encoder) =>
            {
                var tagHelperContent = new DefaultTagHelperContent();
                tagHelperContent.SetContent("Something");
                return Task.FromResult<TagHelperContent>(tagHelperContent);
            })
        {
            TagMode = TagMode.StartTagOnly,
        };
        output.PreContent.SetContent(expectedPreContent);
        output.Content.SetContent(expectedContent);
        output.PostContent.SetContent(expectedPostContent);

        var metadataProvider = new TestModelMetadataProvider();
        metadataProvider.ForProperty<Model>("Text").DisplayDetails(dd => dd.DataTypeName = dataTypeName);

        var htmlGenerator = new Mock<IHtmlGenerator>(MockBehavior.Strict);
        var tagHelper = GetTagHelper(
            htmlGenerator.Object,
            model,
            nameof(Model.Text),
            metadataProvider: metadataProvider);
        tagHelper.Format = format;
        tagHelper.InputTypeName = inputTypeName;

        var tagBuilder = new TagBuilder("input")
        {
            Attributes =
                {
                    { "class", "text-control" },
                },
        };
        htmlGenerator
            .Setup(mock => mock.GenerateTextBox(
                tagHelper.ViewContext,
                tagHelper.For.ModelExplorer,
                tagHelper.For.Name,
                model,  // value
                format,
                It.Is<Dictionary<string, object>>(m => m.ContainsKey("type"))))     // htmlAttributes
            .Returns(tagBuilder)
            .Verifiable();

        // Act
        await tagHelper.ProcessAsync(context, output);

        // Assert
        htmlGenerator.Verify();

        Assert.Equal(TagMode.StartTagOnly, output.TagMode);
        Assert.Equal(expectedAttributes, output.Attributes, CaseSensitiveTagHelperAttributeComparer.Default);
        Assert.Equal(expectedPreContent, output.PreContent.GetContent());
        Assert.Equal(expectedContent, output.Content.GetContent());
        Assert.Equal(expectedPostContent, output.PostContent.GetContent());
        Assert.Equal(expectedTagName, output.TagName);
    }

    public static TheoryData<string, string, string> InputTypeData
    {
        get
        {
            return new TheoryData<string, string, string>
                {
                    { null, null, "text" },
                    { "Byte", null, "number" },
                    { "custom-datatype", null, "text" },
                    { "Custom-Datatype", null, "text" },
                    { "date", null, "date" },                  // No date/time special cases since ModelType is string.
                    { "datetime", null, "datetime-local" },
                    { "datetime-local", null, "datetime-local" },
                    { "DATETIME-local", null, "datetime-local" },
                    { "datetimeOffset", null, "text" },
                    { "Decimal", "{0:0.00}", "text" },
                    { "Double", null, "text" },
                    { "Int16", null, "number" },
                    { "Int32", null, "number" },
                    { "int32", null, "number" },
                    { "Int64", null, "number" },
                    { "SByte", null, "number" },
                    { "Single", null, "text" },
                    { "SINGLE", null, "text" },
                    { "string", null, "text" },
                    { "STRING", null, "text" },
                    { "text", null, "text" },
                    { "TEXT", null, "text" },
                    { "time", null, "time" },
                    { "month", "{0:yyyy-MM}", "month" },
                    { "week", null, "week" },
                    { "UInt16", null, "number" },
                    { "uint16", null, "number" },
                    { "UInt32", null, "number" },
                    { "UInt64", null, "number" },
                    { nameof(IFormFile), null, "file" },
                    { TemplateRenderer.IEnumerableOfIFormFileName, null, "file" },
                };
        }
    }

    [Theory]
    [MemberData(nameof(InputTypeData))]
    public async Task ProcessAsync_CallsGenerateTextBox_AddsExpectedAttributes(
        string dataTypeName,
        string expectedFormat,
        string expectedType)
    {
        // Arrange
        var expectedAttributes = new TagHelperAttributeList
            {
                { "type", expectedType },                   // Calculated; not passed to HtmlGenerator.
            };
        var expectedTagName = "not-input";

        var context = new TagHelperContext(
            tagName: "input",
            allAttributes: new TagHelperAttributeList(
                Enumerable.Empty<TagHelperAttribute>()),
            items: new Dictionary<object, object>(),
            uniqueId: "test");

        var output = new TagHelperOutput(
            expectedTagName,
            attributes: new TagHelperAttributeList(),
            getChildContentAsync: (useCachedResult, encoder) => Task.FromResult<TagHelperContent>(
                new DefaultTagHelperContent()))
        {
            TagMode = TagMode.SelfClosing,
        };

        var metadataProvider = new TestModelMetadataProvider();
        metadataProvider.ForProperty<Model>("Text").DisplayDetails(dd => dd.DataTypeName = dataTypeName);

        var htmlGenerator = new Mock<IHtmlGenerator>(MockBehavior.Strict);
        var tagHelper = GetTagHelper(
            htmlGenerator.Object,
            model: null,
            propertyName: nameof(Model.Text),
            metadataProvider: metadataProvider);

        var tagBuilder = new TagBuilder("input");

        var htmlAttributes = new Dictionary<string, object>
            {
                { "type", expectedType }
            };
        if (string.Equals(dataTypeName, TemplateRenderer.IEnumerableOfIFormFileName))
        {
            htmlAttributes["multiple"] = "multiple";
        }
        htmlGenerator
            .Setup(mock => mock.GenerateTextBox(
                tagHelper.ViewContext,
                tagHelper.For.ModelExplorer,
                tagHelper.For.Name,
                null,                                   // value
                expectedFormat,
                htmlAttributes))                // htmlAttributes
            .Returns(tagBuilder)
            .Verifiable();

        // Act
        await tagHelper.ProcessAsync(context, output);

        // Assert
        htmlGenerator.Verify();

        Assert.Equal(TagMode.SelfClosing, output.TagMode);
        Assert.Equal(expectedAttributes, output.Attributes);
        Assert.Empty(output.PreContent.GetContent());
        Assert.Equal(string.Empty, output.Content.GetContent());
        Assert.Empty(output.PostContent.GetContent());
        Assert.Equal(expectedTagName, output.TagName);
    }

    [Fact]
    public async Task ProcessAsync_CallsGenerateTextBox_InputTypeDateTime_RendersAsDateTime()
    {
        // Arrange
        var expectedAttributes = new TagHelperAttributeList
            {
                { "type", "datetime" },                   // Calculated; not passed to HtmlGenerator.
            };
        var expectedTagName = "not-input";

        var context = new TagHelperContext(
            tagName: "input",
            allAttributes: new TagHelperAttributeList()
            {
                    {"type", "datetime" }
            },
            items: new Dictionary<object, object>(),
            uniqueId: "test");

        var output = new TagHelperOutput(
            expectedTagName,
            attributes: new TagHelperAttributeList(),
            getChildContentAsync: (useCachedResult, encoder) => Task.FromResult<TagHelperContent>(
                new DefaultTagHelperContent()))
        {
            TagMode = TagMode.SelfClosing,
        };

        var htmlAttributes = new Dictionary<string, object>
            {
                { "type", "datetime" }
            };

        var metadataProvider = TestModelMetadataProvider.CreateDefaultProvider();

        var htmlGenerator = new Mock<IHtmlGenerator>(MockBehavior.Strict);
        var tagHelper = GetTagHelper(
            htmlGenerator.Object,
            model: null,
            propertyName: "DateTime",
            metadataProvider: metadataProvider);
        tagHelper.ViewContext.Html5DateRenderingMode = Html5DateRenderingMode.Rfc3339;
        tagHelper.InputTypeName = "datetime";
        var tagBuilder = new TagBuilder("input");
        htmlGenerator
            .Setup(mock => mock.GenerateTextBox(
                tagHelper.ViewContext,
                tagHelper.For.ModelExplorer,
                tagHelper.For.Name,
                null,                                   // value
                @"{0:yyyy-MM-ddTHH\:mm\:ss.fffK}",
                htmlAttributes))                    // htmlAttributes
            .Returns(tagBuilder)
            .Verifiable();

        // Act
        await tagHelper.ProcessAsync(context, output);

        // Assert
        htmlGenerator.Verify();

        Assert.Equal(TagMode.SelfClosing, output.TagMode);
        Assert.Equal(expectedAttributes, output.Attributes);
        Assert.Empty(output.PreContent.GetContent());
        Assert.Equal(string.Empty, output.Content.GetContent());
        Assert.Empty(output.PostContent.GetContent());
        Assert.Equal(expectedTagName, output.TagName);
    }

    [Fact]
    public async Task ProcessAsync_CallsGenerateTextBox_InputTypeDateOnly_RendersAsDate()
    {
        // Arrange
        var expectedAttributes = new TagHelperAttributeList
            {
                { "type", "date" },                   // Calculated; not passed to HtmlGenerator.
            };
        var expectedTagName = "not-input";

        var context = new TagHelperContext(
            tagName: "input",
            allAttributes: new TagHelperAttributeList()
            {
                    {"type", "date" }
            },
            items: new Dictionary<object, object>(),
            uniqueId: "test");

        var output = new TagHelperOutput(
            expectedTagName,
            attributes: new TagHelperAttributeList(),
            getChildContentAsync: (useCachedResult, encoder) => Task.FromResult<TagHelperContent>(
                new DefaultTagHelperContent()))
        {
            TagMode = TagMode.SelfClosing,
        };

        var htmlAttributes = new Dictionary<string, object>
            {
                { "type", "date" }
            };

        var metadataProvider = TestModelMetadataProvider.CreateDefaultProvider();

        var htmlGenerator = new Mock<IHtmlGenerator>(MockBehavior.Strict);
        var tagHelper = GetTagHelper(
            htmlGenerator.Object,
            model: null,
            propertyName: "DateOnly",
            metadataProvider: metadataProvider);
        tagHelper.ViewContext.Html5DateRenderingMode = Html5DateRenderingMode.Rfc3339;
        tagHelper.InputTypeName = "date";
        var tagBuilder = new TagBuilder("input");
        htmlGenerator
            .Setup(mock => mock.GenerateTextBox(
                tagHelper.ViewContext,
                tagHelper.For.ModelExplorer,
                tagHelper.For.Name,
                null,                                   // value
                @"{0:yyyy-MM-dd}",
                htmlAttributes))                    // htmlAttributes
            .Returns(tagBuilder)
            .Verifiable();

        // Act
        await tagHelper.ProcessAsync(context, output);

        // Assert
        htmlGenerator.Verify();

        Assert.Equal(TagMode.SelfClosing, output.TagMode);
        Assert.Equal(expectedAttributes, output.Attributes);
        Assert.Empty(output.PreContent.GetContent());
        Assert.Equal(string.Empty, output.Content.GetContent());
        Assert.Empty(output.PostContent.GetContent());
        Assert.Equal(expectedTagName, output.TagName);
    }

    [Theory]
    [InlineData("Date", Html5DateRenderingMode.CurrentCulture, "{0:d}", "date")]    // Format from [DataType].
    [InlineData("Date", Html5DateRenderingMode.Rfc3339, "{0:yyyy-MM-dd}", "date")]
    [InlineData("DateTime", Html5DateRenderingMode.CurrentCulture, null, "datetime-local")]
    [InlineData("DateTime", Html5DateRenderingMode.Rfc3339, @"{0:yyyy-MM-ddTHH\:mm\:ss.fff}", "datetime-local")]
    [InlineData("DateOnlyDate", Html5DateRenderingMode.CurrentCulture, "{0:d}", "date")]    // Format from [DataType].
    [InlineData("DateOnlyDate", Html5DateRenderingMode.Rfc3339, "{0:yyyy-MM-dd}", "date")]
    [InlineData("DateOnly", Html5DateRenderingMode.CurrentCulture, null, "text")]    // Format from [DataType].
    [InlineData("DateOnly", Html5DateRenderingMode.Rfc3339, null, "text")]
    [InlineData("DateTimeOffset", Html5DateRenderingMode.CurrentCulture, null, "text")]
    [InlineData("DateTimeOffset", Html5DateRenderingMode.Rfc3339, @"{0:yyyy-MM-ddTHH\:mm\:ss.fffK}", "text")]
    [InlineData("DateTimeLocal", Html5DateRenderingMode.CurrentCulture, null, "datetime-local")]
    [InlineData("DateTimeLocal", Html5DateRenderingMode.Rfc3339, @"{0:yyyy-MM-ddTHH\:mm\:ss.fff}", "datetime-local")]
    [InlineData("Time", Html5DateRenderingMode.CurrentCulture, "{0:t}", "time")]    // Format from [DataType].
    [InlineData("Time", Html5DateRenderingMode.Rfc3339, @"{0:HH\:mm\:ss.fff}", "time")]
    [InlineData("Month", Html5DateRenderingMode.CurrentCulture, "{0:yyyy-MM}", "month")]
    [InlineData("Month", Html5DateRenderingMode.Rfc3339, "{0:yyyy-MM}", "month")]
    [InlineData("Week", Html5DateRenderingMode.CurrentCulture, null, "week")]
    [InlineData("Week", Html5DateRenderingMode.Rfc3339, null, "week")]
    [InlineData("NullableDate", Html5DateRenderingMode.Rfc3339, "{0:yyyy-MM-dd}", "date")]
    [InlineData("NullableDateTime", Html5DateRenderingMode.Rfc3339, @"{0:yyyy-MM-ddTHH\:mm\:ss.fff}", "datetime-local")]
    [InlineData("NullableDateOnlyDate", Html5DateRenderingMode.Rfc3339, "{0:yyyy-MM-dd}", "date")]
    [InlineData("NullableDateOnly", Html5DateRenderingMode.Rfc3339, null, "text")]
    [InlineData("NullableDateTimeOffset", Html5DateRenderingMode.Rfc3339, @"{0:yyyy-MM-ddTHH\:mm\:ss.fffK}", "text")]
    public async Task ProcessAsync_CallsGenerateTextBox_AddsExpectedAttributesForRfc3339(
        string propertyName,
        Html5DateRenderingMode dateRenderingMode,
        string expectedFormat,
        string expectedType)
    {
        // Arrange
        var expectedAttributes = new TagHelperAttributeList
            {
                { "type", expectedType },                   // Calculated; not passed to HtmlGenerator.
            };
        var expectedTagName = "not-input";

        var context = new TagHelperContext(
            tagName: "input",
            allAttributes: new TagHelperAttributeList(
                Enumerable.Empty<TagHelperAttribute>()),
            items: new Dictionary<object, object>(),
            uniqueId: "test");

        var output = new TagHelperOutput(
            expectedTagName,
            attributes: new TagHelperAttributeList(),
            getChildContentAsync: (useCachedResult, encoder) => Task.FromResult<TagHelperContent>(
                new DefaultTagHelperContent()))
        {
            TagMode = TagMode.SelfClosing,
        };

        var htmlAttributes = new Dictionary<string, object>
            {
                { "type", expectedType }
            };

        var metadataProvider = TestModelMetadataProvider.CreateDefaultProvider();

        var htmlGenerator = new Mock<IHtmlGenerator>(MockBehavior.Strict);
        var tagHelper = GetTagHelper(
            htmlGenerator.Object,
            model: null,
            propertyName: propertyName,
            metadataProvider: metadataProvider);
        tagHelper.ViewContext.Html5DateRenderingMode = dateRenderingMode;

        var tagBuilder = new TagBuilder("input");
        htmlGenerator
            .Setup(mock => mock.GenerateTextBox(
                tagHelper.ViewContext,
                tagHelper.For.ModelExplorer,
                tagHelper.For.Name,
                null,                                   // value
                expectedFormat,
                htmlAttributes))                    // htmlAttributes
            .Returns(tagBuilder)
            .Verifiable();

        // Act
        await tagHelper.ProcessAsync(context, output);

        // Assert
        htmlGenerator.Verify();

        Assert.Equal(TagMode.SelfClosing, output.TagMode);
        Assert.Equal(expectedAttributes, output.Attributes);
        Assert.Empty(output.PreContent.GetContent());
        Assert.Equal(string.Empty, output.Content.GetContent());
        Assert.Empty(output.PostContent.GetContent());
        Assert.Equal(expectedTagName, output.TagName);
    }

    // Html5DateRenderingMode.Rfc3339 is enabled by default.
    [Theory]
    [InlineData("Date", "2000-01-02", "date")]
    [InlineData("DateTime", "2000-01-02T03:04:05.060", "datetime-local")]
    [InlineData("DateTimeLocal", "2000-01-02T03:04:05.060", "datetime-local")]
    [InlineData("DateTimeOffset", "2000-01-02T03:04:05.060Z", "text")]
    [InlineData("Time", "03:04:05.060", "time")]
    [InlineData("Month", "2000-01", "month")]
    [InlineData("Week", "1999-W52", "week")]
    public async Task ProcessAsync_CallsGenerateTextBox_ProducesExpectedValue_ForDateTime(
        string propertyName,
        string expectedValue,
        string expectedType)
    {
        // Arrange
        var expectedAttributes = new TagHelperAttributeList
            {
                { "type", expectedType },
                { "id", propertyName },
                { "name", propertyName },
                { "value", expectedValue },
            };

        var context = new TagHelperContext(
            tagName: "input",
            allAttributes: new TagHelperAttributeList(
                Enumerable.Empty<TagHelperAttribute>()),
            items: new Dictionary<object, object>(),
            uniqueId: "test");

        var output = new TagHelperOutput(
            expectedType,
            attributes: new TagHelperAttributeList(),
            getChildContentAsync: (useCachedResult, encoder) => Task.FromResult<TagHelperContent>(
                new DefaultTagHelperContent()))
        {
            TagMode = TagMode.SelfClosing,
        };

        var metadataProvider = TestModelMetadataProvider.CreateDefaultProvider();
        var model = new DateTime(
            year: 2000,
            month: 1,
            day: 2,
            hour: 3,
            minute: 4,
            second: 5,
            millisecond: 60,
            kind: DateTimeKind.Utc);

        var htmlGenerator = HtmlGeneratorUtilities.GetHtmlGenerator(metadataProvider);
        var tagHelper = GetTagHelper(
            htmlGenerator,
            model: model,
            propertyName: propertyName,
            metadataProvider: metadataProvider);
        tagHelper.ViewContext.Html5DateRenderingMode = Html5DateRenderingMode.Rfc3339;

        // Act
        await tagHelper.ProcessAsync(context, output);

        // Assert
        Assert.Equal(expectedAttributes, output.Attributes);
        Assert.Equal(expectedType, output.TagName);
    }

    // Html5DateRenderingMode.Rfc3339 is enabled by default.
    [Theory]
    [InlineData("DateOnlyDate", "2000-01-02", "date")]
    [InlineData("DateOnly", "02/01/2000", "text")]
    [ReplaceCulture]
    public async Task ProcessAsync_CallsGenerateTextBox_ProducesExpectedValue_ForDateOnly(
        string propertyName,
        string expectedValue,
        string expectedType)
    {
        // Arrange
        var expectedAttributes = new TagHelperAttributeList
            {
                { "type", expectedType },
                { "id", propertyName },
                { "name", propertyName },
                { "value", expectedValue },
            };

        var context = new TagHelperContext(
            tagName: "input",
            allAttributes: new TagHelperAttributeList(
                Enumerable.Empty<TagHelperAttribute>()),
            items: new Dictionary<object, object>(),
            uniqueId: "test");

        var output = new TagHelperOutput(
            expectedType,
            attributes: new TagHelperAttributeList(),
            getChildContentAsync: (useCachedResult, encoder) => Task.FromResult<TagHelperContent>(
                new DefaultTagHelperContent()))
        {
            TagMode = TagMode.SelfClosing,
        };

        var metadataProvider = TestModelMetadataProvider.CreateDefaultProvider();
        var model = new DateOnly(
            year: 2000,
            month: 1,
            day: 2);

        var htmlGenerator = HtmlGeneratorUtilities.GetHtmlGenerator(metadataProvider);
        var tagHelper = GetTagHelper(
            htmlGenerator,
            model: model,
            propertyName: propertyName,
            metadataProvider: metadataProvider);
        tagHelper.ViewContext.Html5DateRenderingMode = Html5DateRenderingMode.Rfc3339;

        // Act
        await tagHelper.ProcessAsync(context, output);

        // Assert
        Assert.Equal(expectedAttributes, output.Attributes);
        Assert.Equal(expectedType, output.TagName);
    }

    // Html5DateRenderingMode.Rfc3339 can be disabled.
    [Theory]
    [InlineData("Date", null, "02/01/2000", "date")]
    [InlineData("Date", "{0:d}", "02/01/2000", "date")]
    [InlineData("DateTime", null, "02/01/2000 03:04:05", "datetime-local")]
    [InlineData("DateTimeLocal", 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", null, "03:04", "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 async Task ProcessAsync_CallsGenerateTextBox_ProducesExpectedValue_ForDateTimeNotRfc3339(
        string propertyName,
        string editFormatString,
        string expectedValue,
        string expectedType)
    {
        // Arrange
        var expectedAttributes = new TagHelperAttributeList
            {
                { "type", expectedType },
                { "id", propertyName },
                { "name", propertyName },
                { "value", expectedValue },
            };

        var context = new TagHelperContext(
            tagName: "input",
            allAttributes: new TagHelperAttributeList(
                Enumerable.Empty<TagHelperAttribute>()),
            items: new Dictionary<object, object>(),
            uniqueId: "test");

        var output = new TagHelperOutput(
            expectedType,
            attributes: new TagHelperAttributeList(),
            getChildContentAsync: (useCachedResult, encoder) => Task.FromResult<TagHelperContent>(
                new DefaultTagHelperContent()))
        {
            TagMode = TagMode.SelfClosing,
        };

        var metadataProvider = TestModelMetadataProvider.CreateDefaultProvider();

        var model = new DateTime(
            year: 2000,
            month: 1,
            day: 2,
            hour: 3,
            minute: 4,
            second: 5,
            millisecond: 60,
            kind: DateTimeKind.Utc);

        var htmlGenerator = HtmlGeneratorUtilities.GetHtmlGenerator(metadataProvider);
        var tagHelper = GetTagHelper(
            htmlGenerator,
            model: model,
            propertyName: propertyName,
            metadataProvider: metadataProvider);
        tagHelper.ViewContext.Html5DateRenderingMode = Html5DateRenderingMode.CurrentCulture;
        tagHelper.Format = editFormatString;

        // Act
        await tagHelper.ProcessAsync(context, output);

        // Assert
        Assert.Equal(expectedAttributes, output.Attributes);
        Assert.Equal(expectedType, output.TagName);
    }

    // Html5DateRenderingMode.Rfc3339 can be disabled.
    [Theory]
    [InlineData("DateOnlyDate", null, "02/01/2000", "date")]
    [InlineData("DateOnlyDate", "{0:d}", "02/01/2000", "date")]
    [InlineData("DateOnly", null, "02/01/2000", "text")]
    [ReplaceCulture]
    public async Task ProcessAsync_CallsGenerateTextBox_ProducesExpectedValue_ForDateOnlyNotRfc3339(
        string propertyName,
        string editFormatString,
        string expectedValue,
        string expectedType)
    {
        // Arrange
        var expectedAttributes = new TagHelperAttributeList
            {
                { "type", expectedType },
                { "id", propertyName },
                { "name", propertyName },
                { "value", expectedValue },
            };

        var context = new TagHelperContext(
            tagName: "input",
            allAttributes: new TagHelperAttributeList(
                Enumerable.Empty<TagHelperAttribute>()),
            items: new Dictionary<object, object>(),
            uniqueId: "test");

        var output = new TagHelperOutput(
            expectedType,
            attributes: new TagHelperAttributeList(),
            getChildContentAsync: (useCachedResult, encoder) => Task.FromResult<TagHelperContent>(
                new DefaultTagHelperContent()))
        {
            TagMode = TagMode.SelfClosing,
        };

        var metadataProvider = TestModelMetadataProvider.CreateDefaultProvider();

        var model = new DateOnly(
            year: 2000,
            month: 1,
            day: 2);

        var htmlGenerator = HtmlGeneratorUtilities.GetHtmlGenerator(metadataProvider);
        var tagHelper = GetTagHelper(
            htmlGenerator,
            model: model,
            propertyName: propertyName,
            metadataProvider: metadataProvider);
        tagHelper.ViewContext.Html5DateRenderingMode = Html5DateRenderingMode.CurrentCulture;
        tagHelper.Format = editFormatString;

        // Act
        await tagHelper.ProcessAsync(context, output);

        // Assert
        Assert.Equal(expectedAttributes, output.Attributes);
        Assert.Equal(expectedType, output.TagName);
    }

    // Html5DateRenderingMode.Rfc3339 can be disabled.
    [Theory]
    [InlineData("Month", "month")]
    [InlineData("Week", "week")]
    [ReplaceCulture]
    public async Task ProcessAsync_CallsGenerateTextBox_ProducesExpectedValue_OverridesDefaultFormat(
        string propertyName,
        string expectedType)
    {
        // Arrange
        var expectedAttributes = new TagHelperAttributeList
            {
                { "type", expectedType },
                { "id", propertyName },
                { "name", propertyName },
                { "value", "non-default format string" },
            };

        var context = new TagHelperContext(
            tagName: "input",
            allAttributes: new TagHelperAttributeList(
                Enumerable.Empty<TagHelperAttribute>()),
            items: new Dictionary<object, object>(),
            uniqueId: "test");

        var output = new TagHelperOutput(
            expectedType,
            attributes: new TagHelperAttributeList(),
            getChildContentAsync: (useCachedResult, encoder) => Task.FromResult<TagHelperContent>(
                new DefaultTagHelperContent()))
        {
            TagMode = TagMode.SelfClosing,
        };

        var metadataProvider = TestModelMetadataProvider.CreateDefaultProvider();

        var model = new DateTime(
            year: 2000,
            month: 1,
            day: 2,
            hour: 3,
            minute: 4,
            second: 5,
            millisecond: 60,
            kind: DateTimeKind.Utc);

        var htmlGenerator = HtmlGeneratorUtilities.GetHtmlGenerator(metadataProvider);
        var tagHelper = GetTagHelper(
            htmlGenerator,
            model: model,
            propertyName: propertyName,
            metadataProvider: metadataProvider);
        tagHelper.ViewContext.Html5DateRenderingMode = Html5DateRenderingMode.CurrentCulture;
        tagHelper.Format = "non-default format string";

        // Act
        await tagHelper.ProcessAsync(context, output);

        // Assert
        Assert.Equal(expectedAttributes, output.Attributes);
        Assert.Equal(expectedType, output.TagName);
    }

    private static InputTagHelper GetTagHelper(
        IHtmlGenerator htmlGenerator,
        object model,
        string propertyName,
        IModelMetadataProvider metadataProvider = null)
    {
        return GetTagHelper(
            htmlGenerator,
            container: new Model(),
            containerType: typeof(Model),
            model: model,
            propertyName: propertyName,
            expressionName: propertyName,
            metadataProvider: metadataProvider);
    }

    private static InputTagHelper GetTagHelper(
        IHtmlGenerator htmlGenerator,
        object container,
        Type containerType,
        object model,
        string propertyName,
        string expressionName,
        IModelMetadataProvider metadataProvider = null)
    {
        if (metadataProvider == null)
        {
            metadataProvider = new TestModelMetadataProvider();
        }

        var containerMetadata = metadataProvider.GetMetadataForType(containerType);
        var containerExplorer = metadataProvider.GetModelExplorerForType(containerType, container);

        var propertyMetadata = metadataProvider.GetMetadataForProperty(containerType, propertyName);
        var modelExplorer = containerExplorer.GetExplorerForExpression(propertyMetadata, model);

        var modelExpression = new ModelExpression(expressionName, modelExplorer);
        var viewContext = TestableHtmlGenerator.GetViewContext(container, htmlGenerator, metadataProvider);
        var inputTagHelper = new InputTagHelper(htmlGenerator)
        {
            For = modelExpression,
            ViewContext = viewContext,
        };

        return inputTagHelper;
    }

    public class NameAndId
    {
        public NameAndId(string name, string id)
        {
            Name = name;
            Id = id;
        }

        public string Name { get; private set; }

        public string Id { get; private set; }
    }

    private class Model
    {
        public string Text { get; set; }

        public NestedModel NestedModel { get; set; }

        public bool IsACar { get; set; }

        [DataType(DataType.Date)]
        public DateTime Date { get; set; }

        public DateTime DateTime { get; set; }

        [DataType(DataType.Date)]
        public DateOnly DateOnlyDate { get; set; }

        public DateOnly DateOnly { get; set; }

        public DateTimeOffset DateTimeOffset { get; set; }

        [DataType(DataType.Date)]
        public DateTime? NullableDate { get; set; }

        public DateTime? NullableDateTime { get; set; }

        [DataType(DataType.Date)]
        public DateOnly? NullableDateOnlyDate { get; set; }

        public DateOnly? NullableDateOnly { get; set; }

        public DateTimeOffset? NullableDateTimeOffset { get; set; }

        [DataType("datetime-local")]
        public DateTime DateTimeLocal { get; set; }

        [DataType(DataType.Time)]
        public DateTimeOffset Time { get; set; }

        [DataType("month")]
        public DateTimeOffset Month { get; set; }

        [DataType("week")]
        public DateTimeOffset Week { get; set; }
    }

    private class NestedModel
    {
        public string Text { get; set; }
    }
}
