/**
 * BSD-style license; for more info see http://pmd.sourceforge.net/license.html
 */

package net.sourceforge.pmd.lang.ecmascript.ast;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;

import java.io.Reader;
import java.io.StringReader;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

import org.junit.Test;
import org.mozilla.javascript.ast.AstRoot;

import net.sourceforge.pmd.PMD;
import net.sourceforge.pmd.RuleContext;
import net.sourceforge.pmd.lang.ast.Node;
import net.sourceforge.pmd.lang.ecmascript.Ecmascript3Parser;
import net.sourceforge.pmd.lang.ecmascript.EcmascriptParserOptions;
import net.sourceforge.pmd.lang.ecmascript.rule.AbstractEcmascriptRule;

public class EcmascriptParserTest extends EcmascriptParserTestBase {

    /**
     * https://sourceforge.net/p/pmd/bugs/1043/
     */
    @Test
    public void testLineNumbers() {
        final String SOURCE_CODE = "function a() {" + PMD.EOL + "  alert('hello');" + PMD.EOL + "}" + PMD.EOL;
        EcmascriptNode<AstRoot> node = parse(SOURCE_CODE);
        assertEquals(1, node.getBeginLine());
        assertEquals(1, node.getBeginColumn());
        assertEquals(3, node.getEndLine());
        assertEquals(1, node.getEndColumn());

        Node child = node.getFirstChildOfType(ASTFunctionNode.class);
        assertEquals(1, child.getBeginLine());
        assertEquals(1, child.getBeginColumn());
        assertEquals(3, child.getEndLine());
        assertEquals(1, child.getEndColumn());

        child = node.getFirstDescendantOfType(ASTFunctionCall.class);
        assertEquals(2, child.getBeginLine());
        assertEquals(3, child.getBeginColumn());
        assertEquals(2, child.getEndLine());
        assertEquals(16, child.getEndColumn());
    }

    /**
     * https://sourceforge.net/p/pmd/bugs/1149/
     */
    @Test
    public void testLineNumbersWithinEcmascriptRules() {
        String source = "function f(x){\n" + "   if (x) {\n" + "       return 1;\n" + "   } else {\n"
                + "       return 0;\n" + "   }\n" + "}";
        final List<String> output = new ArrayList<>();

        class MyEcmascriptRule extends AbstractEcmascriptRule {
            public Object visit(ASTScope node, Object data) {
                output.add("Scope from " + node.getBeginLine() + " to " + node.getEndLine());
                return super.visit(node, data);
            }
        }

        MyEcmascriptRule rule = new MyEcmascriptRule();
        RuleContext ctx = new RuleContext();
        rule.apply(Arrays.asList(parse(source)), ctx);

        assertEquals("Scope from 2 to 4", output.get(0));
        assertEquals("Scope from 4 to 6", output.get(1));
    }

    /**
     * Test bug https://sourceforge.net/p/pmd/bugs/1118/
     */
    @Test
    public void testArrayAccess() {
        EcmascriptNode<AstRoot> node = parse("function a() { b['a'] = 1; c[1] = 2; }");
        List<ASTElementGet> arrays = node.findDescendantsOfType(ASTElementGet.class);
        assertEquals("b", arrays.get(0).getTarget().getImage());
        assertEquals("a", arrays.get(0).getElement().getImage());
        assertEquals("c", arrays.get(1).getTarget().getImage());
        assertEquals("1", arrays.get(1).getElement().getImage());
    }

    /**
     * Test for bug #1136 ECAMScript: NullPointerException in getLeft() and
     * getRight()
     */
    @Test
    public void testArrayMethod() {
        EcmascriptNode<AstRoot> rootNode = parse(
                "function test(){\n" + "  a();      // OK\n" + "  b.c();    // OK\n" + "  d[0]();   // OK\n"
                        + "  e[0].f(); // OK\n" + "  y.z[0](); // FAIL ==> java.lang.NullPointerException\n" + "}");

        List<ASTFunctionCall> calls = rootNode.findDescendantsOfType(ASTFunctionCall.class);
        List<String> results = new ArrayList<>();
        for (ASTFunctionCall f : calls) {
            Node node = f.getTarget();
            results.add(getName(node));
        }
        assertEquals("[a, b.c, d[], e[].f, y.z[]]", results.toString());
    }

    private String getName(Node node) {
        if (node instanceof ASTName) {
            return ((ASTName) node).getIdentifier();
        }
        if (node instanceof ASTPropertyGet) {
            final ASTPropertyGet pgNode = (ASTPropertyGet) node;
            final String leftName = getName(pgNode.getLeft());
            final String rightName = getName(pgNode.getRight());
            return leftName + "." + rightName;
        }
        if (node instanceof ASTElementGet) {
            return getName(((ASTElementGet) node).getTarget()) + "[]";
        }
        return "????";
    }

    /**
     * https://sourceforge.net/p/pmd/bugs/1150/ #1150 "EmptyExpression" for
     * valid statements!
     */
    @Test
    public void testCaseAsIdentifier() {
        ASTAstRoot rootNode = parse("function f(a){\n" + "    a.case.flag = 1;\n" + "    return;\n" + "}");
        ASTBlock block = rootNode.getFirstDescendantOfType(ASTBlock.class);
        assertFalse(block.jjtGetChild(0) instanceof ASTEmptyExpression);
        assertTrue(block.jjtGetChild(0) instanceof ASTExpressionStatement);
        assertTrue(block.jjtGetChild(0).jjtGetChild(0) instanceof ASTAssignment);
    }

    /**
     * https://sourceforge.net/p/pmd/bugs/1045/ #1045 //NOPMD not working (or
     * not implemented) with ECMAscript
     */
    @Test
    public void testSuppresionComment() {
        Ecmascript3Parser parser = new Ecmascript3Parser(new EcmascriptParserOptions());
        Reader sourceCode = new StringReader("function(x) {\n" + "x = x; //NOPMD I know what I'm doing\n" + "}\n");
        parser.parse("foo", sourceCode);
        assertEquals(" I know what I'm doing", parser.getSuppressMap().get(2));
        assertEquals(1, parser.getSuppressMap().size());

        EcmascriptParserOptions parserOptions = new EcmascriptParserOptions();
        parserOptions.setSuppressMarker("FOOOO");
        parser = new Ecmascript3Parser(parserOptions);
        sourceCode = new StringReader(
                "function(x) {\n" + "y = y; //NOPMD xyz\n" + "x = x; //FOOOO I know what I'm doing\n" + "}\n");
        parser.parse("foo", sourceCode);
        assertEquals(" I know what I'm doing", parser.getSuppressMap().get(3));
        assertEquals(1, parser.getSuppressMap().size());
    }

    /**
     * #1191 Ecmascript fails to parse "void(0)"
     */
    @Test
    public void testVoidKeyword() {
        ASTAstRoot rootNode = parse("function f(matchFn, fieldval, n){\n"
                + "    return (matchFn)?(matcharray = eval(matchFn+\"('\"+fieldval+\"','\"+n.id+\"')\")):void(0);\n"
                + "}\n");
        ASTUnaryExpression unary = rootNode.getFirstDescendantOfType(ASTUnaryExpression.class);
        assertEquals("void", unary.getImage());
    }

    /**
     * #1192 Ecmascript fails to parse this operator " ^= "
     */
    @Test
    public void testXorAssignment() {
        ASTAstRoot rootNode = parse("function f() { var x = 2; x ^= 2; x &= 2; x |= 2; "
                + "x &&= true; x ||= false; x *= 2; x /= 2; x %= 2; x += 2; x -= 2; "
                + "x <<= 2; x >>= 2; x >>>= 2; }");
        ASTAssignment infix = rootNode.getFirstDescendantOfType(ASTAssignment.class);
        assertEquals("^=", infix.getImage());
    }
}
