package function.builtin.special;

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.getInstance(), 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 testLetWithNonList() {
        evaluateString("(let a)");
    }

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

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

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

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

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

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

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

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

}
