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 DEFMACROTest extends SymbolAndFunctionCleaner {

    private ByteArrayOutputStream outputStream;
    private RuntimeEnvironment environment;

    public DEFMACROTest() {
        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 defmacro() {
        String input = "(defmacro m () t)";

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

    @Test
    public void defmacroWithEmptyBody() {
        String input = "(defmacro m ())";

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

    @Test
    public void defmacroDoesNotEvaluateArguments() {
        evaluateString("(setq x 'grains)");
        evaluateString("(defmacro m (x))");
        evaluateString("(m (setq x 'sprouts))");

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

    @Test
    public void defmacroAdd() {
        evaluateString("(defmacro m (x) (+ (eval x) 23))");
        assertSExpressionsMatch(parseString("27"), evaluateString("(m (+ 2 2))"));
    }

    @Test
    public void defmacroSetVariable() {
        evaluateString("(defmacro m (x) (set x 23))");
        evaluateString("(m y)");
        assertSExpressionsMatch(parseString("23"), evaluateString("y"));
    }

    @Test
    public void defmacroVariableCapture() {
        evaluateString("(setq x 0)");
        evaluateString("(defmacro m (x) (set x 23))");
        evaluateString("(m x)");
        assertSExpressionsMatch(parseString("0"), evaluateString("x"));
    }

    @Test
    public void redefineMacro_DisplaysWarning() {
        String input = "(defmacro myMacro () nil)";
        evaluateString(input);
        evaluateString(input);

        assertSomethingPrinted();
    }

    @Test
    public void redefineMacro_ActuallyRedefinesSpecialFunction() {
        evaluateString("(defmacro myMacro () nil)");
        evaluateString("(defmacro myMacro () T)");

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

    @Test(expected = DottedArgumentListException.class)
    public void defmacroWithDottedLambdaList() {
        evaluateString("(funcall 'defmacro 'm (cons 'a 'b) ())");
    }

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

    @Test(expected = BadArgumentTypeException.class)
    public void defmacroWithBadLambdaList() {
        evaluateString("(defmacro m a ())");
    }

    @Test(expected = TooFewArgumentsException.class)
    public void defmacroWithTooFewArguments() {
        evaluateString("(defmacro m)");
    }

    @Test(expected = TooFewArgumentsException.class)
    public void defmacroAndCallWithTooFewArguments() {
        evaluateString("(defmacro m (a b))");
        evaluateString("(m a)");
    }

    @Test(expected = TooManyArgumentsException.class)
    public void defmacroAndCallWithTooManyArguments() {
        evaluateString("(defmacro m (a b))");
        evaluateString("(m a b c)");
    }

    @Test
    public void defmacroWithKeywordRestParameter() {
        evaluateString("(defmacro m (&rest x) (car x))");
        assertSExpressionsMatch(parseString("1"), evaluateString("(m 1 2 3 4 5)"));
    }

    @Test
    public void defmacroWithNormalAndKeywordRestParameter() {
        evaluateString("(defmacro m (a &rest b) (list 'cons a (list 'quote b)))");
        assertSExpressionsMatch(parseString("(1 2 3 4 5)"), evaluateString("(m 1 2 3 4 5)"));
    }

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

    @Test
    public void defmacroWithKeywordRest_CallWithNoArguments() {
        evaluateString("(defmacro m (&rest a) (car a))");
        assertSExpressionsMatch(parseString("nil"), evaluateString("(m)"));
    }

    @Test(expected = TooFewArgumentsException.class)
    public void defmacroWithNormalAndKeywordRest_CallWithNoArguments() {
        evaluateString("(defmacro m (a &rest b) a)");
        evaluateString("(m)");
    }

    @Test
    public void macroIsEvaluatedAfterExpansion() {
        evaluateString("(setq x 'grains)");
        evaluateString("(defmacro m (x) x)");
        evaluateString("(m (setq x 'sprouts))");

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

}
