// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

#nullable disable

using System.IO;
using System.Linq;
using System.Threading;

namespace CSharpSyntaxGenerator
{
    internal class TestWriter : AbstractFileWriter
    {
        private TestWriter(TextWriter writer, Tree tree, CancellationToken cancellationToken = default) : base(writer, tree, cancellationToken)
        {
        }

        public static void Write(TextWriter writer, Tree tree)
        {
            new TestWriter(writer, tree).WriteFile();
        }

        private void WriteFile()
        {
            WriteLine("// <auto-generated />");
            WriteLine();
            WriteLine("using Microsoft.CodeAnalysis.CSharp.Syntax;");
            WriteLine("using Roslyn.Utilities;");
            WriteLine("using Xunit;");
            WriteLine("using InternalSyntaxFactory = Microsoft.CodeAnalysis.CSharp.Syntax.InternalSyntax.SyntaxFactory;");
            WriteLine();

            WriteLine("namespace Microsoft.CodeAnalysis.CSharp.UnitTests");
            OpenBlock();

            WriteLine();
            WriteLine("public partial class GreenNodeTests");
            OpenBlock();

            WriteLine("#region Green Generators");
            WriteNodeGenerators(isGreen: true);
            WriteLine("#endregion Green Generators");
            WriteLine();

            WriteLine("#region Green Factory and Property Tests");
            WriteFactoryPropertyTests(isGreen: true);
            WriteLine("#endregion Green Factory and Property Tests");
            WriteLine();

            WriteLine("#region Green Rewriters");
            WriteRewriterTests();
            WriteLine("#endregion Green Rewriters");

            CloseBlock();

            WriteLine();
            WriteLine("public partial class RedNodeTests");
            OpenBlock();

            WriteLine("#region Red Generators");
            WriteNodeGenerators(isGreen: false);
            WriteLine("#endregion Red Generators");
            WriteLine();

            WriteLine("#region Red Factory and Property Tests");
            WriteFactoryPropertyTests(isGreen: false);
            WriteLine("#endregion Red Factory and Property Tests");
            WriteLine();

            WriteLine("#region Red Rewriters");
            WriteRewriterTests();
            WriteLine("#endregion Red Rewriters");

            CloseBlock();

            CloseBlock();
        }

        private void WriteNodeGenerators(bool isGreen)
        {
            var nodes = Tree.Types.Where(n => n is not PredefinedNode and not AbstractNode);
            bool first = true;
            foreach (var node in nodes)
            {
                if (!first)
                {
                    WriteLine();
                }
                first = false;
                WriteNodeGenerator((Node)node, isGreen);
            }
        }

        private void WriteNodeGenerator(Node node, bool isGreen)
        {
            var valueFields = node.Fields.Where(n => !IsNodeOrNodeList(n.Type));
            var nodeFields = node.Fields.Where(n => IsNodeOrNodeList(n.Type));

            var internalNamespace = isGreen ? "Microsoft.CodeAnalysis.Syntax.InternalSyntax." : "";
            var csharpNamespace = isGreen ? "Syntax.InternalSyntax." : "";
            var syntaxFactory = isGreen ? "InternalSyntaxFactory" : "SyntaxFactory";

            var strippedName = StripPost(node.Name, "Syntax");

            WriteLine($"private static {csharpNamespace}{node.Name} Generate{strippedName}()");

            Write($"    => {syntaxFactory}.{strippedName}(");
            //instantiate node

            bool first = true;

            if (node.Kinds.Count > 1)
            {
                Write($"SyntaxKind.{node.Kinds[0].Name}"); //TODO: other kinds?
                first = false;
            }

            foreach (var field in nodeFields)
            {
                if (!first)
                {
                    Write(", ");
                }
                first = false;

                if (IsOptional(field))
                {
                    if (isGreen)
                    {
                        Write("null");
                    }
                    else
                    {
                        Write($"default({field.Type})");
                    }
                }
                else if (IsAnyList(field.Type))
                {
                    string typeName;
                    if (isGreen)
                    {
                        typeName = internalNamespace + field.Type.Replace("<", "<" + csharpNamespace);
                    }
                    else
                    {
                        typeName = (field.Type == "SyntaxList<SyntaxToken>") ? "SyntaxTokenList" : field.Type;
                    }
                    Write($"new {typeName}()");
                }
                else if (field.Type == "SyntaxToken")
                {
                    var kind = ChooseValidKind(field, node);
                    var leadingTrivia = isGreen ? "null, " : string.Empty;
                    var trailingTrivia = isGreen ? ", null" : string.Empty;
                    if (kind == "IdentifierToken")
                    {
                        Write($"{syntaxFactory}.Identifier(\"{field.Name}\")");
                    }
                    else if (kind == "StringLiteralToken")
                    {
                        Write($"{syntaxFactory}.Literal({leadingTrivia}\"string\", \"string\"{trailingTrivia})");
                    }
                    else if (kind == "CharacterLiteralToken")
                    {
                        Write($"{syntaxFactory}.Literal({leadingTrivia}\"a\", 'a'{trailingTrivia})");
                    }
                    else if (kind == "NumericLiteralToken")
                    {
                        Write($"{syntaxFactory}.Literal({leadingTrivia}\"1\", 1{trailingTrivia})");
                    }
                    else
                    {
                        Write($"{syntaxFactory}.Token(SyntaxKind.{kind})");
                    }
                }
                else if (field.Type == "CSharpSyntaxNode")
                {
                    Write($"{syntaxFactory}.IdentifierName({syntaxFactory}.Identifier(\"{field.Name}\"))");
                }
                else
                {
                    //drill down to a concrete type
                    var type = field.Type;
                    while (true)
                    {
                        var subTypes = ChildMap[type];
                        if (!subTypes.Any())
                        {
                            break;
                        }
                        type = subTypes.First();
                    }
                    Write($"Generate{StripPost(type, "Syntax")}()");
                }
            }

            foreach (var field in valueFields)
            {
                if (!first)
                {
                    Write(", ");
                }
                first = false;

                Write($"new {field.Type}()");
            }

            WriteLine(");");
        }

        private void WriteFactoryPropertyTests(bool isGreen)
        {
            var nodes = Tree.Types.Where(n => n is not PredefinedNode and not AbstractNode);
            bool first = true;
            foreach (var node in nodes)
            {
                if (!first)
                {
                    WriteLine();
                }
                first = false;
                WriteFactoryPropertyTest((Node)node, isGreen);
            }
        }

        private void WriteFactoryPropertyTest(Node node, bool isGreen)
        {
            var valueFields = node.Fields.Where(n => !IsNodeOrNodeList(n.Type));
            var nodeFields = node.Fields.Where(n => IsNodeOrNodeList(n.Type));

            var strippedName = StripPost(node.Name, "Syntax");

            WriteLine("[Fact]");
            WriteLine($"public void Test{strippedName}FactoryAndProperties()");
            OpenBlock();

            WriteLine($"var node = Generate{strippedName}();");

            WriteLine();

            //check properties
            {
                string withStat = null;
                foreach (var field in nodeFields)
                {
                    if (IsOptional(field))
                    {
                        if (!isGreen && field.Type == "SyntaxToken")
                        {
                            WriteLine($"Assert.Equal(SyntaxKind.None, node.{field.Name}.Kind());");
                        }
                        else
                        {
                            WriteLine($"Assert.Null(node.{field.Name});");
                        }
                    }
                    else if (field.Type == "SyntaxToken")
                    {
                        var kind = ChooseValidKind(field, node);
                        if (!isGreen)
                        {
                            WriteLine($"Assert.Equal(SyntaxKind.{kind}, node.{field.Name}.Kind());");
                        }
                        else
                        {
                            WriteLine($"Assert.Equal(SyntaxKind.{kind}, node.{field.Name}.Kind);");
                        }
                    }
                    else
                    {
                        if (field.Type == "SyntaxToken")
                        {
                            WriteLine($"Assert.NotEqual(default, node.{field.Name});");
                        }
                        else if (
                            field.Type == "SyntaxTokenList" ||
                            field.Type.StartsWith("SyntaxList<") ||
                            field.Type.StartsWith("SeparatedSyntaxList<"))
                        {
                            WriteLine($"Assert.Equal(default, node.{field.Name});");
                        }
                        else
                        {
                            WriteLine($"Assert.NotNull(node.{field.Name});");
                        }
                    }

                    if (!isGreen)
                    {
                        withStat += $".With{field.Name}(node.{field.Name})";
                    }
                }

                foreach (var field in valueFields)
                {
                    WriteLine($"Assert.Equal(new {field.Type}(), node.{field.Name});");
                    if (!isGreen)
                    {
                        withStat += $".With{field.Name}(node.{field.Name})";
                    }
                }

                if (!isGreen && withStat != null)
                {
                    WriteLine($"var newNode = node{withStat};");
                    WriteLine("Assert.Equal(node, newNode);");
                }
            }

            if (isGreen)
            {
                WriteLine();
                WriteLine("AttachAndCheckDiagnostics(node);");
            }

            CloseBlock();
        }

        private void WriteRewriterTests()
        {
            var nodes = Tree.Types.Where(n => n is not PredefinedNode and not AbstractNode);
            bool first = true;
            foreach (var node in nodes)
            {
                if (!first)
                {
                    WriteLine();
                }
                first = false;
                WriteTokenDeleteRewriterTest((Node)node);
                WriteLine();
                WriteIdentityRewriterTest((Node)node);
            }
        }

        private void WriteTokenDeleteRewriterTest(Node node)
        {
            var valueFields = node.Fields.Where(n => !IsNodeOrNodeList(n.Type));
            var nodeFields = node.Fields.Where(n => IsNodeOrNodeList(n.Type));

            var strippedName = StripPost(node.Name, "Syntax");

            WriteLine("[Fact]");
            WriteLine($"public void Test{strippedName}TokenDeleteRewriter()");
            OpenBlock();

            WriteLine($"var oldNode = Generate{strippedName}();");
            WriteLine("var rewriter = new TokenDeleteRewriter();");
            WriteLine("var newNode = rewriter.Visit(oldNode);");

            WriteLine();
            WriteLine("if(!oldNode.IsMissing)");
            OpenBlock();
            WriteLine("Assert.NotEqual(oldNode, newNode);");
            CloseBlock();

            WriteLine();
            WriteLine("Assert.NotNull(newNode);");
            WriteLine("Assert.True(newNode.IsMissing, \"No tokens => missing\");");

            CloseBlock();
        }

        private void WriteIdentityRewriterTest(Node node)
        {
            var valueFields = node.Fields.Where(n => !IsNodeOrNodeList(n.Type));
            var nodeFields = node.Fields.Where(n => IsNodeOrNodeList(n.Type));

            var strippedName = StripPost(node.Name, "Syntax");

            WriteLine("[Fact]");
            WriteLine($"public void Test{strippedName}IdentityRewriter()");
            OpenBlock();

            WriteLine($"var oldNode = Generate{strippedName}();");
            WriteLine("var rewriter = new IdentityRewriter();");
            WriteLine("var newNode = rewriter.Visit(oldNode);");

            WriteLine();

            WriteLine("Assert.Same(oldNode, newNode);");

            CloseBlock();
        }

        //guess a reasonable kind if there are no constraints
        private string ChooseValidKind(Field field, Node nd)
        {
            var fieldKinds = GetKindsOfFieldOrNearestParent(nd, field);
            return fieldKinds?.Any() == true ? fieldKinds[0].Name : "IdentifierToken";
        }
    }
}
