/*
 *  Licensed to the Apache Software Foundation (ASF) under one
 *  or more contributor license agreements.  See the NOTICE file
 *  distributed with this work for additional information
 *  regarding copyright ownership.  The ASF licenses this file
 *  to you under the Apache License, Version 2.0 (the
 *  "License"); you may not use this file except in compliance
 *  with the License.  You may obtain a copy of the License at
 *
 *    http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing,
 *  software distributed under the License is distributed on an
 *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 *  KIND, either express or implied.  See the License for the
 *  specific language governing permissions and limitations
 *  under the License.
 */
package org.codehaus.groovy.ast.builder

import groovy.test.GroovyTestCase
import org.codehaus.groovy.ast.AnnotationNode
import org.codehaus.groovy.ast.ClassHelper
import org.codehaus.groovy.ast.ClassNode
import org.codehaus.groovy.ast.ConstructorNode
import org.codehaus.groovy.ast.FieldNode
import org.codehaus.groovy.ast.GenericsType
import org.codehaus.groovy.ast.ImportNode
import org.codehaus.groovy.ast.InnerClassNode
import org.codehaus.groovy.ast.MethodNode
import org.codehaus.groovy.ast.MixinNode
import org.codehaus.groovy.ast.Parameter
import org.codehaus.groovy.ast.PropertyNode
import org.codehaus.groovy.ast.VariableScope
import org.codehaus.groovy.ast.expr.AnnotationConstantExpression
import org.codehaus.groovy.ast.expr.ArgumentListExpression
import org.codehaus.groovy.ast.expr.ArrayExpression
import org.codehaus.groovy.ast.expr.AttributeExpression
import org.codehaus.groovy.ast.expr.BinaryExpression
import org.codehaus.groovy.ast.expr.BitwiseNegationExpression
import org.codehaus.groovy.ast.expr.BooleanExpression
import org.codehaus.groovy.ast.expr.CastExpression
import org.codehaus.groovy.ast.expr.ClassExpression
import org.codehaus.groovy.ast.expr.ClosureExpression
import org.codehaus.groovy.ast.expr.ClosureListExpression
import org.codehaus.groovy.ast.expr.ConstantExpression
import org.codehaus.groovy.ast.expr.ConstructorCallExpression
import org.codehaus.groovy.ast.expr.DeclarationExpression
import org.codehaus.groovy.ast.expr.ElvisOperatorExpression
import org.codehaus.groovy.ast.expr.FieldExpression
import org.codehaus.groovy.ast.expr.GStringExpression
import org.codehaus.groovy.ast.expr.ListExpression
import org.codehaus.groovy.ast.expr.MapEntryExpression
import org.codehaus.groovy.ast.expr.MapExpression
import org.codehaus.groovy.ast.expr.MethodCallExpression
import org.codehaus.groovy.ast.expr.MethodPointerExpression
import org.codehaus.groovy.ast.expr.NamedArgumentListExpression
import org.codehaus.groovy.ast.expr.NotExpression
import org.codehaus.groovy.ast.expr.PostfixExpression
import org.codehaus.groovy.ast.expr.PrefixExpression
import org.codehaus.groovy.ast.expr.PropertyExpression
import org.codehaus.groovy.ast.expr.RangeExpression
import org.codehaus.groovy.ast.expr.SpreadExpression
import org.codehaus.groovy.ast.expr.SpreadMapExpression
import org.codehaus.groovy.ast.expr.StaticMethodCallExpression
import org.codehaus.groovy.ast.expr.TernaryExpression
import org.codehaus.groovy.ast.expr.TupleExpression
import org.codehaus.groovy.ast.expr.UnaryMinusExpression
import org.codehaus.groovy.ast.expr.UnaryPlusExpression
import org.codehaus.groovy.ast.expr.VariableExpression
import org.codehaus.groovy.ast.stmt.AssertStatement
import org.codehaus.groovy.ast.stmt.BlockStatement
import org.codehaus.groovy.ast.stmt.BreakStatement
import org.codehaus.groovy.ast.stmt.CaseStatement
import org.codehaus.groovy.ast.stmt.CatchStatement
import org.codehaus.groovy.ast.stmt.ContinueStatement
import org.codehaus.groovy.ast.stmt.EmptyStatement
import org.codehaus.groovy.ast.stmt.ExpressionStatement
import org.codehaus.groovy.ast.stmt.ForStatement
import org.codehaus.groovy.ast.stmt.IfStatement
import org.codehaus.groovy.ast.stmt.ReturnStatement
import org.codehaus.groovy.ast.stmt.SwitchStatement
import org.codehaus.groovy.ast.stmt.SynchronizedStatement
import org.codehaus.groovy.ast.stmt.ThrowStatement
import org.codehaus.groovy.ast.stmt.TryCatchStatement
import org.codehaus.groovy.ast.stmt.WhileStatement
import org.codehaus.groovy.syntax.Token
import org.codehaus.groovy.syntax.Types

import static org.objectweb.asm.Opcodes.ACC_PUBLIC
import static org.objectweb.asm.Opcodes.ACC_STATIC

/**
 * Unit test for the AST from Psuedo-specification feature.
 */
class AstBuilderFromSpecificationTest extends GroovyTestCase {

    void testSimpleMethodCall() {

        def result = new AstBuilder().buildFromSpec {
            methodCall {
                variable "this"
                constant "println"
                argumentList {
                    constant "Hello"
                }
            }
        }

        def expected = new MethodCallExpression(
                new VariableExpression("this"),
                new ConstantExpression("println"),
                new ArgumentListExpression(
                        [new ConstantExpression("Hello")]
                )
        )
        AstAssert.assertSyntaxTree([expected], result)
    }

    void testErrorHandling_TooManyArguments() {

        def message = shouldFail(IllegalArgumentException) {
            new AstBuilder().buildFromSpec {
                methodCall {
                    constant "four arguments"
                    constant "is too"
                    constant "many for"
                    constant "this method"
                }
            }
        }
        assertTrue("Unhelpful error message: $message", message.contains('methodCall could not be invoked'))
        assertTrue("Unhelpful error message: $message", message.contains('Expected to receive'))
        assertTrue("Unhelpful error message: $message", message.contains('but found'))
    }

    void testErrorHandling_WrongArgumentTypes() {

        def message = shouldFail(IllegalArgumentException) {
            new AstBuilder().buildFromSpec {
                methodCall {
                    returnStatement {
                        constant 1
                    }
                    constant "ignored"
                    empty()
                }
            }
        }
        assertTrue("Unhelpful error message: $message", message.contains('methodCall could not be invoked'))
        assertTrue("Unhelpful error message: $message", message.contains('Expected to receive'))
        assertTrue("Unhelpful error message: $message", message.contains('but found'))
    }

    void testAnnotationConstantExpression() {

        def result = new AstBuilder().buildFromSpec {
            annotationConstant {
                annotation Override
            }
        }

        def expected = new AnnotationConstantExpression(
                new AnnotationNode(
                        ClassHelper.OVERRIDE_TYPE
                )
        )
        AstAssert.assertSyntaxTree([expected], result)
    }

    void testArgumentListExpression_NoArgs() {

        def result = new AstBuilder().buildFromSpec {
            argumentList()
        }

        def expected = new ArgumentListExpression()
        AstAssert.assertSyntaxTree([expected], result)
    }

    void testArgumentListExpression_OneListArg() {

        def result = new AstBuilder().buildFromSpec {
            argumentList {
                constant "constant1"
                constant "constant2"
                constant "constant3"
                constant "constant4"
            }
        }

        def expected = new ArgumentListExpression(
                [new ConstantExpression("constant1"),
                        new ConstantExpression("constant2"),
                        new ConstantExpression("constant3"),
                        new ConstantExpression("constant4"),
                ]
        )
        AstAssert.assertSyntaxTree([expected], result)
    }

    void testAttributeExpression() {

        // represents foo.bar attribute invocation
        def result = new AstBuilder().buildFromSpec {
            attribute {
                variable "foo"
                constant "bar"
            }
        }

        def expected = new AttributeExpression(
                new VariableExpression("foo"),
                new ConstantExpression("bar")
        )
        AstAssert.assertSyntaxTree([expected], result)
    }

    /**
     * Test for code:
     * if (foo == bar) println "Hello" else println "World"
     */
    void testIfStatement() {
        // if (foo == bar) println "Hello" else println "World"
        def result = new AstBuilder().buildFromSpec {
            ifStatement {
                booleanExpression {
                    binary {
                        variable "foo"
                        token "=="
                        variable "bar"
                    }
                }
                //if block
                expression {        // NOTE: if block and else block are order dependent and same type
                    methodCall {
                        variable "this"
                        constant "println"
                        argumentList {
                            constant "Hello"
                        }
                    }
                }
                //else block
                expression {
                    methodCall {
                        variable "this"
                        constant "println"
                        argumentList {
                            constant "World"
                        }
                    }
                }
            }
        }

        def expected = new IfStatement(
                new BooleanExpression(
                        new BinaryExpression(
                                new VariableExpression("foo"),
                                new Token(Types.COMPARE_EQUAL, "==", -1, -1),
                                new VariableExpression("bar")
                        )
                ),
                new ExpressionStatement(
                        new MethodCallExpression(
                                new VariableExpression("this"),
                                new ConstantExpression("println"),
                                new ArgumentListExpression(
                                        [new ConstantExpression("Hello")]
                                )
                        )
                ),
                new ExpressionStatement(
                        new MethodCallExpression(
                                new VariableExpression("this"),
                                new ConstantExpression("println"),
                                new ArgumentListExpression(
                                        [new ConstantExpression("World")]
                                )
                        )
                )
        )
        AstAssert.assertSyntaxTree([expected], result)
    }

    void testDeclarationAndListExpression() {

        // represents def foo = [1, 2, 3]
        def result = new AstBuilder().buildFromSpec {
            declaration {
                variable "foo"
                token "="
                list {
                    constant 1
                    constant 2
                    constant 3
                }
            }
        }

        def expected = new DeclarationExpression(
                new VariableExpression("foo"),
                new Token(Types.EQUALS, "=", -1, -1),
                new ListExpression(
                        [new ConstantExpression(1),
                                new ConstantExpression(2),
                                new ConstantExpression(3),]
                )
        )
        AstAssert.assertSyntaxTree([expected], result)
    }

    void testArrayExpression() {

        // new Integer[]{1, 2, 3}
        def result = new AstBuilder().buildFromSpec {
            array(Integer) {
                constant 1
                constant 2
                constant 3
            }
        }

        def expected = new ArrayExpression(
                ClassHelper.make(Integer, false),
                [
                        new ConstantExpression(1),
                        new ConstantExpression(2),
                        new ConstantExpression(3),]
        )
        AstAssert.assertSyntaxTree([expected], result)
    }

    void testBitwiseNegationExpression() {
        def result = new AstBuilder().buildFromSpec {
            bitwiseNegation {
                constant 1
            }
        }

        def expected = new BitwiseNegationExpression(
                new ConstantExpression(1)
        )
        AstAssert.assertSyntaxTree([expected], result)
    }

    void testCastExpression() {
        def result = new AstBuilder().buildFromSpec {
            cast(Integer) {
                constant ""
            }
        }

        def expected = new CastExpression(
                ClassHelper.make(Integer, false),
                new ConstantExpression("")
        )
        AstAssert.assertSyntaxTree([expected], result)
    }


    void testClosureExpression() {

        // { parm -> println parm }
        def result = new AstBuilder().buildFromSpec {
            closure {
                parameters {
                    parameter 'parm': Object.class
                }
                block {
                    expression {
                        methodCall {
                            variable "this"
                            constant "println"
                            argumentList {
                                variable "parm"
                            }
                        }
                    }
                }
            }
        }

        def expected = new ClosureExpression(
                [new Parameter(
                        ClassHelper.make(Object, false), "parm"
                )] as Parameter[],
                new BlockStatement(
                        [new ExpressionStatement(
                                new MethodCallExpression(
                                        new VariableExpression("this"),
                                        new ConstantExpression("println"),
                                        new ArgumentListExpression(
                                                new VariableExpression("parm")
                                        )
                                )
                        )],
                        new VariableScope()
                )

        )
        AstAssert.assertSyntaxTree([expected], result)
    }

    void testClosureExpression_MultipleParameters() {

        // { x,y,z -> println z }
        def result = new AstBuilder().buildFromSpec {
            closure {
                parameters {
                    parameter 'x': Object.class
                    parameter 'y': Object.class
                    parameter 'z': Object.class
                }
                block {
                    expression {
                        methodCall {
                            variable "this"
                            constant "println"
                            argumentList {
                                variable "z"
                            }
                        }
                    }
                }
            }
        }

        def expected = new ClosureExpression(
                [
                        new Parameter(ClassHelper.make(Object, false), "x"),
                        new Parameter(ClassHelper.make(Object, false), "y"),
                        new Parameter(ClassHelper.make(Object, false), "z")] as Parameter[],
                new BlockStatement(
                        [new ExpressionStatement(
                                new MethodCallExpression(
                                        new VariableExpression("this"),
                                        new ConstantExpression("println"),
                                        new ArgumentListExpression(
                                                new VariableExpression("z")
                                        )
                                )
                        )],
                        new VariableScope()
                )

        )
        AstAssert.assertSyntaxTree([expected], result)
    }

    void testConstructorCallExpression() {

        // new Integer(4)
        def result = new AstBuilder().buildFromSpec {
            constructorCall(Integer) {
                argumentList {
                    constant 4
                }
            }
        }

        def expected = new ConstructorCallExpression(
                ClassHelper.make(Integer, false),
                new ArgumentListExpression(
                        new ConstantExpression(4)
                )
        )

        AstAssert.assertSyntaxTree([expected], result)
    }

    void testNotExpression() {
        // !true
        def result = new AstBuilder().buildFromSpec {
            not {
                constant true
            }
        }

        def expected = new NotExpression(
                new ConstantExpression(true)
        )

        AstAssert.assertSyntaxTree([expected], result)
    }

    void testPostfixExpression() {
        // 1++
        def result = new AstBuilder().buildFromSpec {
            postfix {
                constant 1
                token "++"
            }
        }

        def expected = new PostfixExpression(
                new ConstantExpression(1),
                new Token(Types.PLUS_PLUS, "++", -1, -1)
        )

        AstAssert.assertSyntaxTree([expected], result)
    }

    void testPrefixExpression() {
        // ++1
        def result = new AstBuilder().buildFromSpec {
            prefix {
                token "++"
                constant 1
            }
        }

        def expected = new PrefixExpression(
                new Token(Types.PLUS_PLUS, "++", -1, -1),
                new ConstantExpression(1)
        )

        AstAssert.assertSyntaxTree([expected], result)
    }

    void testUnaryMinusExpression() {
        // (-foo)
        def result = new AstBuilder().buildFromSpec {
            unaryMinus {
                variable "foo"
            }
        }

        def expected = new UnaryMinusExpression(
                new VariableExpression("foo")
        )

        AstAssert.assertSyntaxTree([expected], result)
    }

    void testUnaryPlusExpression() {
        // (+foo)
        def result = new AstBuilder().buildFromSpec {
            unaryPlus {
                variable "foo"
            }
        }

        def expected = new UnaryPlusExpression(
                new VariableExpression("foo")
        )

        AstAssert.assertSyntaxTree([expected], result)
    }


    void testClassExpression() {
        // def foo = String
        def result = new AstBuilder().buildFromSpec {
            declaration {
                variable "foo"
                token "="
                classExpression String
            }
        }

        def expected = new DeclarationExpression(
                new VariableExpression("foo"),
                new Token(Types.EQUALS, "=", -1, -1),
                new ClassExpression(ClassHelper.make(String, false))
        )

        AstAssert.assertSyntaxTree([expected], result)
    }

    void testFieldExpression() {
        // public static String foo = "a value"
        def result = new AstBuilder().buildFromSpec {
            field {
                fieldNode "foo", ACC_PUBLIC | ACC_STATIC, String, this.class, {
                    constant "a value"
                    annotations {
                        annotation Deprecated
                    }
                }
            }
        }

        def fieldNode = new FieldNode(
                "foo",
                ACC_PUBLIC | ACC_STATIC,
                ClassHelper.make(String, false),
                ClassHelper.make(this.class, false),
                new ConstantExpression("a value")
        )
        fieldNode.addAnnotation(new AnnotationNode(ClassHelper.make(Deprecated, false)))
        def expected = new FieldExpression(fieldNode)

        AstAssert.assertSyntaxTree([expected], result)
    }

    void testMapAndMapEntryExpression() {

        // [foo: 'bar', baz: 'buz']
        def result = new AstBuilder().buildFromSpec {
            map {
                mapEntry {
                    constant 'foo'
                    constant 'bar'
                }
                mapEntry {
                    constant 'baz'
                    constant 'buz'
                }
            }
        }

        def expected = new MapExpression([
                new MapEntryExpression(new ConstantExpression('foo'), new ConstantExpression('bar')),
                new MapEntryExpression(new ConstantExpression('baz'), new ConstantExpression('buz')),
        ])

        AstAssert.assertSyntaxTree([expected], result)
    }

    void testMapAndMapEntryExpression_SimpleCase() {

        // [foo: 'bar', baz: 'buz']
        def result = new AstBuilder().buildFromSpec {
            map {
                mapEntry 'foo': 'bar'       // NOTE: this really only works for constants.
                mapEntry 'baz': 'buz'
                mapEntry 'qux': 'quux', 'corge': 'grault'
            }
        }

        def expected = new MapExpression([
                new MapEntryExpression(new ConstantExpression('foo'), new ConstantExpression('bar')),
                new MapEntryExpression(new ConstantExpression('baz'), new ConstantExpression('buz')),
                new MapEntryExpression(new ConstantExpression('qux'), new ConstantExpression('quux')),
                new MapEntryExpression(new ConstantExpression('corge'), new ConstantExpression('grault')),
        ])

        AstAssert.assertSyntaxTree([expected], result)
    }

    void testGStringExpression() {
        // "$foo"
        def result = new AstBuilder().buildFromSpec {
            gString '$foo astring $bar', {
                strings {
                    constant ''
                    constant ' astring '
                    constant ''
                }
                values {
                    variable 'foo'
                    variable 'bar'
                }
            }
        }

        def expected = new GStringExpression('$foo astring $bar',
                [new ConstantExpression(''), new ConstantExpression(' astring '), new ConstantExpression('')],
                [new VariableExpression('foo'), new VariableExpression('bar')])


        AstAssert.assertSyntaxTree([expected], result)
    }

    void testMethodPointerExpression() {
        // Integer.&toString
        def result = new AstBuilder().buildFromSpec {
            methodPointer {
                classExpression Integer
                constant "toString"
            }
        }

        def expected = new MethodPointerExpression(
                new ClassExpression(ClassHelper.make(Integer, false)),
                new ConstantExpression("toString")
        )

        AstAssert.assertSyntaxTree([expected], result)
    }

    void testRangeExpression() {
        // (0..10)
        def result = new AstBuilder().buildFromSpec {
            range {
                constant 0
                constant 10
                inclusive true
            }
        }

        def expected = new RangeExpression(
                new ConstantExpression(0),
                new ConstantExpression(10),
                true
        )

        AstAssert.assertSyntaxTree([expected], result)
    }

    void testRangeExpression_Exclusive() {
        // (0..10)
        def result = new AstBuilder().buildFromSpec {
            range {
                constant 0
                constant 10
                inclusive false
            }
        }

        def expected = new RangeExpression(
                new ConstantExpression(0),
                new ConstantExpression(10),
                false
        )

        AstAssert.assertSyntaxTree([expected], result)
    }

    void testRangeExpression_SimpleForm() {
        // (0..10)
        def result = new AstBuilder().buildFromSpec {
            range(0..10)
        }

        def expected = new RangeExpression(
                new ConstantExpression(0),
                new ConstantExpression(10),
                true
        )

        AstAssert.assertSyntaxTree([expected], result)
    }

    void testPropertyExpression() {
        // foo.bar
        def result = new AstBuilder().buildFromSpec {
            property {                  // this name conflicts with PropertyNode.
                variable "foo"
                constant "bar"
            }
        }

        def expected = new PropertyExpression(
                new VariableExpression("foo"),
                new ConstantExpression("bar")
        )

        AstAssert.assertSyntaxTree([expected], result)
    }

    void testSwitchAndCaseAndBreakStatements() {
        /*
                  switch (foo) {
                      case 0: break "some label"
                      case 1:
                      case 2:
                          println "<3"
                          break;
                      default:
                          println ">2"
                  }
                   */
        def result = new AstBuilder().buildFromSpec {
            switchStatement {           //NOTE: switchStatement is abnormal pattern
                variable "foo"
                defaultCase {       // NOTE: this creates a block statement
                    expression {        //NOTE: default branch is problematic b/c it is order dependent
                        methodCall {
                            variable "this"
                            constant "println"
                            argumentList {
                                constant ">2"
                            }
                        }
                    }
                }
                caseStatement {
                    constant 0
                    breakStatement "some label"  // label parameter is optional
                }
                caseStatement {
                    constant 1
                    empty()
                }
                caseStatement {
                    constant 2
                    block {
                        expression {
                            methodCall {
                                variable "this"
                                constant "println"
                                argumentList {
                                    constant "<3"
                                }
                            }
                        }
                        breakStatement()
                    }
                }
            }
        }

        def expected = new SwitchStatement(
                new VariableExpression("foo"),
                [
                        new CaseStatement(
                                new ConstantExpression(0),
                                new BreakStatement("some label")
                        ),
                        new CaseStatement(
                                new ConstantExpression(1),
                                EmptyStatement.INSTANCE
                        ),
                        new CaseStatement(
                                new ConstantExpression(2),
                                new BlockStatement(
                                        [
                                                new ExpressionStatement(
                                                        new MethodCallExpression(
                                                                new VariableExpression("this"),
                                                                new ConstantExpression("println"),
                                                                new ArgumentListExpression(
                                                                        [new ConstantExpression("<3")]
                                                                )
                                                        )
                                                ),
                                                new BreakStatement()
                                        ], new VariableScope()
                                )
                        )
                ],
                new BlockStatement(
                        [new ExpressionStatement(
                                new MethodCallExpression(
                                        new VariableExpression("this"),
                                        new ConstantExpression("println"),
                                        new ArgumentListExpression(
                                                [new ConstantExpression(">2")]
                                        )
                                )
                        )],
                        new VariableScope()
                )
        )

        AstAssert.assertSyntaxTree([expected], result)
    }

    void testAssertStatement() {
        /*
                  assert true : "should always be true"
                  assert 1 == 2
                  */
        def result = new AstBuilder().buildFromSpec {
            block {
                assertStatement {
                    booleanExpression {
                        constant true
                    }
                    constant "should always be true"
                }
                assertStatement {
                    booleanExpression {
                        binary {
                            constant 1
                            token "=="
                            constant 2
                        }
                    }
                }
            }
        }

        def expected = new BlockStatement(
                [
                        new AssertStatement(
                                new BooleanExpression(
                                        new ConstantExpression(true)
                                ),
                                new ConstantExpression("should always be true")
                        ),
                        new AssertStatement(
                                new BooleanExpression(
                                        new BinaryExpression(
                                                new ConstantExpression(1),
                                                new Token(Types.COMPARE_EQUAL, "==", -1, -1),
                                                new ConstantExpression(2)
                                        )
                                )
                        ),
                ],
                new VariableScope()
        )

        AstAssert.assertSyntaxTree([expected], result)
    }


    void testReturnAndSynchronizedStatement() {
        /*
                  synchronized (this) {
                      return 1
                  }
          */
        def result = new AstBuilder().buildFromSpec {
            synchronizedStatement {
                variable "this"
                block {
                    returnStatement {
                        constant 1
                    }
                }
            }
        }

        def expected = new SynchronizedStatement(
                new VariableExpression("this"),
                new BlockStatement(
                        [new ReturnStatement(
                                new ConstantExpression(1)
                        )],
                        new VariableScope()
                )
        )

        AstAssert.assertSyntaxTree([expected], result)
    }

    void testTryCatchAndCatchAndThrowStatements() {
        /*
                  try {
                      return 1
                  } catch (Exception e) {
                       throw e
                  }
          */
        def result = new AstBuilder().buildFromSpec {
            tryCatch {
                block {
                    returnStatement {
                        constant 1
                    }
                }
                empty() //finally block must be specified?
                catchStatement {
                    parameter 'e': Exception.class
                    block {
                        throwStatement {
                            variable "e"
                        }
                    }
                }
            }
        }

        TryCatchStatement expected = new TryCatchStatement(
                new BlockStatement(
                        [new ReturnStatement(
                                new ConstantExpression(1)
                        )],
                        new VariableScope()
                ),
                EmptyStatement.INSTANCE
        )
        expected.addCatch(
                new CatchStatement(
                        new Parameter(
                                ClassHelper.make(Exception, false), "e"
                        ),
                        new BlockStatement(
                                [new ThrowStatement(
                                        new VariableExpression("e")
                                )],
                                new VariableScope()
                        )
                )
        )
        AstAssert.assertSyntaxTree([expected], result)
    }

    void testFinallyStatement() {
        /*
                  try {
                      return 1
                  } finally {
                       x.close()
                  }
          */
        def result = new AstBuilder().buildFromSpec {
            tryCatch {
                block {
                    returnStatement {
                        constant 1
                    }
                }
                block {
                    block {
                        expression {
                            methodCall {
                                variable 'x'
                                constant 'close'
                                argumentList()
                            }
                        }
                    }
                }
            }
        }

        TryCatchStatement expected = new TryCatchStatement(
                new BlockStatement(
                        [new ReturnStatement(
                                new ConstantExpression(1)
                        )],
                        new VariableScope()
                ),
                new BlockStatement(
                        [
                                new BlockStatement(
                                        [
                                                new ExpressionStatement(
                                                        new MethodCallExpression(
                                                                new VariableExpression('x'),
                                                                'close',
                                                                new ArgumentListExpression()
                                                        )
                                                )
                                        ],
                                        new VariableScope())
                        ],
                        new VariableScope()
                )
        )
        AstAssert.assertSyntaxTree([expected], result)
    }

    void testForStatementAndClosureListExpression() {
        /*
              for (int x = 0; x < 10; x++) {
                  println x
              }
          */

        def result = new AstBuilder().buildFromSpec {
            forStatement {
                parameter 'forLoopDummyParameter': Object.class
                closureList {
                    declaration {
                        variable 'x'
                        token '='
                        constant 0
                    }
                    binary {
                        variable 'x'
                        token '<'
                        constant 10
                    }
                    postfix {
                        variable 'x'
                        token '++'
                    }
                }
                block {
                    expression {
                        methodCall {
                            variable 'this'
                            constant 'println'
                            argumentList {
                                variable 'x'
                            }
                        }
                    }
                }
            }
        }

        def expected = new ForStatement(
                new Parameter(ClassHelper.make(Object, false), "forLoopDummyParameter"),
                new ClosureListExpression(
                        [
                                new DeclarationExpression(
                                        new VariableExpression("x"),
                                        new Token(Types.EQUALS, "=", -1, -1),
                                        new ConstantExpression(0)
                                ),
                                new BinaryExpression(
                                        new VariableExpression("x"),
                                        new Token(Types.COMPARE_LESS_THAN, "<", -1, -1),
                                        new ConstantExpression(10)
                                ),
                                new PostfixExpression(
                                        new VariableExpression("x"),
                                        new Token(Types.PLUS_PLUS, "++", -1, -1)
                                )
                        ]
                ),
                new BlockStatement(
                        [
                                new ExpressionStatement(
                                        new MethodCallExpression(
                                                new VariableExpression("this"),
                                                new ConstantExpression("println"),
                                                new ArgumentListExpression(
                                                        new VariableExpression("x"),
                                                )
                                        )
                                )
                        ],
                        new VariableScope()
                )
        )

        AstAssert.assertSyntaxTree([expected], result)
    }

    void testStaticMethodCallExpression_MethodAsString() {
        // Math.min(1,2)
        def result = new AstBuilder().buildFromSpec {
            staticMethodCall(Math, "min") {
                argumentList {
                    constant 1
                    constant 2
                }
            }
        }

        def expected = new StaticMethodCallExpression(
                ClassHelper.make(Math, false),
                "min",
                new ArgumentListExpression(
                        new ConstantExpression(1),
                        new ConstantExpression(2)
                )
        )

        AstAssert.assertSyntaxTree([expected], result)
    }

    void testStaticMethodCallExpression_PassingMethodPointer() {
        // Math.min(1,2)
        def result = new AstBuilder().buildFromSpec {
            staticMethodCall(Math.&min) {      // more terse way to call existing methods
                argumentList {
                    constant 1
                    constant 2
                }
            }
        }

        def expected = new StaticMethodCallExpression(
                ClassHelper.make(Math, false),
                "min",
                new ArgumentListExpression(
                        new ConstantExpression(1),
                        new ConstantExpression(2)
                )
        )

        AstAssert.assertSyntaxTree([expected], result)
    }

    void testSpreadExpression() {
        // ['foo', *['bar','baz']]
        def result = new AstBuilder().buildFromSpec {
            list {
                constant 'foo'
                spread {
                    list {
                        constant 'bar'
                        constant 'baz'
                    }
                }
            }
        }

        def expected = new ListExpression([
            new ConstantExpression('foo'),
            new SpreadExpression(
                new ListExpression([
                        new ConstantExpression('bar'),
                        new ConstantExpression('baz'),
                ])
            )
        ])

        AstAssert.assertSyntaxTree([expected], result)
    }

    void testSpreadMapExpression() {
        // func (*:m)
        def result = new AstBuilder().buildFromSpec {
            methodCall {
                variable 'this'
                constant 'func'
                mapEntry {
                    spreadMap {
                        variable 'm'
                    }
                    variable 'm'
                }
            }
        }

        def expected = new MethodCallExpression(
                new VariableExpression('this', ClassHelper.make(Object, false)),
                'func',
                new MapEntryExpression(
                        new SpreadMapExpression(new VariableExpression('m', ClassHelper.make(Object, false))),
                        new VariableExpression('m', ClassHelper.make(Object, false))
                )

        )

        AstAssert.assertSyntaxTree([expected], result)
    }

    void testTernaryExpression() {
        // true ? "male" : "female"
        def result = new AstBuilder().buildFromSpec {
            ternary {
                booleanExpression {
                    constant true
                }
                constant 'male'
                constant 'female'
            }
        }

        def expected = new TernaryExpression(
                new BooleanExpression(new ConstantExpression(true)),
                new ConstantExpression('male'),
                new ConstantExpression('female')
        )

        AstAssert.assertSyntaxTree([expected], result)
    }

    void testDoWhileStatement() {
        // DoWhileStatement is not currently supported by Groovy syntax but is allowed in the AST
    }

    void testStatement() {
        // Statement is used as an abstract class within the groovy source and is never instantiated
    }

    void testWhileStatementAndContinueStatement() {
        /*
              while (true) {
                  x++
                  continue
              }
          */
        def result = new AstBuilder().buildFromSpec {
            whileStatement {
                booleanExpression {
                    constant true
                }
                block {
                    expression {
                        postfix {
                            variable 'x'
                            token '++'
                        }
                    }
                    continueStatement()
                }
            }
        }

        def expected = new WhileStatement(
                new BooleanExpression(
                        new ConstantExpression(true)
                ),
                new BlockStatement(
                        [
                                new ExpressionStatement(
                                        new PostfixExpression(
                                                new VariableExpression("x"),
                                                new Token(Types.PLUS_PLUS, "++", -1, -1),
                                        )
                                ),
                                new ContinueStatement()
                        ],
                        new VariableScope()
                )
        )

        AstAssert.assertSyntaxTree([expected], result)
    }

    void testWhileStatementAndContinueToLabelStatement() {
        /*
              while (true) {
                  x++
                  continue "some label"
              }
          */
        def result = new AstBuilder().buildFromSpec {
            whileStatement {
                booleanExpression {
                    constant true
                }
                block {
                    expression {
                        postfix {
                            variable 'x'
                            token '++'
                        }
                    }
                    continueStatement {
                        label "some label"
                    }
                }
            }
        }

        def expected = new WhileStatement(
                new BooleanExpression(
                        new ConstantExpression(true)
                ),
                new BlockStatement(
                        [
                                new ExpressionStatement(
                                        new PostfixExpression(
                                                new VariableExpression("x"),
                                                new Token(Types.PLUS_PLUS, "++", -1, -1),
                                        )
                                ),
                                new ContinueStatement("some label")
                        ],
                        new VariableScope()
                )
        )

        AstAssert.assertSyntaxTree([expected], result)
    }

    void testElvisOperatorExpression() {
        // name ?: 'Anonymous'
        def result = new AstBuilder().buildFromSpec {
            elvisOperator {
                variable 'name'
                constant 'Anonymous'
            }
        }

        def expected = new ElvisOperatorExpression(
                new VariableExpression('name'),
                new ConstantExpression('Anonymous')
        )

        AstAssert.assertSyntaxTree([expected], result)
    }

    void testNamedArgumentListExpression() {
        // new String(foo: 'bar')

        def result = new AstBuilder().buildFromSpec {
            constructorCall(String) {
                tuple {
                    namedArgumentList {
                        mapEntry {
                            constant 'foo'
                            constant 'bar'
                        }
                    }
                }
            }
        }

        def expected = new ConstructorCallExpression(
                ClassHelper.make(String),
                new TupleExpression(
                        new NamedArgumentListExpression(
                                [
                                        new MapEntryExpression(
                                                new ConstantExpression('foo'),
                                                new ConstantExpression('bar'),
                                        )
                                ]
                        )
                )
        )

        AstAssert.assertSyntaxTree([expected], result)
    }

    void testParameters_DefaultValues() {
        /*
          public String myMethod(String parameter = null) {
            'some result'
          }
         */

        def result = new AstBuilder().buildFromSpec {
            method('myMethod', ACC_PUBLIC, String) {
                parameters {
                    parameter 'parameter': String.class, {
                        constant null
                    }
                }
                exceptions {}
                block {
                    returnStatement {
                        constant 'some result'
                    }
                }
                annotations {}
            }
        }

        def expected = new MethodNode(
                "myMethod",
                ACC_PUBLIC,
                ClassHelper.make(String.class, false),
                [new Parameter(ClassHelper.make(String, false), "parameter", new ConstantExpression(null))] as Parameter[],
                [] as ClassNode[],
                new BlockStatement(
                        [new ReturnStatement(
                                new ConstantExpression('some result')
                        )],
                        new VariableScope()
                ))
        AstAssert.assertSyntaxTree([expected], result)
    }

    void testParameters_VarArgs() {
        /*
          public String myMethod(String... parameters) {
            'some result'
          }
         */
        // vararg methods are just array methods.
        def result = new AstBuilder().buildFromSpec {
            method('myMethod', ACC_PUBLIC, String) {
                parameters {
                    parameter 'parameters': String[].class
                }
                exceptions {}
                block {
                    returnStatement {
                        constant 'some result'
                    }
                }
            }
        }

        def expected = new MethodNode(
                "myMethod",
                ACC_PUBLIC,
                ClassHelper.make(String.class, false),
                [new Parameter(ClassHelper.make(String[], false), "parameters")] as Parameter[],
                [] as ClassNode[],
                new BlockStatement(
                        [new ReturnStatement(
                                new ConstantExpression('some result')
                        )],
                        new VariableScope()
                ))
        AstAssert.assertSyntaxTree([expected], result)

    }

    void testInnerClassNode() {
        /*
            class Foo {
              static class Bar {
              }
            }
        */
        def result = new AstBuilder().buildFromSpec {
            innerClass 'Foo$Bar', ACC_PUBLIC, {
                //outer class
                classNode 'Foo', ACC_PUBLIC, {
                    classNode Object        //superclass
                    interfaces {
                        classNode GroovyObject
                    }
                    mixins {}
                }
                classNode Object            //superclass
                interfaces {
                    classNode GroovyObject
                }
                mixins {}
            }
        }

        def expected = new InnerClassNode(
                new ClassNode(
                        "Foo",
                        ACC_PUBLIC,
                        ClassHelper.make(Object, false),
                        [ClassHelper.make(GroovyObject, false)] as ClassNode[],
                        [] as MixinNode[]
                ),
                'Foo$Bar',
                ACC_PUBLIC,
                ClassHelper.make(Object, false),
                [ClassHelper.make(GroovyObject, false)] as ClassNode[],
                [] as MixinNode[]
        )

        AstAssert.assertSyntaxTree([expected], result)
    }

    void testAnnotatedNode() {
        // this class is never instantiated. It is used as an abstract class but not marked as such.
    }

    void testConstructorNode() {

        // public <init>(String foo, Integer bar) throws IOException, Exception {}
        def result = new AstBuilder().buildFromSpec {
            constructor(ACC_PUBLIC) {
                parameters {
                    parameter 'foo': String.class
                    parameter 'bar': Integer.class
                }
                exceptions {
                    classNode Exception
                    classNode IOException
                }
                block {

                }
            }
        }

        def expected = new ConstructorNode(
                ACC_PUBLIC,
                [
                        new Parameter(ClassHelper.make(String, false), "foo"),
                        new Parameter(ClassHelper.make(Integer, false), "bar")
                ] as Parameter[],
                [
                        ClassHelper.make(Exception, false),
                        ClassHelper.make(IOException, false)
                ] as ClassNode[],
                new BlockStatement()
        )
        AstAssert.assertSyntaxTree([expected], result)
    }

    void testGenericsType() {
        // class MyClass<T, U extends Number> {}

        def result = new AstBuilder().buildFromSpec {
            classNode 'MyClass', ACC_PUBLIC, {
                classNode Object        //superclass
                interfaces {
                    classNode GroovyObject
                }
                mixins {}
                genericsTypes {
                    genericsType Object
                    genericsType Number, {
                        upperBound {
                            classNode Number
                        }
                    }
                }
            }
        }

        def expected = new ClassNode(
                "MyClass", ACC_PUBLIC, ClassHelper.make(Object, false)
        )
        expected.setGenericsTypes(
                [
                        new GenericsType(ClassHelper.make(Object, false)),
                        new GenericsType(ClassHelper.make(Number, false), [ClassHelper.make(Number, false)] as ClassNode[], null),
                ] as GenericsType[]
        )
        AstAssert.assertSyntaxTree([expected], result)
    }

    void testClassWithMethods() {
        // class MyClass {
        //   String myProp = 'foo'
        //   String myMethod(String parameter) throws IOException { 'some result' }
        //   String myOtherMethod() { 'some other result' }
        // }

        def result = new AstBuilder().buildFromSpec {
            classNode 'MyClass', ACC_PUBLIC, {
                classNode Object        //superclass
                interfaces {
                    classNode GroovyObject
                }
                mixins {}
                methods {
                    method('myMethod', ACC_PUBLIC, String) {
                        parameters {
                            parameter 'parameter': String
                        }
                        exceptions {
                            classNode IOException
                        }
                        block {
                            returnStatement {
                                constant 'some result'
                            }
                        }
                    }
                    method('myOtherMethod', ACC_PUBLIC, String) {
                        parameters {}
                        exceptions {}
                        block {
                            returnStatement {
                                constant 'some other result'
                            }
                        }
                    }
                }
                properties {
                    propertyNode "myProp", ACC_PUBLIC, String, this.class, {
                        constant "foo"
                        annotations {
                            annotation Deprecated
                        }
                    }
                }
                annotations {
                    annotation Deprecated
                }
            }
        }

        def expected = new ClassNode("MyClass", ACC_PUBLIC, ClassHelper.make(Object, false))
        expected.addAnnotation(new AnnotationNode(ClassHelper.make(Deprecated, false)))
        def pNode = new PropertyNode("myProp", ACC_PUBLIC, ClassHelper.make(String, false),
                ClassHelper.make(this.class, false), new ConstantExpression("foo"), null, null)
        pNode.addAnnotation(new AnnotationNode(ClassHelper.make(Deprecated, false)))
        expected.addProperty(pNode)
        expected.addMethod(new MethodNode(
                "myMethod",
                ACC_PUBLIC,
                ClassHelper.make(String, false),
                [new Parameter(ClassHelper.make(String, false), "parameter")] as Parameter[],
                [ClassHelper.make(IOException, false)] as ClassNode[],
                new BlockStatement(
                        [new ReturnStatement(new ConstantExpression('some result'))], new VariableScope()
                )))
        expected.addMethod(new MethodNode(
                "myOtherMethod",
                ACC_PUBLIC,
                ClassHelper.make(String, false),
                [] as Parameter[],
                [] as ClassNode[],
                new BlockStatement(
                        [new ReturnStatement(new ConstantExpression('some other result'))], new VariableScope()
                )))
        AstAssert.assertSyntaxTree([expected], result)
    }

    void testGenericsType_WithLowerBounds() {
        // class MyClass<T, U extends Number> {}

        def result = new AstBuilder().buildFromSpec {
            classNode 'MyClass', ACC_PUBLIC, {
                classNode Object        //superclass
                interfaces {
                    classNode GroovyObject
                }
                mixins {}
                genericsTypes {
                    genericsType Object
                    genericsType Number, {
                        upperBound {
                            classNode Number        //upper bound 1
                            classNode Comparable    //upper bound 2
                        }
                        lowerBound Integer
                    }
                }
            }
        }

        def expected = new ClassNode(
                "MyClass", ACC_PUBLIC, ClassHelper.make(Object, false)
        )
        expected.setGenericsTypes(
                [
                        new GenericsType(ClassHelper.make(Object, false)),
                        new GenericsType(
                                ClassHelper.make(Number, false),
                                [ClassHelper.make(Number, false), ClassHelper.make(Comparable, false)] as ClassNode[],
                                ClassHelper.make(Integer, false)),
                ] as GenericsType[]
        )
        AstAssert.assertSyntaxTree([expected], result)
    }

    void testImportNode() {
        // what source will trigger this node?
        def result = new AstBuilder().buildFromSpec {
            importNode String, "string"
            importNode Integer
        }

        def expected = [
                new ImportNode(ClassHelper.make(String, false), "string"),
                new ImportNode(ClassHelper.make(Integer, false), null)
        ]

        AstAssert.assertSyntaxTree(expected, result)
    }

    void testMethodNode() {
        /*
          @Override
          public String myMethod(String parameter) throws Exception, IOException {
            'some result'
          }
        }
         */

        def result = new AstBuilder().buildFromSpec {
            method('myMethod', ACC_PUBLIC, String) {
                parameters {
                    parameter 'parameter': String.class
                }
                exceptions {
                    classNode Exception
                    classNode IOException
                }
                block {
                    returnStatement {
                        constant 'some result'
                    }
                }
                annotations {
                    annotation Override
                }
            }
        }

        def expected = new MethodNode(
                "myMethod",
                ACC_PUBLIC,
                ClassHelper.make(String, false),
                [new Parameter(ClassHelper.make(String, false), "parameter")] as Parameter[],
                [ClassHelper.make(Exception, false), ClassHelper.make(IOException, false)] as ClassNode[],
                new BlockStatement(
                        [new ReturnStatement(
                                new ConstantExpression('some result')
                        )],
                        new VariableScope()
                ))
        expected.addAnnotation(new AnnotationNode(ClassHelper.make(Override, false)))
        AstAssert.assertSyntaxTree([expected], result)
    }

    void testAnnotation_WithParameter() {
        // @org.junit.Test(timeout=50L) def myMethod() {}
        def result = new AstBuilder().buildFromSpec {
            method 'myMethod', ACC_PUBLIC, Object, {
                parameters {}
                exceptions {}
                block { }
                annotations {
                    annotation(Override) {
                        member 'timeout', {
                            constant 50L
                        }
                    }
                }
            }
        }

        def expected = new MethodNode(
                "myMethod",
                ACC_PUBLIC,
                ClassHelper.make(Object, false),
                [] as Parameter[],
                [] as ClassNode[],
                new BlockStatement([], new VariableScope()))

        def annotation = new AnnotationNode(ClassHelper.make(Override, false))
        annotation.setMember('timeout', new ConstantExpression(50L))
        expected.addAnnotation(annotation)

        AstAssert.assertSyntaxTree([expected], result)
    }

    void testMixinNode() {

        // todo: what source code will generate a MixinNode?
        def result = new AstBuilder().buildFromSpec {
            classNode 'MyClass', ACC_PUBLIC, {
                classNode Object        //superclass
                interfaces {
                    classNode GroovyObject
                }
                mixins {
                    mixin "ClassA", ACC_PUBLIC, {
                        classNode String
                    }
                    mixin "ClassB", ACC_PUBLIC, {
                        classNode String
                        interfaces {
                            classNode GroovyObject
                        }
                    }
                }
            }
        }

        def expected = new ClassNode(
                "MyClass", ACC_PUBLIC,
                ClassHelper.make(Object, false),
                [ClassHelper.make(GroovyObject, false)] as ClassNode[],
                [
                        new MixinNode("ClassA", ACC_PUBLIC, ClassHelper.make(String, false)),
                        new MixinNode(
                                "ClassB",
                                ACC_PUBLIC,
                                ClassHelper.make(String, false),
                                [ClassHelper.make(GroovyObject, false)] as ClassNode[]), // interfaces
                ] as MixinNode[]
        )

        AstAssert.assertSyntaxTree([expected], result)
    }

    void testModuleNode() {
        // todo: what source code creates a ModuleNode? ModuleNode has a ton of setters that aren't currently being tested. Should this even be part of the DSL?
    }

    void testPropertyNode() {
        //  def myField = "foo"
        def result = new AstBuilder().buildFromSpec {
            propertyNode "MY_VALUE", ACC_PUBLIC, String, this.class, {
                constant "foo"
            }
        }

        def expected = new PropertyNode(
                "MY_VALUE",
                ACC_PUBLIC,
                ClassHelper.make(String, false),
                ClassHelper.make(this.class, false),
                new ConstantExpression("foo"),
                null,
                null        //todo: do we need to support getter and setter blocks?
        )
        AstAssert.assertSyntaxTree([expected], result)
    }

    void testMethodCallContract_TooManyArguments() {

        def msg = shouldFail(IllegalArgumentException) {
            new AstBuilder().buildFromSpec {
                methodCall {
                    variable "this"
                    constant "println"
                    argumentList {
                        constant "Hello"
                    }
                    constant "illegal value"
                }
            }
        }
        assertEquals("Wrong exception message",
                "methodCall could not be invoked. Expected to receive parameters [class org.codehaus.groovy.ast.expr.Expression, class org.codehaus.groovy.ast.expr.Expression, class org.codehaus.groovy.ast.expr.Expression] but found [class org.codehaus.groovy.ast.expr.VariableExpression, class org.codehaus.groovy.ast.expr.ConstantExpression, class org.codehaus.groovy.ast.expr.ArgumentListExpression, class org.codehaus.groovy.ast.expr.ConstantExpression]",
                msg)
    }

    void testMethodCallContract_TooFewArguments() {

        def msg = shouldFail(IllegalArgumentException) {
            new AstBuilder().buildFromSpec {
                methodCall {
                    variable "this"
                    constant "println"
                    // missing argument list!
                }
            }
        }
        assertEquals("Wrong exception message",
                "methodCall could not be invoked. Expected to receive parameters [class org.codehaus.groovy.ast.expr.Expression, class org.codehaus.groovy.ast.expr.Expression, class org.codehaus.groovy.ast.expr.Expression] but found [class org.codehaus.groovy.ast.expr.VariableExpression, class org.codehaus.groovy.ast.expr.ConstantExpression]",
                msg)
    }

    void testAnnotationConstantExpressionContract_TooFewArguments() {

        shouldFail(IllegalArgumentException) {
            new AstBuilder().buildFromSpec {
                annotationConstant {
                    // missing argument
                }
            }
        }
    }

    void testAnnotationConstantExpressionContract_TooManyArguments() {

        shouldFail(IllegalArgumentException) {
            new AstBuilder().buildFromSpec {
                annotationConstant {
                    annotation Override
                    constant 'illegal parameter'
                }
            }
        }
    }

    void testConstructorCallExpressionContract_TooFewParameters() {

        shouldFail(IllegalArgumentException) {
            new AstBuilder().buildFromSpec {
                constructorCall(Integer) {
                    // missing argument list
                }
            }
        }
    }

    void testConstructorCallExpressionContract_TooManyParameters() {

        shouldFail(IllegalArgumentException) {
            new AstBuilder().buildFromSpec {
                constructorCall(Integer) {
                    argumentList {
                        constant 4
                    }
                    constant 'illegal argument'
                }
            }
        }
    }
}
