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

using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Html;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.AspNetCore.Mvc.ViewFeatures;
using Microsoft.AspNetCore.Razor.TagHelpers;
using Microsoft.AspNetCore.Testing;
using Moq;
using Xunit;

namespace Microsoft.AspNetCore.Mvc.TagHelpers
{
    public class SelectTagHelperTest
    {
        // Model (List<Model> or Model instance), container type (Model or NestModel), model accessor,
        // property path / id, expected content.
        public static TheoryData<object, Type, Func<object>, NameAndId, string> GeneratesExpectedDataSet
        {
            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,
                };
                var noneSelected = "<option></option>" + Environment.NewLine +
                    "<option>HtmlEncode[[outer text]]</option>" + Environment.NewLine +
                    "<option>HtmlEncode[[inner text]]</option>" + Environment.NewLine +
                    "<option>HtmlEncode[[other text]]</option>" + Environment.NewLine;
                var innerSelected = "<option></option>" + Environment.NewLine +
                    "<option>HtmlEncode[[outer text]]</option>" + Environment.NewLine +
                    "<option selected=\"HtmlEncode[[selected]]\">HtmlEncode[[inner text]]</option>" + Environment.NewLine +
                    "<option>HtmlEncode[[other text]]</option>" + Environment.NewLine;
                var outerSelected = "<option></option>" + Environment.NewLine +
                    "<option selected=\"HtmlEncode[[selected]]\">HtmlEncode[[outer text]]</option>" + Environment.NewLine +
                    "<option>HtmlEncode[[inner text]]</option>" + Environment.NewLine +
                    "<option>HtmlEncode[[other text]]</option>" + Environment.NewLine;

                return new TheoryData<object, Type, Func<object>, NameAndId, string>
                {
                    { null, typeof(Model), () => null, new NameAndId("Text", "Text"), noneSelected },

                    // Imitate a temporary variable set from somewhere else in the view model.
                    { null, typeof(Model), () => modelWithText.NestedModel.Text,
                        new NameAndId("item.Text", "item_Text"), innerSelected },

                    { modelWithNull, typeof(Model), () => modelWithNull.Text,
                        new NameAndId("Text", "Text"), noneSelected },
                    { modelWithText, typeof(Model), () => modelWithText.Text,
                        new NameAndId("Text", "Text"), outerSelected },

                    { modelWithNull, typeof(NestedModel), () => modelWithNull.NestedModel.Text,
                        new NameAndId("NestedModel.Text", "NestedModel_Text"), noneSelected },
                    { modelWithText, typeof(NestedModel), () => modelWithText.NestedModel.Text,
                        new NameAndId("NestedModel.Text", "NestedModel_Text"), innerSelected },

                    { models, typeof(Model), () => models[0].Text,
                        new NameAndId("[0].Text", "z0__Text"), noneSelected },
                    { models, typeof(NestedModel), () => models[0].NestedModel.Text,
                        new NameAndId("[0].NestedModel.Text", "z0__NestedModel_Text"), noneSelected },

                    { models, typeof(Model), () => models[1].Text,
                        new NameAndId("[1].Text", "z1__Text"), outerSelected },
                    { models, typeof(NestedModel), () => models[1].NestedModel.Text,
                        new NameAndId("[1].NestedModel.Text", "z1__NestedModel_Text"), innerSelected },
                };
            }
        }

        // Items property value, attribute name, attribute value, expected items value (passed to generator). Provides
        // cross product of Items and attributes. These values should not interact.
        public static TheoryData<IEnumerable<SelectListItem>, string, string, IEnumerable<SelectListItem>>
            ItemsAndMultipleDataSet
        {
            get
            {
                var arrayItems = new[] { new SelectListItem() };
                var listItems = new List<SelectListItem>();
                var multiItems = new MultiSelectList(Enumerable.Range(0, 5));
                var selectItems = new SelectList(Enumerable.Range(0, 5));
                var itemsData = new[]
                {
                    new[] { (IEnumerable<SelectListItem>)null, Enumerable.Empty<SelectListItem>() },
                    new[] { arrayItems, arrayItems },
                    new[] { listItems, listItems },
                    new[] { multiItems, multiItems },
                    new[] { selectItems, selectItems },
                };
                var attributeData = new Dictionary<string, string>(StringComparer.Ordinal)
                {
                    // SelectTagHelper ignores all "multiple" attribute values.
                    { "multiple", null },
                    { "mUltiple", string.Empty },
                    { "muLtiple", "true" },
                    { "Multiple", "false" },
                    { "MUltiple", "multiple" },
                    { "MuLtiple", "Multiple" },
                    { "mUlTiPlE", "mUlTiPlE" },
                    { "mULTiPlE", "MULTIPLE" },
                    { "mUlTIPlE", "Invalid" },
                    { "MULTiPLE", "0" },
                    { "MUlTIPLE", "1" },
                    { "MULTIPLE", "__true" },
                    // SelectTagHelper also ignores non-"multiple" attributes.
                    { "multiple_", "multiple" },
                    { "not-multiple", "multiple" },
                    { "__multiple", "multiple" },
                };

                var theoryData =
                    new TheoryData<IEnumerable<SelectListItem>, string, string, IEnumerable<SelectListItem>>();
                foreach (var items in itemsData)
                {
                    foreach (var attribute in attributeData)
                    {
                        theoryData.Add(items[0], attribute.Key, attribute.Value, items[1]);
                    }
                }

                return theoryData;
            }
        }

        // Model type, model, allowMultiple expected value
        public static TheoryData<Type, object, bool> RealModelTypeDataSet
        {
            get
            {
                return new TheoryData<Type, object, bool>
                {
                    { typeof(object), string.Empty, false },
                    { typeof(string), null, false },
                    { typeof(int?), null, false },
                    { typeof(int), 23, false },
                    { typeof(IEnumerable), null, true },
                    { typeof(IEnumerable<string>), null, true },
                    { typeof(List<int>), null, true },
                    { typeof(object), new[] { "", "", "" }, true },
                    { typeof(object), new List<string>(), true },
                };
            }
        }

        [Theory]
        [MemberData(nameof(GeneratesExpectedDataSet))]
        public async Task ProcessAsync_GeneratesExpectedOutput(
            object model,
            Type containerType,
            Func<object> modelAccessor,
            NameAndId nameAndId,
            string ignored)
        {
            // Arrange
            var originalAttributes = new TagHelperAttributeList
            {
                { "class", "form-control" },
            };
            var originalPostContent = "original content";

            var expectedAttributes = new TagHelperAttributeList(originalAttributes)
            {
                { "id", nameAndId.Id },
                { "name", nameAndId.Name },
                { "valid", "from validation attributes" },
            };
            var expectedPreContent = "original pre-content";
            var expectedContent = "original content";
            var expectedPostContent = originalPostContent;
            var expectedTagName = "not-select";

            var metadataProvider = new TestModelMetadataProvider();
            var containerMetadata = metadataProvider.GetMetadataForType(containerType);
            var containerExplorer = metadataProvider.GetModelExplorerForType(containerType, model);

            var propertyMetadata = metadataProvider.GetMetadataForProperty(containerType, "Text");
            var modelExplorer = containerExplorer.GetExplorerForExpression(propertyMetadata, modelAccessor());

            var modelExpression = new ModelExpression(nameAndId.Name, modelExplorer);

            var tagHelperContext = new TagHelperContext(
                tagName: "select",
                allAttributes: new TagHelperAttributeList(
                    Enumerable.Empty<TagHelperAttribute>()),
                items: new Dictionary<object, object>(),
                uniqueId: "test");
            var output = new TagHelperOutput(
                expectedTagName,
                originalAttributes,
                getChildContentAsync: (useCachedResult, encoder) =>
                {
                    var tagHelperContent = new DefaultTagHelperContent();
                    tagHelperContent.SetContent("Something");
                    return Task.FromResult<TagHelperContent>(tagHelperContent);
                })
            {
                TagMode = TagMode.SelfClosing,
            };
            output.PreContent.SetContent(expectedPreContent);
            output.Content.SetContent(expectedContent);
            output.PostContent.SetContent(originalPostContent);

            var htmlGenerator = new TestableHtmlGenerator(metadataProvider)
            {
                ValidationAttributes =
                {
                    {  "valid", "from validation attributes" },
                }
            };
            var viewContext = TestableHtmlGenerator.GetViewContext(model, htmlGenerator, metadataProvider);
            var tagHelper = new SelectTagHelper(htmlGenerator)
            {
                For = modelExpression,
                ViewContext = viewContext,
            };

            // Act
            tagHelper.Init(tagHelperContext);
            await tagHelper.ProcessAsync(tagHelperContext, output);

            // Assert
            Assert.Equal(TagMode.SelfClosing, output.TagMode);
            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(expectedTagName, output.TagName);

            Assert.Single(
                tagHelperContext.Items,
                entry => (Type)entry.Key == typeof(SelectTagHelper));
        }

        [Theory]
        [MemberData(nameof(GeneratesExpectedDataSet))]
        public async Task ProcessAsync_WithItems_GeneratesExpectedOutput_DoesNotChangeSelectList(
            object model,
            Type containerType,
            Func<object> modelAccessor,
            NameAndId nameAndId,
            string expectedOptions)
        {
            // Arrange
            var originalAttributes = new TagHelperAttributeList
            {
                { "class", "form-control" },
            };
            var originalPostContent = "original content";

            var expectedAttributes = new TagHelperAttributeList(originalAttributes)
            {
                { "id", nameAndId.Id },
                { "name", nameAndId.Name },
                { "valid", "from validation attributes" },
            };
            var expectedPreContent = "original pre-content";
            var expectedContent = "original content";
            var expectedPostContent = originalPostContent + expectedOptions;
            var expectedTagName = "select";

            var metadataProvider = new TestModelMetadataProvider();

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

            var propertyMetadata = metadataProvider.GetMetadataForProperty(containerType, "Text");
            var modelExplorer = containerExplorer.GetExplorerForExpression(propertyMetadata, modelAccessor());

            var modelExpression = new ModelExpression(nameAndId.Name, modelExplorer);

            var tagHelperContext = new TagHelperContext(
                tagName: "select",
                allAttributes: new TagHelperAttributeList(
                    Enumerable.Empty<TagHelperAttribute>()),
                items: new Dictionary<object, object>(),
                uniqueId: "test");
            var output = new TagHelperOutput(
                expectedTagName,
                originalAttributes,
                getChildContentAsync: (useCachedResult, encoder) =>
                {
                    var tagHelperContent = new DefaultTagHelperContent();
                    tagHelperContent.AppendHtml("Something");
                    return Task.FromResult<TagHelperContent>(tagHelperContent);
                })
            {
                TagMode = TagMode.SelfClosing,
            };
            output.PreContent.AppendHtml(expectedPreContent);
            output.Content.AppendHtml(expectedContent);
            output.PostContent.AppendHtml(originalPostContent);

            var htmlGenerator = new TestableHtmlGenerator(metadataProvider)
            {
                ValidationAttributes =
                {
                    {  "valid", "from validation attributes" },
                }
            };
            var viewContext = TestableHtmlGenerator.GetViewContext(model, htmlGenerator, metadataProvider);

            var items = new SelectList(new[] { "", "outer text", "inner text", "other text" });
            var savedDisabled = items.Select(item => item.Disabled).ToList();
            var savedGroup = items.Select(item => item.Group).ToList();
            var savedSelected = items.Select(item => item.Selected).ToList();
            var savedText = items.Select(item => item.Text).ToList();
            var savedValue = items.Select(item => item.Value).ToList();

            var tagHelper = new SelectTagHelper(htmlGenerator)
            {
                For = modelExpression,
                Items = items,
                ViewContext = viewContext,
            };

            // Act
            tagHelper.Init(tagHelperContext);
            await tagHelper.ProcessAsync(tagHelperContext, output);

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

            Assert.Single(
                tagHelperContext.Items,
                entry => (Type)entry.Key == typeof(SelectTagHelper));

            Assert.Equal(savedDisabled, items.Select(item => item.Disabled));
            Assert.Equal(savedGroup, items.Select(item => item.Group));
            Assert.Equal(savedSelected, items.Select(item => item.Selected));
            Assert.Equal(savedText, items.Select(item => item.Text));
            Assert.Equal(savedValue, items.Select(item => item.Value));
        }

        [Fact]
        public async Task ProcessAsync_WithItems_AndNoModelExpression_GeneratesExpectedOutput()
        {
            // Arrange
            var originalAttributes = new TagHelperAttributeList
            {
                { "class", "form-control" },
            };
            var originalPostContent = "original content";

            var expectedAttributes = new TagHelperAttributeList(originalAttributes);
            var selectItems = new SelectList(Enumerable.Range(0, 5));
            var expectedOptions = "<option>HtmlEncode[[0]]</option>" + Environment.NewLine
                + "<option>HtmlEncode[[1]]</option>" + Environment.NewLine
                + "<option>HtmlEncode[[2]]</option>" + Environment.NewLine
                + "<option>HtmlEncode[[3]]</option>" + Environment.NewLine
                + "<option>HtmlEncode[[4]]</option>" + Environment.NewLine;

            var expectedPreContent = "original pre-content";
            var expectedContent = "original content";
            var expectedPostContent = originalPostContent + expectedOptions;
            var expectedTagName = "select";

            var tagHelperContext = new TagHelperContext(
                tagName: "select",
                allAttributes: new TagHelperAttributeList(
                    Enumerable.Empty<TagHelperAttribute>()),
                items: new Dictionary<object, object>(),
                uniqueId: "test");
            var output = new TagHelperOutput(
                expectedTagName,
                originalAttributes,
                getChildContentAsync: (useCachedResult, encoder) =>
                {
                    var tagHelperContent = new DefaultTagHelperContent();
                    tagHelperContent.AppendHtml("Something");
                    return Task.FromResult<TagHelperContent>(tagHelperContent);
                })
            {
                TagMode = TagMode.SelfClosing,
            };
            output.PreContent.AppendHtml(expectedPreContent);
            output.Content.AppendHtml(expectedContent);
            output.PostContent.AppendHtml(originalPostContent);

            var metadataProvider = new TestModelMetadataProvider();
            var htmlGenerator = new TestableHtmlGenerator(metadataProvider);
            var viewContext = TestableHtmlGenerator.GetViewContext(
                model: null,
                htmlGenerator: htmlGenerator,
                metadataProvider: metadataProvider);

            var tagHelper = new SelectTagHelper(htmlGenerator)
            {
                Items = selectItems,
                ViewContext = viewContext,
            };

            // Act
            tagHelper.Init(tagHelperContext);
            await tagHelper.ProcessAsync(tagHelperContext, output);

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

            var kvp = Assert.Single(tagHelperContext.Items);
            Assert.Equal(typeof(SelectTagHelper), kvp.Key);
            Assert.Null(kvp.Value);
        }

        [Theory]
        [MemberData(nameof(GeneratesExpectedDataSet))]
        public async Task ProcessAsyncInTemplate_WithItems_GeneratesExpectedOutput_DoesNotChangeSelectList(
            object model,
            Type containerType,
            Func<object> modelAccessor,
            NameAndId nameAndId,
            string expectedOptions)
        {
            // Arrange
            var originalAttributes = new TagHelperAttributeList
            {
                { "class", "form-control" },
            };
            var originalPostContent = "original content";

            var expectedAttributes = new TagHelperAttributeList(originalAttributes)
            {
                { "id", nameAndId.Id },
                { "name", nameAndId.Name },
                { "valid", "from validation attributes" },
            };
            var expectedPreContent = "original pre-content";
            var expectedContent = "original content";
            var expectedPostContent = originalPostContent + expectedOptions;
            var expectedTagName = "select";

            var metadataProvider = new TestModelMetadataProvider();

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

            var propertyMetadata = metadataProvider.GetMetadataForProperty(containerType, "Text");
            var modelExplorer = containerExplorer.GetExplorerForExpression(propertyMetadata, modelAccessor());

            var modelExpression = new ModelExpression(name: string.Empty, modelExplorer: modelExplorer);

            var tagHelperContext = new TagHelperContext(
                tagName: "select",
                allAttributes: new TagHelperAttributeList(
                    Enumerable.Empty<TagHelperAttribute>()),
                items: new Dictionary<object, object>(),
                uniqueId: "test");
            var output = new TagHelperOutput(
                expectedTagName,
                originalAttributes,
                getChildContentAsync: (useCachedResult, encoder) =>
                {
                    var tagHelperContent = new DefaultTagHelperContent();
                    tagHelperContent.AppendHtml("Something");
                    return Task.FromResult<TagHelperContent>(tagHelperContent);
                })
            {
                TagMode = TagMode.SelfClosing,
            };
            output.PreContent.AppendHtml(expectedPreContent);
            output.Content.AppendHtml(expectedContent);
            output.PostContent.AppendHtml(originalPostContent);

            var htmlGenerator = new TestableHtmlGenerator(metadataProvider)
            {
                ValidationAttributes =
                {
                    {  "valid", "from validation attributes" },
                }
            };
            var viewContext = TestableHtmlGenerator.GetViewContext(model, htmlGenerator, metadataProvider);
            viewContext.ViewData.TemplateInfo.HtmlFieldPrefix = nameAndId.Name;

            var items = new SelectList(new[] { "", "outer text", "inner text", "other text" });
            var savedDisabled = items.Select(item => item.Disabled).ToList();
            var savedGroup = items.Select(item => item.Group).ToList();
            var savedSelected = items.Select(item => item.Selected).ToList();
            var savedText = items.Select(item => item.Text).ToList();
            var savedValue = items.Select(item => item.Value).ToList();
            var tagHelper = new SelectTagHelper(htmlGenerator)
            {
                For = modelExpression,
                Items = items,
                ViewContext = viewContext,
            };

            // Act
            tagHelper.Init(tagHelperContext);
            await tagHelper.ProcessAsync(tagHelperContext, output);

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

            Assert.Single(
                tagHelperContext.Items,
                entry => (Type)entry.Key == typeof(SelectTagHelper));

            Assert.Equal(savedDisabled, items.Select(item => item.Disabled));
            Assert.Equal(savedGroup, items.Select(item => item.Group));
            Assert.Equal(savedSelected, items.Select(item => item.Selected));
            Assert.Equal(savedText, items.Select(item => item.Text));
            Assert.Equal(savedValue, items.Select(item => item.Value));
        }

        [Theory]
        [MemberData(nameof(ItemsAndMultipleDataSet))]
        public async Task ProcessAsync_CallsGeneratorWithExpectedValues_ItemsAndAttribute(
            IEnumerable<SelectListItem> inputItems,
            string attributeName,
            string attributeValue,
            IEnumerable<SelectListItem> expectedItems)
        {
            // Arrange
            var contextAttributes = new TagHelperAttributeList
            {
                // Provided for completeness. Select tag helper does not confirm AllAttributes set is consistent.
                { attributeName, attributeValue },
            };
            var originalAttributes = new TagHelperAttributeList
            {
                { attributeName, attributeValue },
            };
            var propertyName = "Property1";
            var expectedTagName = "select";

            var tagHelperContext = new TagHelperContext(
                tagName: "select",
                allAttributes: contextAttributes,
                items: new Dictionary<object, object>(),
                uniqueId: "test");

            var output = new TagHelperOutput(
                expectedTagName,
                originalAttributes,
                getChildContentAsync: (useCachedResult, encoder) =>
                {
                    var tagHelperContent = new DefaultTagHelperContent();
                    tagHelperContent.SetContent("Something");
                    return Task.FromResult<TagHelperContent>(tagHelperContent);
                });
            var metadataProvider = new EmptyModelMetadataProvider();
            string model = null;
            var modelExplorer = metadataProvider.GetModelExplorerForType(typeof(string), model);

            var htmlGenerator = new Mock<IHtmlGenerator>(MockBehavior.Strict);
            var viewContext = TestableHtmlGenerator.GetViewContext(model, htmlGenerator.Object, metadataProvider);

            // Simulate a (model => model) scenario. E.g. the calling helper may appear in a low-level template.
            var modelExpression = new ModelExpression(string.Empty, modelExplorer);
            viewContext.ViewData.TemplateInfo.HtmlFieldPrefix = propertyName;

            var currentValues = new string[0];
            htmlGenerator
                .Setup(real => real.GetCurrentValues(
                    viewContext,
                    modelExplorer,
                    string.Empty,   // expression
                    false))         // allowMultiple
                .Returns(currentValues)
                .Verifiable();
            htmlGenerator
                .Setup(real => real.GenerateSelect(
                    viewContext,
                    modelExplorer,
                    null,           // optionLabel
                    string.Empty,   // expression
                    expectedItems,
                    currentValues,
                    false,          // allowMultiple
                    null))          // htmlAttributes
                .Returns((TagBuilder)null)
                .Verifiable();

            var tagHelper = new SelectTagHelper(htmlGenerator.Object)
            {
                For = modelExpression,
                Items = inputItems,
                ViewContext = viewContext,
            };

            // Act
            tagHelper.Init(tagHelperContext);
            await tagHelper.ProcessAsync(tagHelperContext, output);

            // Assert
            htmlGenerator.Verify();

            var keyValuePair = Assert.Single(
                tagHelperContext.Items,
                entry => (Type)entry.Key == typeof(SelectTagHelper));
            var actualCurrentValues = Assert.IsType<CurrentValues>(keyValuePair.Value);
            Assert.Same(currentValues, actualCurrentValues.Values);
        }

        [Theory]
        [MemberData(nameof(RealModelTypeDataSet))]
        public async Task TagHelper_CallsGeneratorWithExpectedValues_RealModelType(
            Type modelType,
            object model,
            bool allowMultiple)
        {
            // Arrange
            var contextAttributes = new TagHelperAttributeList(
                    Enumerable.Empty<TagHelperAttribute>());
            var originalAttributes = new TagHelperAttributeList();
            var propertyName = "Property1";
            var tagName = "select";

            var tagHelperContext = new TagHelperContext(
                tagName: "select",
                allAttributes: contextAttributes,
                items: new Dictionary<object, object>(),
                uniqueId: "test");
            var output = new TagHelperOutput(
                tagName,
                originalAttributes,
                getChildContentAsync: (useCachedResult, encoder) =>
                {
                    var tagHelperContent = new DefaultTagHelperContent();
                    tagHelperContent.SetContent("Something");
                    return Task.FromResult<TagHelperContent>(tagHelperContent);
                });
            var metadataProvider = new EmptyModelMetadataProvider();
            var modelExplorer = metadataProvider.GetModelExplorerForType(modelType, model);
            var modelExpression = new ModelExpression(propertyName, modelExplorer);

            var htmlGenerator = new Mock<IHtmlGenerator>(MockBehavior.Strict);
            var viewContext = TestableHtmlGenerator.GetViewContext(model, htmlGenerator.Object, metadataProvider);
            var currentValues = new string[0];
            htmlGenerator
                .Setup(real => real.GetCurrentValues(
                    viewContext,
                    modelExplorer,
                    propertyName,   // expression
                    allowMultiple))
                .Returns(currentValues)
                .Verifiable();
            htmlGenerator
                .Setup(real => real.GenerateSelect(
                    viewContext,
                    modelExplorer,
                    null,           // optionLabel
                    propertyName,   // expression
                    It.IsAny<IEnumerable<SelectListItem>>(),
                    currentValues,
                    allowMultiple,
                    null))          // htmlAttributes
                .Returns((TagBuilder)null)
                .Verifiable();

            var tagHelper = new SelectTagHelper(htmlGenerator.Object)
            {
                For = modelExpression,
                ViewContext = viewContext,
            };

            // Act
            tagHelper.Init(tagHelperContext);
            await tagHelper.ProcessAsync(tagHelperContext, output);

            // Assert
            htmlGenerator.Verify();

            var keyValuePair = Assert.Single(
                tagHelperContext.Items,
                entry => (Type)entry.Key == typeof(SelectTagHelper));
            var actualCurrentValues = Assert.IsType<CurrentValues>(keyValuePair.Value);
            Assert.Same(currentValues, actualCurrentValues.Values);
        }

        [Fact]
        public void Process_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 expectedTagName = "select";

            var metadataProvider = new EmptyModelMetadataProvider();
            var htmlGenerator = new TestableHtmlGenerator(metadataProvider);
            var model = "model-value";
            var modelExplorer = metadataProvider.GetModelExplorerForType(typeof(string), model);
            var modelExpression = new ModelExpression(name: string.Empty, modelExplorer: modelExplorer);
            var viewContext = TestableHtmlGenerator.GetViewContext(model, htmlGenerator, metadataProvider);
            var tagHelper = new SelectTagHelper(htmlGenerator)
            {
                For = modelExpression,
                ViewContext = viewContext,
            };

            var context = new TagHelperContext(new TagHelperAttributeList(), new Dictionary<object, object>(), "test");
            var output = new TagHelperOutput(
                expectedTagName,
                new TagHelperAttributeList(),
                (_, __) => Task.FromResult<TagHelperContent>(new DefaultTagHelperContent()));

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

        [Fact]
        public void Process_WithEmptyForName_DoesNotThrow_WithName()
        {
            // Arrange
            var expectedAttributeValue = "-expression-";
            var expectedTagName = "select";

            var metadataProvider = new EmptyModelMetadataProvider();
            var htmlGenerator = new TestableHtmlGenerator(metadataProvider);
            var model = "model-value";
            var modelExplorer = metadataProvider.GetModelExplorerForType(typeof(string), model);
            var modelExpression = new ModelExpression(name: string.Empty, modelExplorer: modelExplorer);
            var viewContext = TestableHtmlGenerator.GetViewContext(model, htmlGenerator, metadataProvider);
            var tagHelper = new SelectTagHelper(htmlGenerator)
            {
                For = modelExpression,
                Name = expectedAttributeValue,
                ViewContext = viewContext,
            };

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

            var context = new TagHelperContext(attributes, new Dictionary<object, object>(), "test");
            var output = new TagHelperOutput(
                expectedTagName,
                new TagHelperAttributeList(),
                (_, __) => Task.FromResult<TagHelperContent>(new DefaultTagHelperContent()));

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

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

            var attribute = Assert.Single(output.Attributes);
            Assert.Equal("name", attribute.Name);
            Assert.Equal(expectedAttributeValue, attribute.Value);
        }

        [Fact]
        public void Process_PassesNameThrough_EvenIfNullFor()
        {
            // Arrange
            var expectedAttributeValue = "-expression-";
            var expectedTagName = "span";

            var selectList = Array.Empty<SelectListItem>();
            var generator = new Mock<IHtmlGenerator>(MockBehavior.Strict);
            generator
                .Setup(gen => gen.GenerateGroupsAndOptions(/* optionLabel: */ null, selectList))
                .Returns(HtmlString.Empty)
                .Verifiable();

            var metadataProvider = new EmptyModelMetadataProvider();
            var viewContext = TestableHtmlGenerator.GetViewContext(
                model: null,
                htmlGenerator: generator.Object,
                metadataProvider: metadataProvider);

            var tagHelper = new SelectTagHelper(generator.Object)
            {
                Items = selectList,
                Name = expectedAttributeValue,
                ViewContext = viewContext,
            };

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

            var tagHelperContext = new TagHelperContext(attributes, new Dictionary<object, object>(), "test");
            var output = new TagHelperOutput(
                expectedTagName,
                new TagHelperAttributeList(),
                (_, __) => Task.FromResult<TagHelperContent>(new DefaultTagHelperContent()));

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

            // Assert
            generator.VerifyAll();
            Assert.Equal(expectedTagName, output.TagName);
            var attribute = Assert.Single(output.Attributes);
            Assert.Equal("name", attribute.Name);
            Assert.Equal(expectedAttributeValue, attribute.Value);
        }

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

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