package function.builtin.special;

import function.ArgumentValidator.BadArgumentTypeException;
import function.ArgumentValidator.DottedArgumentListException;
import function.ArgumentValidator.TooFewArgumentsException;
import function.ArgumentValidator.TooManyArgumentsException;
import function.builtin.EVAL.UndefinedFunctionException;
import org.junit.Test;
import sexpression.Cons;
import sexpression.LispNumber;
import sexpression.Symbol;
import testutil.SymbolAndFunctionCleaner;

import static function.builtin.special.LAMBDA.Lambda;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static sexpression.LispNumber.ONE;
import static sexpression.Nil.NIL;
import static sexpression.Symbol.T;
import static testutil.TestUtilities.assertSExpressionsMatch;
import static testutil.TestUtilities.evaluateString;
import static testutil.TestUtilities.parseString;

public class LAMBDATest extends SymbolAndFunctionCleaner {

    @Test
    public void lambda() {
        String input = "(lambda (x) x)";

        assertSExpressionsMatch(parseString("(LAMBDA (X) X)"), evaluateString(input));
    }

    @Test
    public void lambdaSymbol() {
        String input = "(λ (x) x)";

        assertSExpressionsMatch(parseString("(LAMBDA (X) X)"), evaluateString(input));
    }

    @Test
    public void lambdaWithNoBody() {
        String input = "(lambda ())";

        assertSExpressionsMatch(parseString("(LAMBDA ())"), evaluateString(input));
    }

    @Test
    public void lambdaExpressionIsLambdaExpression() {
        Cons lambdaExpression = new Cons(new Symbol("LAMBDA"), new Cons(NIL, new Cons(NIL, NIL)));

        assertTrue(Lambda.isLambdaExpression(lambdaExpression));
    }

    @Test
    public void somethingElseIsNotLambdaExpression() {
        assertFalse(Lambda.isLambdaExpression(T));
    }

    @Test
    public void createLambdaExpression() {
        Cons lambdaExpression = new Cons(new Symbol("LAMBDA"), new Cons(NIL, new Cons(NIL, NIL)));

        assertSExpressionsMatch(parseString("(:LAMBDA () ())"),
                                Lambda.createFunction(lambdaExpression).getLambdaExpression());
    }

    @Test(expected = DottedArgumentListException.class)
    public void lambdaWithDottedArgumentList() {
        String input = "(apply 'lambda (cons '(x) 1))";

        evaluateString(input);
    }

    @Test(expected = DottedArgumentListException.class)
    public void lambdaWithDottedLambdaList() {
        String input = "(funcall 'lambda (cons 'a 'b) ())";

        evaluateString(input);
    }

    @Test(expected = DottedArgumentListException.class)
    public void createFunctionWithDottedArgumentList() {
        Cons lambdaExpression = new Cons(new Symbol("LAMBDA"), new Cons(NIL, ONE));

        Lambda.createFunction(lambdaExpression);
    }

    @Test(expected = BadArgumentTypeException.class)
    public void createFunctionWithNonList() {
        Cons lambdaExpression = new Cons(new Symbol("LAMBDA"), ONE);

        Lambda.createFunction(lambdaExpression);
    }

    @Test(expected = BadArgumentTypeException.class)
    public void lambdaWithNonSymbolParameter() {
        evaluateString("(lambda (1) ())");
    }

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

    @Test
    public void anonymousLambdaCall() {
        String input = "((lambda (x) x) 203)";

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

    @Test
    public void anonymousLambdaCallWithMultipleArguments() {
        String input = "((lambda (x y) (+ x y)) 203 2)";

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

    @Test
    public void anonymousLambdaCallWithSymbol() {
        String input = "((λ (x) (+ x 1)) 3)";

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

    @Test(expected = TooFewArgumentsException.class)
    public void anonymousLambdaCallWithTooFewArguments() {
        evaluateString("((lambda (x) x))");
    }

    @Test(expected = TooManyArgumentsException.class)
    public void anonymousLambdaCallWithTooManyArguments() {
        evaluateString("((lambda (x y) x) 1 2 3)");
    }

    @Test(expected = UndefinedFunctionException.class)
    public void badAnonymousFunctionCall() {
        evaluateString("((bad-lambda (x y) x) 1 2 3)");
    }

    @Test
    public void lexicalClosure() {
        evaluateString("(setq increment-count (let ((counter 0)) (lambda () (setq counter (+ 1 counter)))))");

        assertSExpressionsMatch(parseString("1"), evaluateString("(funcall increment-count)"));
        assertSExpressionsMatch(parseString("2"), evaluateString("(funcall increment-count)"));
        assertSExpressionsMatch(parseString("3"), evaluateString("(funcall increment-count)"));
        assertSExpressionsMatch(parseString("4"), evaluateString("(funcall increment-count)"));
    }
}