package function.builtin.special;

import static sexpression.Nil.NIL;
import static testutil.TestUtilities.*;

import org.junit.*;

import function.ArgumentValidator.*;
import function.builtin.EVAL.UndefinedSymbolException;
import sexpression.*;
import table.ExecutionContext;

public class LETTester {

    private ExecutionContext executionContext;

    public LETTester() {
        this.executionContext = ExecutionContext.getInstance();
    }

    @Before
    public void setUp() {
        executionContext.clearContext();
    }

    @After
    public void tearDown() {
        executionContext.clearContext();
    }

    @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 letWithSymbolsOnly_SetsValuesToNil() {
        String input = "(let ((x) (y)) (list x y))";

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

    @Test
    public void letWithSetf_DoesNotAlterGlobalVariable() {
        String before = "(setf 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 letWithNestedSetf_DoesNotAlterGlobalVariable() {
        String before = "(setf x 22)";
        String input = "(let ((x 33)) (setf 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 = "(setf 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 = "(setf x 1)";
        String input = "(let ((y 1)) (setf 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 = "(setf x 1)";
        String input = "(let () x)";

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

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

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

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

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

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

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

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

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

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

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

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

    @Test(expected = UndefinedSymbolException.class)
    public void letEvaluatesSymbolsInParallel() {
        String input = "(let ((x 1) (y (+ x 1))) (+ x y))";

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

}