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

#nullable disable

using System;
using System.Collections.Generic;
using System.Text;
using Microsoft.AspNetCore.Razor.Language.Extensions;
using Microsoft.AspNetCore.Razor.PooledObjects;
using Xunit;
using Xunit.Sdk;

namespace Microsoft.AspNetCore.Razor.Language.Intermediate;

public static class IntermediateNodeAssert
{
    public static TNode SingleChild<TNode>(IntermediateNode node)
    {
        if (node.Children.Count == 0)
        {
            throw new IntermediateNodeAssertException(node, "The node has no children.");
        }
        else if (node.Children.Count > 1)
        {
            throw new IntermediateNodeAssertException(node, node.Children, "The node has multiple children");
        }

        var child = node.Children[0];
        return Assert.IsType<TNode>(child);
    }

    public static void NoChildren(IntermediateNode node)
    {
        if (node.Children.Count > 0)
        {
            throw new IntermediateNodeAssertException(node, node.Children, "The node has children.");
        }
    }

    public static void Children(IntermediateNode node, params Action<IntermediateNode>[] validators)
    {
        var i = 0;
        for (; i < validators.Length; i++)
        {
            if (node.Children.Count == i)
            {
                throw new IntermediateNodeAssertException(node, node.Children, $"The node only has {node.Children.Count} children.");
            }

            try
            {
                validators[i].Invoke(node.Children[i]);
            }
            catch (XunitException e)
            {
                throw new IntermediateNodeAssertException(node, node.Children, $"Failed while validating node {node.Children[i]} at {i}.", e);
            }
        }

        if (i < node.Children.Count)
        {
            throw new IntermediateNodeAssertException(node, node.Children, $"The node has extra child {node.Children[i]} at {i}.");
        }
    }

    public static void Html(string expected, IntermediateNode node)
    {
        try
        {
            var html = Assert.IsType<HtmlContentIntermediateNode>(node);
            using var _ = StringBuilderPool.GetPooledObject(out var content);
            for (var i = 0; i < html.Children.Count; i++)
            {
                var token = Assert.IsAssignableFrom<HtmlIntermediateToken>(html.Children[i]);
                content.Append(token.Content);
            }

            Assert.Equal(expected, content.ToString());
        }
        catch (XunitException e)
        {
            throw new IntermediateNodeAssertException(node, node.Children, e.Message, e);
        }
    }

    public static void CSharpCode(string expected, IntermediateNode node)
    {
        try
        {
            var statement = Assert.IsType<CSharpCodeIntermediateNode>(node);
            var content = new StringBuilder();
            for (var i = 0; i < statement.Children.Count; i++)
            {
                var token = Assert.IsAssignableFrom<CSharpIntermediateToken>(statement.Children[i]);
                content.Append(token.Content);
            }

            Assert.Equal(expected, content.ToString());
        }
        catch (XunitException e)
        {
            throw new IntermediateNodeAssertException(node, node.Children, e.Message, e);
        }
    }

    public static void Directive(string expectedName, IntermediateNode node, params Action<IntermediateNode>[] childValidators)
    {
        try
        {
            var directive = Assert.IsType<DirectiveIntermediateNode>(node);
            Assert.Equal(expectedName, directive.DirectiveName);
        }
        catch (XunitException e)
        {
            throw new IntermediateNodeAssertException(node, node.Children, e.Message, e);
        }

        Children(node, childValidators);
    }

    public static void DirectiveToken(DirectiveTokenKind expectedKind, string expectedContent, IntermediateNode node)
    {
        try
        {
            var token = Assert.IsType<DirectiveTokenIntermediateNode>(node);
            Assert.Equal(expectedKind, token.DirectiveToken.Kind);
            Assert.Equal(expectedContent, token.Content);
        }
        catch (XunitException e)
        {
            throw new IntermediateNodeAssertException(node, node.Children, e.Message, e);
        }
    }

    public static void Using(string expected, IntermediateNode node)
    {
        try
        {
            var @using = Assert.IsType<UsingDirectiveIntermediateNode>(node);
            Assert.Equal(expected, @using.Content);
        }
        catch (XunitException e)
        {
            throw new IntermediateNodeAssertException(node, node.Children, e.Message, e);
        }
    }

    public static void ConditionalAttribute(
        string prefix,
        string name,
        string suffix,
        IntermediateNode node,
        params Action<IntermediateNode>[] valueValidators)
    {
        var attribute = Assert.IsType<HtmlAttributeIntermediateNode>(node);

        try
        {
            Assert.Equal(prefix, attribute.Prefix);
            Assert.Equal(name, attribute.AttributeName);
            Assert.Equal(suffix, attribute.Suffix);

            Children(attribute, valueValidators);
        }
        catch (XunitException e)
        {
            throw new IntermediateNodeAssertException(attribute, attribute.Children, e.Message, e);
        }
    }

    public static void CSharpExpressionAttributeValue(string prefix, string expected, IntermediateNode node)
    {
        var attributeValue = Assert.IsType<CSharpExpressionAttributeValueIntermediateNode>(node);

        try
        {
            using var _ = StringBuilderPool.GetPooledObject(out var content);
            for (var i = 0; i < attributeValue.Children.Count; i++)
            {
                var token = Assert.IsAssignableFrom<CSharpIntermediateToken>(attributeValue.Children[i]);
                content.Append(token.Content);
            }

            Assert.Equal(prefix, attributeValue.Prefix);
            Assert.Equal(expected, content.ToString());
        }
        catch (XunitException e)
        {
            throw new IntermediateNodeAssertException(attributeValue, attributeValue.Children, e.Message, e);
        }
    }

    public static void LiteralAttributeValue(string prefix, string expected, IntermediateNode node)
    {
        var attributeValue = Assert.IsType<HtmlAttributeValueIntermediateNode>(node);

        try
        {
            using var _ = StringBuilderPool.GetPooledObject(out var content);
            for (var i = 0; i < attributeValue.Children.Count; i++)
            {
                var token = Assert.IsAssignableFrom<HtmlIntermediateToken>(attributeValue.Children[i]);
                content.Append(token.Content);
            }

            Assert.Equal(prefix, attributeValue.Prefix);
            Assert.Equal(expected, content.ToString());
        }
        catch (XunitException e)
        {
            throw new IntermediateNodeAssertException(attributeValue, e.Message);
        }
    }

    public static void CSharpExpression(string expected, IntermediateNode node)
    {
        try
        {
            var cSharp = Assert.IsType<CSharpExpressionIntermediateNode>(node);

            using var _ = StringBuilderPool.GetPooledObject(out var content);
            for (var i = 0; i < cSharp.Children.Count; i++)
            {
                var token = Assert.IsAssignableFrom<CSharpIntermediateToken>(cSharp.Children[i]);
                content.Append(token.Content);
            }

            Assert.Equal(expected, content.ToString());
        }
        catch (XunitException e)
        {
            throw new IntermediateNodeAssertException(node, node.Children, e.Message, e);
        }
    }

    public static void BeginInstrumentation(string expected, IntermediateNode node)
    {
        try
        {
            var beginNode = Assert.IsType<CSharpCodeIntermediateNode>(node);
            using var _ = StringBuilderPool.GetPooledObject(out var content);
            for (var i = 0; i < beginNode.Children.Count; i++)
            {
                var token = Assert.IsAssignableFrom<CSharpIntermediateToken>(beginNode.Children[i]);
                content.Append(token.Content);
            }

            Assert.Equal($"BeginContext({expected});", content.ToString());
        }
        catch (XunitException e)
        {
            throw new IntermediateNodeAssertException(node, node.Children, e.Message, e);
        }
    }

    public static void EndInstrumentation(IntermediateNode node)
    {
        try
        {
            var endNode = Assert.IsType<CSharpCodeIntermediateNode>(node);
            using var _ = StringBuilderPool.GetPooledObject(out var content);
            for (var i = 0; i < endNode.Children.Count; i++)
            {
                var token = Assert.IsAssignableFrom<CSharpIntermediateToken>(endNode.Children[i]);
                content.Append(token.Content);
            }

            Assert.Equal("EndContext();", content.ToString());
        }
        catch (XunitException e)
        {
            throw new IntermediateNodeAssertException(node, node.Children, e.Message, e);
        }
    }

    internal static void TagHelperFieldDeclaration(IntermediateNode node, string typeFullName)
    {
        try
        {
            var fieldNode = Assert.IsType<FieldDeclarationIntermediateNode>(node);
            Assert.Equal(typeFullName, fieldNode.Type);
        }
        catch (XunitException e)
        {
            throw new IntermediateNodeAssertException(node, e.Message);
        }
    }

    internal static void PreallocatedTagHelperPropertyValue(
        IntermediateNode node,
        string attributeName,
        string value,
        AttributeStructure valueStyle)
    {
        var propertyValueNode = Assert.IsType<PreallocatedTagHelperPropertyValueIntermediateNode>(node);

        try
        {
            Assert.Equal(attributeName, propertyValueNode.AttributeName);
            Assert.Equal(value, propertyValueNode.Value);
            Assert.Equal(valueStyle, propertyValueNode.AttributeStructure);
        }
        catch (XunitException e)
        {
            throw new IntermediateNodeAssertException(propertyValueNode, e.Message);
        }
    }

    internal static void TagHelper(string tagName, TagMode tagMode, IEnumerable<TagHelperDescriptor> tagHelpers, IntermediateNode node, params Action<IntermediateNode>[] childValidators)
    {
        var tagHelperNode = Assert.IsType<TagHelperIntermediateNode>(node);

        try
        {
            Assert.Equal(tagName, tagHelperNode.TagName);
            Assert.Equal(tagMode, tagHelperNode.TagMode);

            Assert.Equal(tagHelpers, tagHelperNode.TagHelpers);
        }
        catch (XunitException e)
        {
            throw new IntermediateNodeAssertException(tagHelperNode, e.Message);
        }

        Children(node, childValidators);
    }

    internal static void TagHelperHtmlAttribute(
        string name,
        AttributeStructure valueStyle,
        IntermediateNode node,
        params Action<IntermediateNode>[] valueValidators)
    {
        var tagHelperHtmlAttribute = Assert.IsType<TagHelperHtmlAttributeIntermediateNode>(node);

        try
        {
            Assert.Equal(name, tagHelperHtmlAttribute.AttributeName);
            Assert.Equal(valueStyle, tagHelperHtmlAttribute.AttributeStructure);
            Children(tagHelperHtmlAttribute, valueValidators);
        }
        catch (XunitException e)
        {
            throw new IntermediateNodeAssertException(tagHelperHtmlAttribute, tagHelperHtmlAttribute.Children, e.Message, e);
        }
    }

    internal static void SetPreallocatedTagHelperProperty(IntermediateNode node, string attributeName, string propertyName)
    {
        var setPreallocatedTagHelperProperty = Assert.IsType<PreallocatedTagHelperPropertyIntermediateNode>(node);

        try
        {
            Assert.Equal(attributeName, setPreallocatedTagHelperProperty.AttributeName);
            Assert.Equal(propertyName, setPreallocatedTagHelperProperty.PropertyName);
        }
        catch (XunitException e)
        {
            throw new IntermediateNodeAssertException(setPreallocatedTagHelperProperty, e.Message);
        }
    }

    internal static void SetTagHelperProperty(
        string name,
        string propertyName,
        AttributeStructure valueStyle,
        IntermediateNode node,
        params Action<IntermediateNode>[] valueValidators)
    {
        var propertyNode = Assert.IsType<TagHelperPropertyIntermediateNode>(node);

        try
        {
            Assert.Equal(name, propertyNode.AttributeName);
            Assert.Equal(propertyName, propertyNode.BoundAttribute.PropertyName);
            Assert.Equal(valueStyle, propertyNode.AttributeStructure);
            Children(propertyNode, valueValidators);
        }
        catch (XunitException e)
        {
            throw new IntermediateNodeAssertException(propertyNode, propertyNode.Children, e.Message, e);
        }
    }

    private class IntermediateNodeAssertException : XunitException
    {
        public IntermediateNodeAssertException(IntermediateNode node, string userMessage)
            : base(Format(null, null, userMessage))
        {
            Node = node;
        }

        public IntermediateNodeAssertException(IntermediateNode node, IEnumerable<IntermediateNode> nodes, string userMessage)
            : base(Format(null, nodes, userMessage))
        {
            Node = node;
            Nodes = nodes;
        }

        public IntermediateNodeAssertException(
            IntermediateNode node,
            IEnumerable<IntermediateNode> nodes,
            string userMessage,
            Exception innerException)
            : base(Format(null, nodes, userMessage), innerException)
        {
            Node = node;
            Nodes = nodes;
        }

        public IntermediateNodeAssertException(
            IntermediateNode node,
            IntermediateNode[] ancestors,
            IEnumerable<IntermediateNode> nodes,
            string userMessage,
            Exception innerException)
            : base(Format(ancestors, nodes, userMessage), innerException)
        {
            Node = node;
            Nodes = nodes;
        }

        public IntermediateNode Node { get; }

        public IEnumerable<IntermediateNode> Nodes { get; }

        private static string Format(IntermediateNode[] ancestors, IEnumerable<IntermediateNode> nodes, string userMessage)
        {
            using var _ = StringBuilderPool.GetPooledObject(out var builder);
            builder.AppendLine(userMessage);
            builder.AppendLine();

            if (nodes != null)
            {
                builder.AppendLine("Nodes:");

                foreach (var n in nodes)
                {
                    builder.AppendLine(n.ToString());
                }

                builder.AppendLine();
            }

            builder.AppendLine("Path:");

            if (ancestors != null)
            {
                builder.AppendLine();
                builder.AppendLine("Path:");

                foreach (var ancestor in ancestors)
                {
                    builder.AppendLine(ancestor.ToString());
                }
            }

            return builder.ToString();
        }
    }
}
