import java.util.TreeSet;

import junit.framework.TestCase;

/**
 * @author David Matuszek
 * @version Feb 3, 2006
 */
public class RecognizerTest extends TestCase {

    Recognizer r0, r1, r2, r3, r4, r5, r6, r7, r8;
    TreeSet<String> visited = new TreeSet<String>();
    
    /**
     * Constructor for RecognizerTest.
     * @param arg0
     */
    public RecognizerTest(String arg0) {
        super(arg0);
        r0 = use("2 + 2");
        r1 = use("");
    }

    /**
     * Creates Recognizers for some Strings.
     *
     * @see TestCase#setUp()
     */
    protected void setUp() throws Exception {
        super.setUp();
        r0 = use("");
        r1 = use("250");
        r2 = use("hello");
        r3 = use("(xyz + 3)");
        r4 = use("12 * 5 - 3 * 4 / 6 + 8");
        r5 = use("12 * ((5 - 3) * 4) / 6 + (8)");
        r6 = use("17 +");
        r7 = use("22 *");
        r8 = use("#");
    }
    
    public void testRecognizer() {
        r0 = new Recognizer("");
        r1 = new Recognizer("2 + 2");
        r0 = use("");
        r1 = use("2 + 2");
    }

    public void testExpression() {
        assertTrue(r1.expression());
        assertTrue(r2.expression());
        assertTrue(r3.expression());
        assertTrue(r4.expression());
        assertTrue(r5.expression());

        assertFalse(r0.expression());
        assertFalse(r8.expression());

        try {
            assertFalse(r6.expression());
            fail();
        }
        catch (LogoException e) {
        }
        try {
            assertFalse(r7.expression());
            fail();
        }
        catch (LogoException e) {
        }
    }

    public void testTerm() {
        assertFalse(r0.term()); // ""

        assertTrue(r1.term()); // "250"

        assertTrue(r2.term()); // "hello"

        assertTrue(r3.term()); // "(xyz + 3)"
        followedBy(r3, "");

        assertTrue(r4.term());  // "12 * 5 - 3 * 4 / 6 + 8"
        assertEquals(new Token(Type.SYMBOL, "-"), nextToken(r4));
        assertTrue(r4.term());
        followedBy(r4, "+ 8");

        assertTrue(r5.term());  // "12 * ((5 - 3) * 4) / 6 + (8)"
        assertEquals(new Token(Type.SYMBOL, "+"), nextToken(r5));
        assertTrue(r5.term());
        followedBy(r5, "");
    }

    /**
     * Test method for 'Tokenizer.hasNext()'
     */
    public void testFactor() {
        assertTrue(r1.factor());
        assertTrue(r2.factor());
        assertTrue(r3.factor());
        assertTrue(r4.factor()); followedBy(r4, "* 5 - 3 * 4 / 6 + 8");
        // r5 = "12 * ((5 - 3) * 4) / 6 + (8)"
        assertTrue(r5.factor());
        followedBy(r5, "* ((5");
        assertTrue(r6.factor()); followedBy(r6, "+");
        assertTrue(r7.factor()); followedBy(r7, "*");

        assertFalse(r0.factor());
        assertFalse(r8.factor()); followedBy(r8, "#");
    }
    
    public void testDisjunct() {
        r1 = use("2 < 3");
        assertTrue(r1.disjunct()); atEnd(r1);
        r2 = use("2 < 3 and 2+2 = 4");
        assertTrue(r2.disjunct()); atEnd(r2);
        r3 = use("2 < 3 and 2+2 = 4 and a + b > (2 + 3) * 5");
        assertTrue(r3.disjunct()); atEnd(r3);
        try { // Don't care whether this fails or throws exception
            r4 = use("2 + 2");
            assertFalse(r4.disjunct());
        }
        catch (LogoException e) {}
        try {
            r5 = use("2 < 3 and 2 + 2");
            if (r5.disjunct()) fail("Incomplete disjunct");
            else bad();
        }
        catch (LogoException e) {}
        
    }
    
    public void testConjunct() {
        r1 = use("2 < 3");
        assertTrue(r1.conjunct()); atEnd(r1);
        r2 = use("2+2 = 4");
        assertTrue(r2.conjunct()); atEnd(r2);
        r3 = use("a + b > (2 + 3) * 5");
        assertTrue(r3.conjunct()); atEnd(r3);
        r4 = use("2 * 2 = 2 + 2)");
        assertTrue(r4.conjunct()); followedBy(r4, ")");
        try { // Don't care whether this fails or throws exception
            r5 = use("2 + 2");
            assertFalse(r5.conjunct());
        }
        catch (LogoException e) {}
        
        r1 = use("not 2 < 3");
        assertTrue(r1.conjunct());
        r2 = use("not 2+2 = 4");
        assertTrue(r2.conjunct());
        r3 = use("not a + b > (2 + 3) * 5");
        assertTrue(r3.conjunct());
        r4 = use("not 2 * 2 = 2 + 2)");
        assertTrue(r4.conjunct());
        followedBy(r4, ")");
        try { // Don't care whether this fails or throws exception
            r5 = use("not 2 + 2");
            assertFalse(r5.conjunct());
        }
        catch (LogoException e) {}        
    }

    public void testLogicalFactor() {
        r1 = use("2 < 3");
        assertTrue(r1.logicalFactor());
        r2 = use("2+2 = 4");
        assertTrue(r2.logicalFactor());
        r3 = use("a + b > (2 + 3) * 5");
        assertTrue(r3.logicalFactor());
        r4 = use("2 * 2 = 2 + 2)");
        assertTrue(r4.logicalFactor());
        followedBy(r4, ")");
        
        try {
            r5 = use("(a = b)");
            if (r5.logicalFactor()) fail("Cannot use parentheses here");
            else bad();
        }
        catch (LogoException e) {}
        
        r6 = use("2 < 3 * ((5 - 2) * 7)");
        assertTrue(r6.logicalFactor());
        atEnd(r6);
        r7 = use("2 < 3 < 4");
        assertTrue(r7.logicalFactor());
        followedBy(r7, "<4");
        
        //TODO Try conditions with logical operators
//        r6 = use()
//        assertTrue(r6.factor()); followedBy(r6, "+");
//        assertTrue(r7.factor()); followedBy(r7, "*");
//
//        assertFalse(r0.factor());
//        assertFalse(r8.factor()); followedBy(r8, "#");
    }

    public void testAdd_operator() {
        Recognizer r = use("+ - $");
        assertTrue(r.addOperator());
        assertTrue(r.addOperator());
        assertFalse(r.addOperator());
        followedBy(r, "$");
    }

    public void testMultiply_operator() {
        Recognizer r = use("* / $");
        assertTrue(r.multiplyOperator());
        assertTrue(r.multiplyOperator());
        assertFalse(r.multiplyOperator());
        followedBy(r, "$");
    }

    final public void testVariable() {
        r1 = use("hello there");
        assertTrue(r1.variable());
        assertTrue(r1.variable());
    }

    public void testPenup() {
        r1 = use("penup\n .");
        assertTrue(r1.command());
        followedBy(r1, ".");
    }

    public void testPendown() {
        r1 = use("pendown \n .");
        assertTrue(r1.command());
        followedBy(r1, ".");
    }

    public void testColor() {
        r1 = use("color red \n .");
        assertTrue(r1.command());
        followedBy(r1, ".");

        String[] colors = new String[] { "magenta", "red", "orange",
                                        "yellow", "green", "aqua",
                                        "blue", "purple", "violet",
                                        "brown", "black", "charcoal",
                                        "gray", "grey", "white", "pink" };
        for (String color : colors) {
            r1 = use(color);
            assertTrue(r1.colorName());
        }

        r1 = use("color red;\n .");
        try {
            if (r1.command()) fail("Color name not followed by EOL");
            else bad();
        }
        catch (LogoException e) {
        }
    }

    public void testHome() {
        r1 = use("home \n .");
        assertTrue(r1.command());
        followedBy(r1, ".");
    }

    public void testSet() {
        r1 = use("x = y \n .");
        assertTrue(r1.command());
        followedBy(r1, ".");

        r1 = use("x  = y + 1 \n .");
        assertTrue(r1.command());
        followedBy(r1, ".");

        r1 = use("x = \n");
        try {
            if (r1.command()) fail("\"=\" not followed by an expression");
            else bad();
        }
        catch (LogoException e) {}

        r1 = use("x = x + 1");
        try {
            if (r1.command()) fail("Assignment command not followed by EOL");
            else bad();
        }
        catch (LogoException e) {}

        r1 = use("x = x + 1; \n");
        try {
            if (r1.command()) fail("Garbage at end of assignment command");
            else bad();
        }
        catch (LogoException e) {}
    }
    
    private void bad() {
        at();
    }

    /**
     * This method writes out where it was called from.
     */
    public void at() {
        Exception e = new Exception();
        StackTraceElement[] elements = e.getStackTrace();
        int lineNumber = elements[2].getLineNumber();
        String methodName = elements[2].getMethodName();
        String className = elements[2].getClassName();
        String location = className + "." + methodName;
        if (!visited.contains(location)) {
            System.out.println("\n********* Better to throw exception **********");
            visited.add(location);
        }
        System.out.println("At line " + lineNumber + " in " +
                           methodName + " in class " + className);
    }
    
    public void testRepeat() {
        String block = "[ \n penup \n ] \n";
        r1 = use(block);
        assertTrue(r1.block());

        r1 = use("repeat 5 " + block);
        assertTrue(r1.command());

        r1 = use("repeat 5 + " + block);
        try {
            if (r1.command()) fail("Error in repeat command expression");
            else bad();
        }
        catch (LogoException e) {}

        r1 = use("repeat 5 \n");
        try {
            if (r1.command()) fail("Repeat <expression> not followed by a block");
            else bad();
        }
        catch (LogoException e) {}
    }

    public void testWhile() {
        String block = "[ \n penup \n ] \n";
        r1 = use("while 2 = 2 " + block);
        assertTrue(r1.command());

        r1 = use("while 2 + 2 " + block);
        try {
            if (r1.command()) fail("Error in while condition");
            else bad();
        }
        catch (LogoException e) {}

        r1 = use("while 2 = 2");
        try {
            if (r1.command()) fail("while <condition> not followed by a block");
            else bad();
        }
        catch (LogoException e) {}
    }

    public void testIf() {
        String block = "[ \n penup \n ] \n";
        r1 = use("if 2 = 2 " + block);
        assertTrue(r1.command());

        r1 = use("if 2 + 2 " + block);
        try {
            if (r1.command()) fail("Error in if condition");
            else bad();
        }
        catch (LogoException e) {}

        r1 = use("if 2 = 2");
        try {
            if (r1.command()) fail("if <condition> not followed by a block");
            else bad();
        }
        catch (LogoException e) {}
    }
    
    public void testParentheticalExpression() {
        r1 = use("if (2 + 2) = 4 [ \n ] \n");
        assertTrue(r1.command());

        r2 = use("if (2 + 2) = 4 and 3 > (5) [ \n ] \n");
        assertTrue(r2.command());

        r3 = use("if (2 + 2) = 4 or 2 + 2 = 5 [ \n ] \n");
        assertTrue(r3.command());

        r4 = use("if (2 + 2) = 4 and 3 < 4 or 4 > 3[ \n ] \n");
        assertTrue(r4.command());

        r5 = use("if not (2 + 2) = 4 [ \n ] \n");
        assertTrue(r5.command());

        try {
            r6 = use("if not not (2 + 2) = 4 [ \n ] \n");
            if (r6.command()) fail("Grammar does not allow \"not not\"");
            else bad();
        }
        catch (LogoException e) {}

        r7 = use("if not (2 + 2) = 4 and not 3 < 4 or not 4 > 3 [ \n ] \n");
        assertTrue(r7.command());
    }

    public void testCall() {
        r1 = use("call foo\n call bar 3 + x\n call baz;\n");
        assertTrue(r1.command());
        assertTrue(r1.command());
        r1 = use("call foo bar 3 + x baz\n");
        assertTrue(r1.command());
    }
    
    final public void testMove() {
        r1 = use("move 3 \n move 12 - 5 \n and move x + y\n");
        assertTrue(r1.command());
        assertTrue(r1.command());
        assertEquals(name("and"), nextToken(r1));
        assertTrue(r1.command());
        atEnd(r1);
        r1 = use("move \n move 5 . ");
        try {
            if (r1.command()) fail("No expression after \"move\"");
            else bad();
        }
        catch (LogoException e) {}
        r1.eol();
        try {
            if (r1.command()) fail("Junk in \"move\" command");
            else bad();
        }
        catch (LogoException e) {}
    }


    final public void testTurn() {
        r1 = use("turn north \n turn south \n turn east " +
                "\n turn west\n");
        assertTrue(r1.command());
        assertTrue(r1.command());
        assertTrue(r1.command());
        assertTrue(r1.command());
        atEnd(r1);
        r1 = use("turn right\n turn left \n " +
                "turn right 90\n turn left x + 30\n");
        assertTrue(r1.command());
        assertTrue(r1.command());
        assertTrue(r1.command());
        assertTrue(r1.command());
        atEnd(r1);
        r1 = use("turn left;");
        try {
            if (r1.command()) fail("Junk after \"left\"");
            else bad();
        }
        catch (LogoException e) {}
        r1 = use("turn right");
        try {
            if (r1.command()) fail("Incomplete \"turn\" command");
            else bad();
        }
        catch (LogoException e) {}
        r1 = use("turn left 60;");
        try {
            if (r1.command()) fail("Junk after \"left\" command");
            else bad();
        }
        catch (LogoException e) {}
    }

    final public void testColorName() {
        r1 = use("magenta red orange yellow green aqua blue " +
                "purple violet brown black charcoal gray grey white pink x");
        assertTrue(r1.colorName()); // magenta
        assertTrue(r1.colorName()); // red
        assertTrue(r1.colorName()); // orange
        assertTrue(r1.colorName()); // yellow
        assertTrue(r1.colorName()); // green
        assertTrue(r1.colorName()); // aqua
        assertTrue(r1.colorName()); // blue
        assertTrue(r1.colorName()); // purple
        assertTrue(r1.colorName()); // violet
        assertTrue(r1.colorName()); // brown
        assertTrue(r1.colorName()); // black
        assertTrue(r1.colorName()); // charcoal
        assertTrue(r1.colorName()); // gray
        assertTrue(r1.colorName()); // grey
        assertTrue(r1.colorName()); // white
        assertTrue(r1.colorName()); // pink
        assertFalse(r1.colorName()); // x
        followedBy(r1, "x");
    }

    final public void testBlock() {
        r1 = use("[ \n ] \n");
        assertTrue(r1.block());

        r1 = use("[ \n penup \n color red \n home \n x = y \n ] \n");
        assertTrue(r1.block());
    }

    final public void testCondition() {
        r1 = use("2 < 3, x = 4 + y, 3 * 3 > 5, 2 + 2 = 3 + 1, x");
        Token comma = symbol(",");

        assertTrue(r1.condition()); // 2 < 3
        assertFalse(r1.condition());
        assertEquals(comma, nextToken(r1));

        assertTrue(r1.condition()); // x = 4 + y
        assertFalse(r1.condition());
        assertEquals(comma, nextToken(r1));

        assertTrue(r1.condition()); // 3 * 3 > 5
        assertFalse(r1.condition());
        assertEquals(comma, nextToken(r1));

        assertTrue(r1.condition()); // 2 + 2 = 3 + 1
        assertFalse(r1.condition());
        assertEquals(comma, nextToken(r1));
        
        r2 = use("2 < 3 or 2 > 3");
        assertTrue(r2.condition()); atEnd(r2);
        
        r3 = use("2 < 3 and 2 > 3");
        assertTrue(r3.condition()); atEnd(r3);
        
        r4 = use("2 < 3 or 2 > 3 and 2 = 3");
        assertTrue(r4.condition()); atEnd(r4);
        
        r5 = use("2 = 3 and 2 < 3 and 2 > 3");
        assertTrue(r5.condition()); atEnd(r5);

        try { // Don't care whether this fails or throws exception
            r1 = use(",");
            assertFalse(r1.condition());
        }
        catch (LogoException e) {};
    }

    final public void testComparator() {
        r1 = use("< = <> !");
        assertTrue(r1.comparator()); //  <
        assertTrue(r1.comparator()); //  =
        assertTrue(r1.comparator()); //  <
        assertTrue(r1.comparator()); //  >
        assertFalse(r1.comparator()); // !
        followedBy(r1, "!");
    }
    
    final public void testComparison() {
        r1 = use("x = y x > y x < y");
        assertTrue(r1.comparison());
        assertTrue(r1.comparison());
        assertTrue(r1.comparison());
        r1 = use("x + 1 = 1 + x 2 * y = 5 w == w");
        assertTrue(r1.comparison());
        assertTrue(r1.comparison());
        try {
            if (r1.comparison()) fail("Not a valid comparison");
            else bad();
        }
        catch (LogoException e) {}
    }

    final public void testEol() {
        r1 = use("+ \n + \r +");
        assertFalse(r1.eol()); skip(r1);
        assertTrue(r1.eol());
        assertFalse(r1.eol()); skip(r1);
        assertFalse(r1.eol());
        followedBy(r1, "\r +");
    }

    final public void testProcedure() {
        r1 = use("define foo\n end\n");
        assertTrue(r1.procedure());
    }
    
    final public void testProgram() {
        r1 = use("color red\n");
        assertTrue(r1.program());
        
        try { // Don't care whether this fails or throws exception
            r1 = use("color red\n garbage");
            assertFalse(r1.program());
        }
        catch (LogoException e) {};
        
        String mainProgram = "color blue\n pendown\n move 50\n";
        r1 = use(mainProgram);
        assertTrue(r1.program());
        
        String procedure1 = "define foo\n end\n";
        r1 = use(mainProgram + procedure1);
        assertTrue(r1.program());
        
        String procedure2 = "define bar a b\n move a\n turn left b\n end\n";
        r1 = use(mainProgram + procedure2);
        assertTrue(r1.program());

        r1 = use(mainProgram + procedure1 + procedure2);
        assertTrue(r1.program());
        
        try { // Don't care whether this fails or throws exception
            r1 = use(mainProgram + procedure1 + procedure2 + "$$");
            assertFalse(r1.program());
        }
        catch (LogoException e) {};
     }



//  ----- "Helper" methods

    private void followedBy(Recognizer recognizer, String rest) {
        Tokenizer actual = recognizer.tokenizer;
        Tokenizer expected = new Tokenizer(rest);
        while (expected.hasNext()) {
            Token actualToken = (Token)actual.next();
            Token expectedToken = (Token)expected.next();
            if (expectedToken.type == Type.END_OF_INPUT) break;
            assertEquals(expectedToken, actualToken);
        }
    }

    private boolean atEnd(Recognizer recognizer) {
        Tokenizer tokenizer = recognizer.tokenizer;
        if (tokenizer.hasNext()) {
            assertEquals(Type.END_OF_INPUT, ((Token)tokenizer.next()).type);
        }
        return false;
    }

    private void skip(Recognizer r) {
        nextToken(r);
    }

    /**
     * Creates a Token of type NAME with the specified value.
     *
     * @param value The value of the new Token.
     * @return The new Token.
     */
    private Token name(String value) {
        return new Token(Type.NAME, value);
    }

    /**
     * Creates a Token of type SYMBOL with the specified value.
     *
     * @param value The value of the new Token.
     * @return The new Token.
     */
    private Token symbol(String value) {
        return new Token(Type.SYMBOL, value);
    }

    /**
     * Returns the next Token from the given Recognizer.
     *
     * @param r The Recognizer to use.
     * @return The next Token from the given Recognizer.
     */
    private Token nextToken(Recognizer r) {
        return (Token)r.tokenizer.next();
    }

    /**
     * Returns a Recognizer that will use the given string.
     * @param s
     */
    private Recognizer use(String s) {
        return new Recognizer(s);
    }
}
