// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.Encodings.Web;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Razor.TagHelpers;
using Microsoft.AspNetCore.Razor.TagHelpers.Testing;
using Microsoft.Extensions.WebEncoders.Testing;
using Xunit;

namespace Microsoft.AspNetCore.Razor.Runtime.TagHelpers
{
    public class TagHelperExecutionContextTest
    {
        [Fact]
        public async Task SetOutputContentAsync_CanHandleExceptionThrowingChildContent()
        {
            // Arrange
            var calledEnd = false;
            var executionContext = new TagHelperExecutionContext(
                "p",
                tagMode: TagMode.StartTagAndEndTag,
                items: new Dictionary<object, object>(),
                uniqueId: string.Empty,
                executeChildContentAsync: () => throw new Exception(),
                startTagHelperWritingScope: _ => { },
                endTagHelperWritingScope: () =>
                {
                    calledEnd = true;
                    return new DefaultTagHelperContent();
                });

            // Act & Assert
            await Assert.ThrowsAsync<Exception>(async () => await executionContext.SetOutputContentAsync());
            Assert.True(calledEnd);
        }

        [Fact]
        public async Task GetChildContentAsync_CanHandleExceptionThrowingChildContent()
        {
            // Arrange
            var calledEnd = false;
            var executionContext = new TagHelperExecutionContext(
                "p",
                tagMode: TagMode.StartTagAndEndTag,
                items: new Dictionary<object, object>(),
                uniqueId: string.Empty,
                executeChildContentAsync: () => throw new Exception(),
                startTagHelperWritingScope: _ => { },
                endTagHelperWritingScope: () =>
                {
                    calledEnd = true;
                    return new DefaultTagHelperContent();
                });

            // Act & Assert
            await Assert.ThrowsAsync<Exception>(
                async () => await executionContext.GetChildContentAsync(useCachedResult: false, encoder: null));
            Assert.True(calledEnd);
        }

        [Fact]
        public async Task SetOutputContentAsync_SetsOutputsContent()
        {
            // Arrange
            var tagHelperContent = new DefaultTagHelperContent();
            var content = "Hello from child content";
            var executionContext = new TagHelperExecutionContext(
                "p",
                tagMode: TagMode.StartTagAndEndTag,
                items: new Dictionary<object, object>(),
                uniqueId: string.Empty,
                executeChildContentAsync: () =>
                {
                    tagHelperContent.SetContent(content);

                    return Task.FromResult(result: true);
                },
                startTagHelperWritingScope: _ => { },
                endTagHelperWritingScope: () => tagHelperContent);

            // Act
            await executionContext.SetOutputContentAsync();

            // Assert
            Assert.Equal(content, executionContext.Output.Content.GetContent());
        }

        [Fact]
        public async Task ExecutionContext_Reinitialize_UpdatesTagHelperOutputAsExpected()
        {
            // Arrange
            var tagName = "div";
            var tagMode = TagMode.StartTagOnly;
            var callCount = 0;
            Func<Task> executeChildContentAsync = () =>
            {
                callCount++;
                return Task.FromResult(true);
            };
            Action<HtmlEncoder> startTagHelperWritingScope = _ => { };
            Func<TagHelperContent> endTagHelperWritingScope = () => null;
            var executionContext = new TagHelperExecutionContext(
                tagName,
                tagMode,
                items: new Dictionary<object, object>(),
                uniqueId: string.Empty,
                executeChildContentAsync: executeChildContentAsync,
                startTagHelperWritingScope: startTagHelperWritingScope,
                endTagHelperWritingScope: endTagHelperWritingScope);
            var updatedTagName = "p";
            var updatedTagMode = TagMode.SelfClosing;
            var updatedCallCount = 0;
            Func<Task> updatedExecuteChildContentAsync = () =>
            {
                updatedCallCount++;
                return Task.FromResult(true);
            };
            executionContext.AddHtmlAttribute(new TagHelperAttribute("something"));

            // Act - 1
            executionContext.Reinitialize(
                updatedTagName,
                updatedTagMode,
                items: new Dictionary<object, object>(),
                uniqueId: string.Empty,
                executeChildContentAsync: updatedExecuteChildContentAsync);
            executionContext.AddHtmlAttribute(new TagHelperAttribute("Another attribute"));

            // Assert - 1
            var output = executionContext.Output;
            Assert.Equal(updatedTagName, output.TagName);
            Assert.Equal(updatedTagMode, output.TagMode);
            var attribute = Assert.Single(output.Attributes);
            Assert.Equal("Another attribute", attribute.Name);

            // Act - 2
            await output.GetChildContentAsync();

            // Assert - 2
            Assert.Equal(0, callCount);
            Assert.Equal(1, updatedCallCount);
        }

        [Fact]
        public void ExecutionContext_Reinitialize_UpdatesTagHelperContextAsExpected()
        {
            // Arrange
            var tagName = "div";
            var tagMode = TagMode.StartTagOnly;
            var items = new Dictionary<object, object>();
            var uniqueId = "some unique id";
            var callCount = 0;
            Func<Task> executeChildContentAsync = () =>
            {
                callCount++;
                return Task.FromResult(true);
            };
            Action<HtmlEncoder> startWritingScope = _ => { };
            Func<TagHelperContent> endWritingScope = () => null;
            var executionContext = new TagHelperExecutionContext(
                tagName,
                tagMode,
                items,
                uniqueId,
                executeChildContentAsync,
                startWritingScope,
                endWritingScope);
            var updatedItems = new Dictionary<object, object>();
            var updatedUniqueId = "another unique id";
            executionContext.AddHtmlAttribute(new TagHelperAttribute("something"));

            // Act
            executionContext.Reinitialize(
                tagName,
                tagMode,
                updatedItems,
                updatedUniqueId,
                executeChildContentAsync);
            executionContext.AddHtmlAttribute(new TagHelperAttribute("Another attribute"));

            // Assert
            var context = executionContext.Context;
            var attribute = Assert.Single(context.AllAttributes);
            Assert.Equal(tagName, context.TagName);
            Assert.Equal("Another attribute", attribute.Name);
            Assert.Equal(updatedUniqueId, context.UniqueId);
            Assert.Same(updatedItems, context.Items);
        }

        [Theory]
        [InlineData(TagMode.SelfClosing)]
        [InlineData(TagMode.StartTagAndEndTag)]
        [InlineData(TagMode.StartTagOnly)]
        public void ExecutionContext_CreateTagHelperOutput_ReturnsExpectedTagMode(TagMode tagMode)
        {
            // Arrange
            var executionContext = new TagHelperExecutionContext("p", tagMode);

            // Act
            var output = executionContext.Output;

            // Assert
            Assert.Equal(tagMode, output.TagMode);
        }

        [Fact]
        public void ParentItems_SetsItemsProperty()
        {
            // Arrange
            var expectedItems = new Dictionary<object, object>
            {
                { "test-entry", 1234 }
            };

            // Act
            var executionContext = new TagHelperExecutionContext(
                "p",
                tagMode: TagMode.StartTagAndEndTag,
                items: expectedItems,
                uniqueId: string.Empty,
                executeChildContentAsync: async () => await Task.FromResult(result: true),
                startTagHelperWritingScope: _ => { },
                endTagHelperWritingScope: () => new DefaultTagHelperContent());

            // Assert
            Assert.NotNull(executionContext.Items);
            Assert.Same(expectedItems, executionContext.Items);
        }

        public static TheoryData<HtmlEncoder> HtmlEncoderData
        {
            get
            {
                return new TheoryData<HtmlEncoder>
                {
                    null,
                    HtmlEncoder.Default,
                    NullHtmlEncoder.Default,
                    new HtmlTestEncoder(),
                };
            }
        }

        public static TheoryData<HtmlEncoder> HtmlEncoderDataLessNull
        {
            get
            {
                var data = new TheoryData<HtmlEncoder>();
                foreach (var row in HtmlEncoderData)
                {
                    var encoder = (HtmlEncoder)(row[0]);
                    if (encoder != null)
                    {
                        data.Add(encoder);
                    }
                }

                return data;
            }
        }

        [Theory]
        [MemberData(nameof(HtmlEncoderData))]
        public async Task GetChildContentAsync_ReturnsExpectedContent(HtmlEncoder encoder)
        {
            // Arrange
            var tagHelperContent = new DefaultTagHelperContent();
            var executionCount = 0;
            var content = "Hello from child content";
            var expectedContent = $"HtmlEncode[[{content}]]";
            var executionContext = new TagHelperExecutionContext(
                "p",
                tagMode: TagMode.StartTagAndEndTag,
                items: new Dictionary<object, object>(),
                uniqueId: string.Empty,
                executeChildContentAsync: () =>
                {
                    executionCount++;
                    tagHelperContent.SetContent(content);

                    return Task.FromResult(result: true);
                },
                startTagHelperWritingScope: _ => { },
                endTagHelperWritingScope: () => tagHelperContent);

            // Act
            var actualContent = await executionContext.GetChildContentAsync(useCachedResult: true, encoder: encoder);

            // Assert
            Assert.Equal(expectedContent, actualContent.GetContent(new HtmlTestEncoder()));
        }

        [Theory]
        [MemberData(nameof(HtmlEncoderData))]
        public async Task GetChildContentAsync_StartsWritingScopeWithGivenEncoder(HtmlEncoder encoder)
        {
            // Arrange
            HtmlEncoder passedEncoder = null;
            var executionContext = new TagHelperExecutionContext(
                "p",
                tagMode: TagMode.StartTagAndEndTag,
                items: new Dictionary<object, object>(),
                uniqueId: string.Empty,
                executeChildContentAsync: () => Task.FromResult(result: true),
                startTagHelperWritingScope: encoderArgument => passedEncoder = encoderArgument,
                endTagHelperWritingScope: () => new DefaultTagHelperContent());

            // Act
            await executionContext.GetChildContentAsync(useCachedResult: true, encoder: encoder);

            // Assert
            Assert.Same(encoder, passedEncoder);
        }

        [Theory]
        [MemberData(nameof(HtmlEncoderData))]
        public async Task GetChildContentAsync_CachesValue(HtmlEncoder encoder)
        {
            // Arrange
            var executionCount = 0;
            var executionContext = new TagHelperExecutionContext(
                "p",
                tagMode: TagMode.StartTagAndEndTag,
                items: new Dictionary<object, object>(),
                uniqueId: string.Empty,
                executeChildContentAsync: () =>
                {
                    executionCount++;
                    return Task.FromResult(result: true);
                },
                startTagHelperWritingScope: _ => { },
                endTagHelperWritingScope: () => new DefaultTagHelperContent());

            // Act
            var content1 = await executionContext.GetChildContentAsync(useCachedResult: true, encoder: encoder);
            var content2 = await executionContext.GetChildContentAsync(useCachedResult: true, encoder: encoder);

            // Assert
            Assert.Equal(1, executionCount);
        }

        [Theory]
        [MemberData(nameof(HtmlEncoderDataLessNull))]
        public async Task GetChildContentAsync_CachesValuePerEncoder(HtmlEncoder encoder)
        {
            // Arrange
            var executionCount = 0;
            var executionContext = new TagHelperExecutionContext(
                "p",
                tagMode: TagMode.StartTagAndEndTag,
                items: new Dictionary<object, object>(),
                uniqueId: string.Empty,
                executeChildContentAsync: () =>
                {
                    executionCount++;
                    return Task.FromResult(result: true);
                },
                startTagHelperWritingScope: _ => { },
                endTagHelperWritingScope: () => new DefaultTagHelperContent());

            // Act
            var content1 = await executionContext.GetChildContentAsync(useCachedResult: true, encoder: null);
            var content2 = await executionContext.GetChildContentAsync(useCachedResult: true, encoder: encoder);

            // Assert
            Assert.Equal(2, executionCount);
        }

        [Theory]
        [MemberData(nameof(HtmlEncoderData))]
        public async Task GetChildContentAsync_CachesValuePerEncoderInstance(HtmlEncoder encoder)
        {
            // Arrange
            var executionCount = 0;
            var executionContext = new TagHelperExecutionContext(
                "p",
                tagMode: TagMode.StartTagAndEndTag,
                items: new Dictionary<object, object>(),
                uniqueId: string.Empty,
                executeChildContentAsync: () =>
                {
                    executionCount++;
                    return Task.FromResult(result: true);
                },
                startTagHelperWritingScope: _ => { },
                endTagHelperWritingScope: () => new DefaultTagHelperContent());

            // HtmlEncoderData includes another HtmlTestEncoder instance but method compares HtmlEncoder instances.
            var firstEncoder = new HtmlTestEncoder();

            // Act
            var content1 = await executionContext.GetChildContentAsync(useCachedResult: true, encoder: firstEncoder);
            var content2 = await executionContext.GetChildContentAsync(useCachedResult: true, encoder: encoder);

            // Assert
            Assert.Equal(2, executionCount);
        }

        [Theory]
        [MemberData(nameof(HtmlEncoderData))]
        public async Task GetChildContentAsync_CanExecuteChildrenMoreThanOnce(HtmlEncoder encoder)
        {
            // Arrange
            var executionCount = 0;
            var executionContext = new TagHelperExecutionContext(
                "p",
                tagMode: TagMode.StartTagAndEndTag,
                items: new Dictionary<object, object>(),
                uniqueId: string.Empty,
                executeChildContentAsync: () =>
                {
                    executionCount++;
                    return Task.FromResult(result: true);
                },
                startTagHelperWritingScope: _ => { },
                endTagHelperWritingScope: () => new DefaultTagHelperContent());

            // Act
            await executionContext.GetChildContentAsync(useCachedResult: false, encoder: encoder);
            await executionContext.GetChildContentAsync(useCachedResult: false, encoder: encoder);

            // Assert
            Assert.Equal(2, executionCount);
        }

        [Theory]
        [InlineData(true)]
        [InlineData(false)]
        public async Task GetChildContentAsync_ReturnsNewObjectEveryTimeItIsCalled(bool useCachedResult)
        {
            // Arrange
            var executionContext = new TagHelperExecutionContext(
                "p",
                tagMode: TagMode.StartTagAndEndTag,
                items: new Dictionary<object, object>(),
                uniqueId: string.Empty,
                executeChildContentAsync: () => Task.FromResult(result: true),
                startTagHelperWritingScope: _ => { },
                endTagHelperWritingScope: () => new DefaultTagHelperContent());

            // Act
            var content1 = await executionContext.GetChildContentAsync(useCachedResult, encoder: null);
            var content2 = await executionContext.GetChildContentAsync(useCachedResult, encoder: null);

            // Assert
            Assert.NotSame(content1, content2);
        }

        [Fact]
        public void AddHtmlAttribute_MaintainsHtmlAttributes()
        {
            // Arrange
            var executionContext = new TagHelperExecutionContext("p", TagMode.StartTagAndEndTag);
            var expectedAttributes = new TagHelperAttributeList
            {
                { "class", "btn" },
            };
            expectedAttributes.Add(new TagHelperAttribute("type", "text", HtmlAttributeValueStyle.SingleQuotes));

            // Act
            executionContext.AddHtmlAttribute("class", "btn", HtmlAttributeValueStyle.DoubleQuotes);
            executionContext.AddHtmlAttribute("type", "text", HtmlAttributeValueStyle.SingleQuotes);
            var output = executionContext.Output;

            // Assert
            Assert.Equal(
                expectedAttributes,
                output.Attributes,
                CaseSensitiveTagHelperAttributeComparer.Default);
        }

        [Fact]
        public void AddHtmlAttribute_MaintainsMinimizedHtmlAttributes()
        {
            // Arrange
            var executionContext = new TagHelperExecutionContext("input", tagMode: TagMode.StartTagOnly);
            var expectedAttributes = new TagHelperAttributeList
            {
                new TagHelperAttribute("checked"),
                new TagHelperAttribute("visible"),
            };

            // Act
            executionContext.AddHtmlAttribute(new TagHelperAttribute("checked"));
            executionContext.AddHtmlAttribute(new TagHelperAttribute("visible"));
            var output = executionContext.Output;

            // Assert
            Assert.Equal(
                expectedAttributes,
                output.Attributes,
                CaseSensitiveTagHelperAttributeComparer.Default);
        }

        [Fact]
        public void AddHtmlAttribute_MaintainsHtmlAttributes_VariousStyles()
        {
            // Arrange
            var executionContext = new TagHelperExecutionContext("input", tagMode: TagMode.SelfClosing);
            var expectedAttributes = new TagHelperAttributeList
            {
                { "class", "btn" },
                { "foo", "bar" }
            };
            expectedAttributes.Add(new TagHelperAttribute("valid", "true", HtmlAttributeValueStyle.NoQuotes));
            expectedAttributes.Add(new TagHelperAttribute("type", "text", HtmlAttributeValueStyle.SingleQuotes));
            expectedAttributes.Add(new TagHelperAttribute(name: "checked"));
            expectedAttributes.Add(new TagHelperAttribute(name: "visible"));

            // Act
            executionContext.AddHtmlAttribute("class", "btn", HtmlAttributeValueStyle.DoubleQuotes);
            executionContext.AddHtmlAttribute("foo", "bar", HtmlAttributeValueStyle.DoubleQuotes);
            executionContext.AddHtmlAttribute("valid", "true", HtmlAttributeValueStyle.NoQuotes);
            executionContext.AddHtmlAttribute("type", "text", HtmlAttributeValueStyle.SingleQuotes);
            executionContext.AddHtmlAttribute(new TagHelperAttribute("checked"));
            executionContext.AddHtmlAttribute(new TagHelperAttribute("visible"));
            var output = executionContext.Output;

            // Assert
            Assert.Equal(
                expectedAttributes,
                output.Attributes,
                CaseSensitiveTagHelperAttributeComparer.Default);
        }

        [Fact]
        public void TagHelperExecutionContext_MaintainsAllAttributes()
        {
            // Arrange
            var executionContext = new TagHelperExecutionContext("p", TagMode.StartTagAndEndTag);
            var expectedAttributes = new TagHelperAttributeList
            {
                { "class", "btn" },
            };
            expectedAttributes.Add(new TagHelperAttribute("something", true, HtmlAttributeValueStyle.SingleQuotes));
            expectedAttributes.Add(new TagHelperAttribute("type", "text", HtmlAttributeValueStyle.NoQuotes));

            // Act
            executionContext.AddHtmlAttribute("class", "btn", HtmlAttributeValueStyle.DoubleQuotes);
            executionContext.AddTagHelperAttribute("something", true, HtmlAttributeValueStyle.SingleQuotes);
            executionContext.AddHtmlAttribute("type", "text", HtmlAttributeValueStyle.NoQuotes);
            var context = executionContext.Context;

            // Assert
            Assert.Equal(
                expectedAttributes,
                context.AllAttributes,
                CaseSensitiveTagHelperAttributeComparer.Default);
        }

        [Fact]
        public void Add_MaintainsTagHelpers()
        {
            // Arrange
            var executionContext = new TagHelperExecutionContext("p", TagMode.StartTagAndEndTag);
            var tagHelper = new PTagHelper();

            // Act
            executionContext.Add(tagHelper);

            // Assert
            var singleTagHelper = Assert.Single(executionContext.TagHelpers);
            Assert.Same(tagHelper, singleTagHelper);
        }

        [Fact]
        public void Add_MaintainsMultipleTagHelpers()
        {
            // Arrange
            var executionContext = new TagHelperExecutionContext("p", TagMode.StartTagAndEndTag);
            var tagHelper1 = new PTagHelper();
            var tagHelper2 = new PTagHelper();

            // Act
            executionContext.Add(tagHelper1);
            executionContext.Add(tagHelper2);

            // Assert
            var tagHelpers = executionContext.TagHelpers.ToArray();
            Assert.Equal(2, tagHelpers.Length);
            Assert.Same(tagHelper1, tagHelpers[0]);
            Assert.Same(tagHelper2, tagHelpers[1]);
        }

        private class PTagHelper : TagHelper
        {
        }
    }
}
