// 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.Diagnostics;
using System.Linq;
using System.Text;
using Xunit;

namespace Microsoft.AspNetCore.Razor.Language.Legacy
{
    public abstract class ParserTestBase
    {
        internal static Block IgnoreOutput = new IgnoreOutputBlock();

        internal ParserTestBase()
        {
            Factory = CreateSpanFactory();
            BlockFactory = CreateBlockFactory();
        }

        /// <summary>
        /// Set to true to autocorrect the locations of spans to appear in document order with no gaps.
        /// Use this when spans were not created in document order.
        /// </summary>
        protected bool FixupSpans { get; set; }

        internal SpanFactory Factory { get; private set; }

        internal BlockFactory BlockFactory { get; private set; }

        internal abstract RazorSyntaxTree ParseBlock(string document, bool designTime);

        internal virtual RazorSyntaxTree ParseDocument(string document, bool designTime = false)
        {
            var source = TestRazorSourceDocument.Create(document);
            var reader = new SeekableTextReader(document, filePath: null);

            var options = RazorParserOptions.CreateDefaultOptions();
            options.DesignTimeMode = designTime;
            var context = new ParserContext(reader, options);

            var codeParser = new CSharpCodeParser(context);
            var markupParser = new HtmlMarkupParser(context);

            codeParser.HtmlParser = markupParser;
            markupParser.CodeParser = codeParser;

            markupParser.ParseDocument();

            var root = context.Builder.Build();
            var diagnostics = context.ErrorSink.Errors?.Select(error => RazorDiagnostic.Create(error));

            var codeDocument = RazorCodeDocument.Create(source);

            var syntaxTree = RazorSyntaxTree.Create(root, source, diagnostics, options);
            codeDocument.SetSyntaxTree(syntaxTree);

            var defaultDirectivePass = new DefaultDirectiveSyntaxTreePass();
            syntaxTree = defaultDirectivePass.Execute(codeDocument, syntaxTree);

            return syntaxTree;
        }

        internal virtual RazorSyntaxTree ParseHtmlBlock(string document, bool designTime = false)
        {
            var source = TestRazorSourceDocument.Create(document);

            using (var reader = new SeekableTextReader(document, filePath: null))
            {
                var options = RazorParserOptions.CreateDefaultOptions();
                options.DesignTimeMode = designTime;
                var context = new ParserContext(reader, options);

                var parser = new HtmlMarkupParser(context);
                parser.CodeParser = new CSharpCodeParser(context)
                {
                    HtmlParser = parser,
                };

                parser.ParseBlock();

                var root = context.Builder.Build();
                var diagnostics = context.ErrorSink.Errors?.Select(error => RazorDiagnostic.Create(error));

                return RazorSyntaxTree.Create(root, source, diagnostics, options);
            }
        }

        internal virtual RazorSyntaxTree ParseCodeBlock(string document, bool designTime = false)
        {
            return ParseCodeBlock(document, Enumerable.Empty<DirectiveDescriptor>(), designTime);
        }

        internal virtual RazorSyntaxTree ParseCodeBlock(
            string document,
            IEnumerable<DirectiveDescriptor> descriptors,
            bool designTime)
        {
            var source = TestRazorSourceDocument.Create(document);

            using (var reader = new SeekableTextReader(document, filePath: null))
            {
                var options = RazorParserOptions.CreateDefaultOptions();
                options.DesignTimeMode = designTime;
                var context = new ParserContext(reader, options);

                var parser = new CSharpCodeParser(descriptors, context);
                parser.HtmlParser = new HtmlMarkupParser(context)
                {
                    CodeParser = parser,
                };

                parser.ParseBlock();

                var root = context.Builder.Build();
                var diagnostics = context.ErrorSink.Errors?.Select(error => RazorDiagnostic.Create(error));

                options.Directives.Clear();
                foreach (var directive in descriptors)
                {
                    options.Directives.Add(directive);
                }

                return RazorSyntaxTree.Create(root, source, diagnostics, options);
            }
        }

        internal SpanFactory CreateSpanFactory()
        {
            return new SpanFactory();
        }

        internal abstract BlockFactory CreateBlockFactory();

        internal virtual void ParseBlockTest(string document)
        {
            ParseBlockTest(document, null, false, new RazorError[0]);
        }

        internal virtual void ParseBlockTest(string document, bool designTime)
        {
            ParseBlockTest(document, null, designTime, new RazorError[0]);
        }

        internal virtual void ParseBlockTest(string document, params RazorError[] expectedErrors)
        {
            ParseBlockTest(document, false, expectedErrors);
        }

        internal virtual void ParseBlockTest(string document, bool designTime, params RazorError[] expectedErrors)
        {
            ParseBlockTest(document, null, designTime, expectedErrors);
        }

        internal virtual void ParseBlockTest(string document, Block expectedRoot)
        {
            ParseBlockTest(document, expectedRoot, false, null);
        }

        internal virtual void ParseBlockTest(string document, Block expectedRoot, bool designTime)
        {
            ParseBlockTest(document, expectedRoot, designTime, null);
        }

        internal virtual void ParseBlockTest(string document, Block expectedRoot, params RazorError[] expectedErrors)
        {
            ParseBlockTest(document, expectedRoot, false, expectedErrors);
        }

        internal virtual void ParseBlockTest(string document, Block expected, bool designTime, params RazorError[] expectedErrors)
        {
            var result = ParseBlock(document, designTime);

            if (FixupSpans)
            {
                SpancestryCorrector.Correct(expected);

                var span = expected.FindFirstDescendentSpan();
                span.ChangeStart(SourceLocation.Zero);
            }

            SyntaxTreeVerifier.Verify(result);
            SyntaxTreeVerifier.Verify(expected);

            if (!ReferenceEquals(expected, IgnoreOutput))
            {
                EvaluateResults(result, expected, expectedErrors?.Select(error => RazorDiagnostic.Create(error)).ToList());
            }
        }

        internal virtual void SingleSpanBlockTest(string document, BlockKind blockKind, SpanKind spanType, AcceptedCharacters acceptedCharacters = AcceptedCharacters.Any)
        {
            SingleSpanBlockTest(document, blockKind, spanType, acceptedCharacters, expectedError: null);
        }

        internal virtual void SingleSpanBlockTest(string document, string spanContent, BlockKind blockKind, SpanKind spanType, AcceptedCharacters acceptedCharacters = AcceptedCharacters.Any)
        {
            SingleSpanBlockTest(document, spanContent, blockKind, spanType, acceptedCharacters, expectedErrors: null);
        }

        internal virtual void SingleSpanBlockTest(string document, BlockKind blockKind, SpanKind spanType, params RazorError[] expectedError)
        {
            SingleSpanBlockTest(document, document, blockKind, spanType, expectedError);
        }

        internal virtual void SingleSpanBlockTest(string document, string spanContent, BlockKind blockKind, SpanKind spanType, params RazorError[] expectedErrors)
        {
            SingleSpanBlockTest(document, spanContent, blockKind, spanType, AcceptedCharacters.Any, expectedErrors ?? new RazorError[0]);
        }

        internal virtual void SingleSpanBlockTest(string document, BlockKind blockKind, SpanKind spanType, AcceptedCharacters acceptedCharacters, params RazorError[] expectedError)
        {
            SingleSpanBlockTest(document, document, blockKind, spanType, acceptedCharacters, expectedError);
        }

        internal virtual void SingleSpanBlockTest(string document, string spanContent, BlockKind blockKind, SpanKind spanType, AcceptedCharacters acceptedCharacters, params RazorError[] expectedErrors)
        {
            var result = ParseBlock(document, designTime: false);

            var builder = new BlockBuilder();
            builder.Type = blockKind;
            var expected = ConfigureAndAddSpanToBlock(builder, Factory.Span(spanType, spanContent, spanType == SpanKind.Markup).Accepts(acceptedCharacters));

            if (FixupSpans)
            {
                SpancestryCorrector.Correct(expected);

                var span = expected.FindFirstDescendentSpan();
                span.ChangeStart(SourceLocation.Zero);
            }

            SyntaxTreeVerifier.Verify(result);
            SyntaxTreeVerifier.Verify(expected);

            if (!ReferenceEquals(expected, IgnoreOutput))
            {
                EvaluateResults(result, expected, expectedErrors?.Select(error => RazorDiagnostic.Create(error)).ToList());
            }
        }

        internal virtual void ParseDocumentTest(string document)
        {
            ParseDocumentTest(document, null, false);
        }

        internal virtual void ParseDocumentTest(string document, Block expectedRoot)
        {
            ParseDocumentTest(document, expectedRoot, false, null);
        }

        internal virtual void ParseDocumentTest(string document, Block expectedRoot, params RazorError[] expectedErrors)
        {
            ParseDocumentTest(document, expectedRoot, false, expectedErrors);
        }

        internal virtual void ParseDocumentTest(string document, bool designTime)
        {
            ParseDocumentTest(document, null, designTime);
        }

        internal virtual void ParseDocumentTest(string document, Block expectedRoot, bool designTime)
        {
            ParseDocumentTest(document, expectedRoot, designTime, null);
        }

        internal virtual void ParseDocumentTest(string document, Block expected, bool designTime, params RazorError[] expectedErrors)
        {
            var result = ParseDocument(document, designTime);

            if (FixupSpans)
            {
                SpancestryCorrector.Correct(expected);

                var span = expected.FindFirstDescendentSpan();
                span.ChangeStart(SourceLocation.Zero);
            }

            SyntaxTreeVerifier.Verify(result);
            SyntaxTreeVerifier.Verify(expected);

            if (!ReferenceEquals(expected, IgnoreOutput))
            {
                EvaluateResults(result, expected, expectedErrors?.Select(error => RazorDiagnostic.Create(error)).ToList());
            }
        }

        [Conditional("PARSER_TRACE")]
        private void WriteNode(int indent, SyntaxTreeNode node)
        {
            var content = node.ToString().Replace("\r", "\\r")
                .Replace("\n", "\\n")
                .Replace("{", "{{")
                .Replace("}", "}}");
            if (indent > 0)
            {
                content = new String('.', indent * 2) + content;
            }
            WriteTraceLine(content);
            var block = node as Block;
            if (block != null)
            {
                foreach (SyntaxTreeNode child in block.Children)
                {
                    WriteNode(indent + 1, child);
                }
            }
        }

        internal static void EvaluateResults(RazorSyntaxTree result, Block expectedRoot)
        {
            EvaluateResults(result, expectedRoot, null);
        }

        internal static void EvaluateResults(RazorSyntaxTree result, Block expectedRoot, IList<RazorDiagnostic> expectedErrors)
        {
            EvaluateParseTree(result.Root, expectedRoot);
            EvaluateRazorErrors(result.Diagnostics, expectedErrors);
        }

        internal static void EvaluateParseTree(Block actualRoot, Block expectedRoot)
        {
            // Evaluate the result
            var collector = new ErrorCollector();

            if (expectedRoot == null)
            {
                Assert.Null(actualRoot);
            }
            else
            {
                // Link all the nodes
                expectedRoot.LinkNodes();
                Assert.NotNull(actualRoot);
                EvaluateSyntaxTreeNode(collector, actualRoot, expectedRoot);
                if (collector.Success)
                {
                    WriteTraceLine("Parse Tree Validation Succeeded:" + Environment.NewLine + collector.Message);
                }
                else
                {
                    Assert.True(false, Environment.NewLine + collector.Message);
                }
            }
        }

        private static void EvaluateTagHelperAttribute(
            ErrorCollector collector,
            TagHelperAttributeNode actual,
            TagHelperAttributeNode expected)
        {
            if (actual.Name != expected.Name)
            {
                collector.AddError("{0} - FAILED :: Attribute names do not match", expected.Name);
            }
            else
            {
                collector.AddMessage("{0} - PASSED :: Attribute names match", expected.Name);
            }

            if (actual.ValueStyle != expected.ValueStyle)
            {
                collector.AddError("{0} - FAILED :: Attribute value styles do not match", expected.ValueStyle.ToString());
            }
            else
            {
                collector.AddMessage("{0} - PASSED :: Attribute value style match", expected.ValueStyle);
            }

            if (actual.ValueStyle != HtmlAttributeValueStyle.Minimized)
            {
                EvaluateSyntaxTreeNode(collector, actual.Value, expected.Value);
            }
        }

        private static void EvaluateSyntaxTreeNode(ErrorCollector collector, SyntaxTreeNode actual, SyntaxTreeNode expected)
        {
            if (actual == null)
            {
                AddNullActualError(collector, actual, expected);
                return;
            }

            if (actual.IsBlock != expected.IsBlock)
            {
                AddMismatchError(collector, actual, expected);
            }
            else
            {
                if (expected.IsBlock)
                {
                    EvaluateBlock(collector, (Block)actual, (Block)expected);
                }
                else
                {
                    EvaluateSpan(collector, (Span)actual, (Span)expected);
                }
            }
        }

        private static void EvaluateSpan(ErrorCollector collector, Span actual, Span expected)
        {
            if (!Equals(expected, actual))
            {
                AddMismatchError(collector, actual, expected);
            }
            else
            {
                AddPassedMessage(collector, expected);
            }
        }

        private static void EvaluateBlock(ErrorCollector collector, Block actual, Block expected)
        {
            if (actual.Type != expected.Type || !expected.ChunkGenerator.Equals(actual.ChunkGenerator))
            {
                AddMismatchError(collector, actual, expected);
            }
            else
            {
                if (actual is TagHelperBlock)
                {
                    EvaluateTagHelperBlock(collector, actual as TagHelperBlock, expected as TagHelperBlock);
                }

                AddPassedMessage(collector, expected);
                using (collector.Indent())
                {
                    var expectedNodes = expected.Children.GetEnumerator();
                    var actualNodes = actual.Children.GetEnumerator();
                    while (expectedNodes.MoveNext())
                    {
                        if (!actualNodes.MoveNext())
                        {
                            collector.AddError("{0} - FAILED :: No more elements at this node", expectedNodes.Current);
                        }
                        else
                        {
                            EvaluateSyntaxTreeNode(collector, actualNodes.Current, expectedNodes.Current);
                        }
                    }
                    while (actualNodes.MoveNext())
                    {
                        collector.AddError("End of Node - FAILED :: Found Node: {0}", actualNodes.Current);
                    }
                }
            }
        }

        private static void EvaluateTagHelperBlock(ErrorCollector collector, TagHelperBlock actual, TagHelperBlock expected)
        {
            if (expected == null)
            {
                AddMismatchError(collector, actual, expected);
            }
            else
            {
                if (!string.Equals(expected.TagName, actual.TagName, StringComparison.Ordinal))
                {
                    collector.AddError(
                        "{0} - FAILED :: TagName mismatch for TagHelperBlock :: ACTUAL: {1}",
                        expected.TagName,
                        actual.TagName);
                }

                if (expected.TagMode != actual.TagMode)
                {
                    collector.AddError(
                        $"{expected.TagMode} - FAILED :: {nameof(TagMode)} for {nameof(TagHelperBlock)} " +
                        $"{actual.TagName} :: ACTUAL: {actual.TagMode}");
                }

                var expectedAttributes = expected.Attributes.GetEnumerator();
                var actualAttributes = actual.Attributes.GetEnumerator();

                while (expectedAttributes.MoveNext())
                {
                    if (!actualAttributes.MoveNext())
                    {
                        collector.AddError("{0} - FAILED :: No more attributes on this node", expectedAttributes.Current);
                    }
                    else
                    {
                        EvaluateTagHelperAttribute(collector, actualAttributes.Current, expectedAttributes.Current);
                    }
                }
                while (actualAttributes.MoveNext())
                {
                    collector.AddError("End of Attributes - FAILED :: Found Attribute: {0}", actualAttributes.Current.Name);
                }
            }
        }

        private static void AddPassedMessage(ErrorCollector collector, SyntaxTreeNode expected)
        {
            collector.AddMessage("{0} - PASSED", expected);
        }

        private static void AddMismatchError(ErrorCollector collector, SyntaxTreeNode actual, SyntaxTreeNode expected)
        {
            collector.AddError("{0} - FAILED :: Actual: {1}", expected, actual);
        }

        private static void AddNullActualError(ErrorCollector collector, SyntaxTreeNode actual, SyntaxTreeNode expected)
        {
            collector.AddError("{0} - FAILED :: Actual: << Null >>", expected);
        }

        internal static void EvaluateRazorErrors(IEnumerable<RazorDiagnostic> actualErrors, IList<RazorDiagnostic> expectedErrors)
        {
            var realCount = actualErrors.Count();

            // Evaluate the errors
            if (expectedErrors == null || expectedErrors.Count == 0)
            {
                Assert.True(
                    realCount == 0,
                    "Expected that no errors would be raised, but the following errors were:" + Environment.NewLine + FormatErrors(actualErrors));
            }
            else
            {
                Assert.True(
                    expectedErrors.Count == realCount,
                    $"Expected that {expectedErrors.Count} errors would be raised, but {realCount} errors were." +
                    $"{Environment.NewLine}Expected Errors: {Environment.NewLine}{FormatErrors(expectedErrors)}" +
                    $"{Environment.NewLine}Actual Errors: {Environment.NewLine}{FormatErrors(actualErrors)}");
                Assert.Equal(expectedErrors, actualErrors);
            }
            WriteTraceLine("Expected Errors were raised:" + Environment.NewLine + FormatErrors(expectedErrors));
        }

        internal static string FormatErrors(IEnumerable<RazorDiagnostic> errors)
        {
            if (errors == null)
            {
                return "\t<< No Errors >>";
            }

            var builder = new StringBuilder();
            foreach (var error in errors)
            {
                builder.AppendFormat("\t{0}", error);
                builder.AppendLine();
            }
            return builder.ToString();
        }

        [Conditional("PARSER_TRACE")]
        private static void WriteTraceLine(string format, params object[] args)
        {
            Trace.WriteLine(string.Format(format, args));
        }

        internal virtual Block CreateSimpleBlockAndSpan(string spanContent, BlockKind blockKind, SpanKind spanType, AcceptedCharacters acceptedCharacters = AcceptedCharacters.Any)
        {
            var span = Factory.Span(spanType, spanContent, spanType == SpanKind.Markup).Accepts(acceptedCharacters);
            var b = new BlockBuilder()
            {
                Type = blockKind
            };
            return ConfigureAndAddSpanToBlock(b, span);
        }

        internal virtual Block ConfigureAndAddSpanToBlock(BlockBuilder block, SpanConstructor span)
        {
            switch (block.Type)
            {
                case BlockKind.Markup:
                    span.With(new MarkupChunkGenerator());
                    break;
                case BlockKind.Statement:
                    span.With(new StatementChunkGenerator());
                    break;
                case BlockKind.Expression:
                    block.ChunkGenerator = new ExpressionChunkGenerator();
                    span.With(new ExpressionChunkGenerator());
                    break;
            }
            block.Children.Add(span);
            return block.Build();
        }

        private class IgnoreOutputBlock : Block
        {
            public IgnoreOutputBlock() : base(BlockKind.Template, new SyntaxTreeNode[0], null) { }
        }

        // Corrects the parents and previous/next information for spans
        internal class SpancestryCorrector : ParserVisitor
        {
            private SpancestryCorrector()
            {
            }

            protected Block CurrentBlock { get; set; }

            protected Span LastSpan { get; set; }

            public static void Correct(Block block)
            {
                new SpancestryCorrector().VisitBlock(block);
            }

            public override void VisitBlock(Block block)
            {
                CurrentBlock = block;
                base.VisitBlock(block);
            }

            public override void VisitSpan(Span span)
            {
                span.Parent = CurrentBlock;

                span.Previous = LastSpan;
                if (LastSpan != null)
                {
                    LastSpan.Next = span;
                }

                LastSpan = span;
            }
        } 
    }
}