package function.builtin.special;

import static org.junit.Assert.assertTrue;
import static testutil.TestUtilities.assertSExpressionsMatch;
import static testutil.TestUtilities.evaluateString;
import static testutil.TestUtilities.parseString;

import java.io.ByteArrayOutputStream;
import java.io.PrintStream;

import org.junit.Test;

import environment.RuntimeEnvironment;
import error.ErrorManager;
import function.ArgumentValidator.BadArgumentTypeException;
import function.ArgumentValidator.DottedArgumentListException;
import function.ArgumentValidator.TooFewArgumentsException;
import function.ArgumentValidator.TooManyArgumentsException;
import function.UserDefinedFunction.IllegalKeywordRestPositionException;
import testutil.SymbolAndFunctionCleaner;

public class DEFUNTest extends SymbolAndFunctionCleaner {

    private ByteArrayOutputStream outputStream;
    private RuntimeEnvironment environment;

    public DEFUNTest() {
        this.environment = RuntimeEnvironment.getInstance();
    }

    private void assertSomethingPrinted() {
        assertTrue(outputStream.toByteArray().length > 0);
    }

    @Override
    public void additionalSetUp() {
        outputStream = new ByteArrayOutputStream();

        environment.reset();
        environment.setOutput(new PrintStream(outputStream));
        environment.setErrorManager(new ErrorManager());
        environment.setWarningOutputDecorator(s -> s);
    }

    @Override
    public void additionalTearDown() {
        environment.reset();
    }

    @Test
    public void defun() {
        String input = "(defun f () t)";

        assertSExpressionsMatch(parseString("f"), evaluateString(input));
        assertSExpressionsMatch(parseString("t"), evaluateString("(f)"));
    }

    @Test
    public void defunWithEmptyBody() {
        String input = "(defun f ())";

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

    @Test
    public void defunEvaluatesArguments() {
        evaluateString("(defun f (x) (car x))");
        assertSExpressionsMatch(parseString("1"), evaluateString("(f '(1 2 3))"));
    }

    @Test
    public void defunRecursiveFunction() {
        evaluateString("(defun fact (x) (if (< x 2) 1 (* x (fact (- x 1)))))");
        assertSExpressionsMatch(parseString("120"), evaluateString("(fact 5)"));
    }

    @Test
    public void defunTailRecursiveFunction() {
        evaluateString("(defun fact-tail (x acc) (if (< x 2) acc (fact-tail (- x 1) (* x acc))))");
        assertSExpressionsMatch(parseString("120"), evaluateString("(fact-tail 5 1)"));
    }

    @Test
    public void defunSimpleClass() {
        evaluateString("(defun counter-class () (let ((counter 0)) (lambda () (setq counter (+ 1 counter)))))");
        evaluateString("(setq my-counter (counter-class))");

        assertSExpressionsMatch(parseString("1"), evaluateString("(funcall my-counter)"));
        assertSExpressionsMatch(parseString("2"), evaluateString("(funcall my-counter)"));
        assertSExpressionsMatch(parseString("3"), evaluateString("(funcall my-counter)"));
        assertSExpressionsMatch(parseString("4"), evaluateString("(funcall my-counter)"));
    }

    @Test
    public void redefineFunction_DisplaysWarning() {
        String input = "(defun myFunction () nil)";
        evaluateString(input);
        evaluateString(input);

        assertSomethingPrinted();
    }

    @Test
    public void redefineFunction_ActuallyRedefinesFunction() {
        evaluateString("(defun myFunction () nil)");
        evaluateString("(defun myFunction () T)");

        assertSomethingPrinted();
        assertSExpressionsMatch(parseString("t"), evaluateString("(myFunction)"));
    }

    @Test(expected = DottedArgumentListException.class)
    public void defunWithDottedLambdaList() {
        evaluateString("(funcall 'defun 'f (cons 'a 'b) ())");
    }

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

    @Test(expected = BadArgumentTypeException.class)
    public void defunWithBadLambdaList() {
        evaluateString("(defun f a ())");
    }

    @Test(expected = BadArgumentTypeException.class)
    public void defunWithNonSymbolInLambdaList() {
        evaluateString("(defun f (1) ())");
    }

    @Test(expected = TooFewArgumentsException.class)
    public void defunWithTooFewArguments() {
        evaluateString("(defun f)");
    }

    @Test(expected = TooFewArgumentsException.class)
    public void defunFunctionAndCallWithTooFewArguments() {
        evaluateString("(defun f (a b))");
        evaluateString("(f 'a)");
    }

    @Test(expected = TooManyArgumentsException.class)
    public void defunFunctionAndCallWithTooManyArguments() {
        evaluateString("(defun f (a b))");
        evaluateString("(f 'a 'b 'c)");
    }

    @Test
    public void defunWithKeywordRestParameter() {
        evaluateString("(defun f (&rest x) (car x))");
        assertSExpressionsMatch(parseString("1"), evaluateString("(f 1 2 3 4 5)"));
    }

    @Test
    public void defunWithNormalAndKeywordRestParameter() {
        evaluateString("(defun f (a &rest b) (cons a b))");
        assertSExpressionsMatch(parseString("(1 2 3 4 5)"), evaluateString("(f 1 2 3 4 5)"));
    }

    @Test(expected = IllegalKeywordRestPositionException.class)
    public void defunWithParametersFollowingKeywordRest() {
        evaluateString("(defun f (a &rest b c) (cons a b))");
        evaluateString("(f 1 2 3)");
    }

    @Test
    public void defunWithKeywordRest_CallWithNoArguments() {
        evaluateString("(defun f (&rest a) (car a))");
        assertSExpressionsMatch(parseString("nil"), evaluateString("(f)"));
    }

    @Test(expected = TooFewArgumentsException.class)
    public void defunWithNormalAndKeywordRest_CallWithNoArguments() {
        evaluateString("(defun f (a &rest b) a)");
        evaluateString("(f)");
    }

    @Test
    public void resultOfFunctionIsNotEvaluated() {
        evaluateString("(setq x 'grains)");
        evaluateString("(define-special f (x) 'x)");
        evaluateString("(f (setq x 'sprouts))");

        assertSExpressionsMatch(parseString("grains"), evaluateString("x"));
    }

}
