package function.builtin.special;

import static sexpression.Nil.NIL;
import static testutil.TestUtilities.assertSExpressionsMatch;
import static testutil.TestUtilities.evaluateString;

import org.junit.Test;

import function.ArgumentValidator.BadArgumentTypeException;
import function.ArgumentValidator.DottedArgumentListException;
import function.ArgumentValidator.TooFewArgumentsException;
import function.ArgumentValidator.TooManyArgumentsException;
import function.builtin.EVAL.UndefinedSymbolException;
import sexpression.Cons;
import sexpression.LispNumber;
import testutil.SymbolAndFunctionCleaner;

public class LET_STARTest extends SymbolAndFunctionCleaner {

    @Test
    public void simpleLet() {
        String input = "(let* ((x 1)) x)";

        assertSExpressionsMatch(new LispNumber("1"), evaluateString(input));
    }

    @Test
    public void emptyLet_ReturnsNil() {
        String input = "(let* ())";

        assertSExpressionsMatch(NIL, evaluateString(input));
    }

    @Test
    public void letStarWithSymbolsOnly_SetsValuesToNil() {
        String input = "(let* ((x) (y)) (list x y))";

        assertSExpressionsMatch(new Cons(NIL, new Cons(NIL, NIL)), evaluateString(input));
    }

    @Test
    public void letStarWithSetq_DoesNotAlterGlobalVariable() {
        String before = "(setq x 22)";
        String input = "(let* ((x 1)) x)";
        String after = "x";

        assertSExpressionsMatch(new LispNumber("22"), evaluateString(before));
        assertSExpressionsMatch(new LispNumber("1"), evaluateString(input));
        assertSExpressionsMatch(new LispNumber("22"), evaluateString(after));
    }

    @Test
    public void letStarWithNestedSetq_DoesNotAlterGlobalVariable() {
        String before = "(setq x 22)";
        String input = "(let* ((x 33)) (setq x 44) x)";
        String after = "x";

        assertSExpressionsMatch(new LispNumber("22"), evaluateString(before));
        assertSExpressionsMatch(new LispNumber("44"), evaluateString(input));
        assertSExpressionsMatch(new LispNumber("22"), evaluateString(after));
    }

    @Test
    public void nestedLet() {
        String input = "(let* ((x 1)) (let* ((y (+ 1 x))) y))";

        assertSExpressionsMatch(new LispNumber("2"), evaluateString(input));
    }

    @Test
    public void nestedLetWithGlobals() {
        String before = "(setq x 92)";
        String input = "(let* ((x 1)) (let* ((y (+ 1 x))) y))";
        String after = "x";

        assertSExpressionsMatch(new LispNumber("92"), evaluateString(before));
        assertSExpressionsMatch(new LispNumber("2"), evaluateString(input));
        assertSExpressionsMatch(new LispNumber("92"), evaluateString(after));
    }

    @Test
    public void alterGlobalVariableFromLet() {
        String before = "(setq x 1)";
        String input = "(let* ((y 1)) (setq x 2))";
        String after = "x";

        assertSExpressionsMatch(new LispNumber("1"), evaluateString(before));
        assertSExpressionsMatch(new LispNumber("2"), evaluateString(input));
        assertSExpressionsMatch(new LispNumber("2"), evaluateString(after));
    }

    @Test
    public void accessGlobalVariableFromLet() {
        String before = "(setq x 1)";
        String input = "(let* () x)";

        assertSExpressionsMatch(new LispNumber("1"), evaluateString(before));
        assertSExpressionsMatch(new LispNumber("1"), evaluateString(input));
    }

    @Test(expected = UndefinedSymbolException.class)
    public void letStarDoesNotSetGlobalVariable() {
        String input = "(let* ((x 1)) nil)";

        evaluateString(input);
        evaluateString("x");
    }

    @Test(expected = BadArgumentTypeException.class)
    public void letStarWithNonList() {
        evaluateString("(let* a)");
    }

    @Test(expected = BadArgumentTypeException.class)
    public void letStarWithNoPairs() {
        evaluateString("(let* (a))");
    }

    @Test(expected = TooFewArgumentsException.class)
    public void letStarWithTooFewItemsInPair() {
        evaluateString("(let* (()))");
    }

    @Test(expected = TooManyArgumentsException.class)
    public void letStarWithTooManyItemsInPair() {
        evaluateString("(let* ((a b c)))");
    }

    @Test(expected = BadArgumentTypeException.class)
    public void letStarWithNonSymbolInPair() {
        evaluateString("(let* ((1 b)))");
    }

    @Test(expected = TooFewArgumentsException.class)
    public void letStarWithTooFewArguments() {
        evaluateString("(let*)");
    }

    @Test(expected = DottedArgumentListException.class)
    public void letStarWithDottedArgumentList() {
        evaluateString("(apply 'let* (cons 'a 'b))");
    }

    @Test(expected = DottedArgumentListException.class)
    public void letStarWithDottedPairList() {
        evaluateString("(apply 'let* (cons (cons 'a 'b) nil))");
    }

    @Test(expected = DottedArgumentListException.class)
    public void letStarWithDottedPair() {
        evaluateString("(apply 'let* (cons (cons (cons 'a 'b) nil) nil))");
    }

    @Test
    public void letStarEvaluatesSymbolsInSequence() {
        String input = "(let* ((x 1) (y (+ x 1))) (+ x y))";

        assertSExpressionsMatch(new LispNumber("3"), evaluateString(input));
    }

}
