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

using System.Text.Encodings.Web;
using Microsoft.AspNetCore.Razor.TagHelpers;
using Microsoft.AspNetCore.Razor.TagHelpers.Testing;
using Microsoft.Extensions.WebEncoders.Testing;

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