package function.builtin.special;

import static org.junit.Assert.assertTrue;
import static testutil.TestUtilities.*;

import java.io.*;

import org.junit.*;

import environment.RuntimeEnvironment;
import error.ErrorManager;
import function.ArgumentValidator.*;
import table.FunctionTable;

public class DEFINE_MACROTester {

    private ByteArrayOutputStream outputStream;
    private RuntimeEnvironment environment;

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

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

    @Before
    public void setUp() {
        outputStream = new ByteArrayOutputStream();

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

        FunctionTable.reset();
    }

    @After
    public void tearDown() {
        FunctionTable.reset();
    }

    @Test
    public void testDefineMacro() {
        String input = "(define-macro f () t)";

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

    @Test
    public void testDefineMacroWithEmptyBody() {
        String input = "(define-macro f ())";

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

    @Test
    public void testDefineMacroDoesNotEvaluatesArguments() {
        evaluateString("(define-macro f (x) (car x))");
        assertSExpressionsMatch(parseString("quote"), evaluateString("(f '(1 2 3))"));
    }

    @Test
    public void testDefineMacroAdd() {
        evaluateString("(define-macro f (x) (+ (eval x) 23))");
        assertSExpressionsMatch(parseString("27"), evaluateString("(f (+ 2 2))"));
    }

    @Test
    public void testDefineMacroSetVariable() {
        evaluateString("(define-macro f (x) (set x 23))");
        evaluateString("(f y)");
        assertSExpressionsMatch(parseString("23"), evaluateString("y"));
    }

    @Test
    public void testDefineMacroVariableCapture() {
        evaluateString("(setf x 0)");
        evaluateString("(define-macro f (x) (set x 23))");
        evaluateString("(f x)");
        assertSExpressionsMatch(parseString("0"), evaluateString("x"));
    }

    @Test
    public void testDefineMacroAvoidVariableCaptureConvention() {
        evaluateString("(setf x 0)");
        evaluateString("(define-macro f (-x-) (set -x- 23))");
        evaluateString("(f x)");
        assertSExpressionsMatch(parseString("23"), evaluateString("x"));
    }

    @Test
    public void redefineMacro_DisplaysWarning() {
        String input = "(define-macro myFunction () nil)";
        evaluateString(input);
        evaluateString(input);

        assertSomethingPrinted();
    }

    @Test
    public void redefineMacro_ActuallyRedefinesMacro() {
        evaluateString("(define-macro myMacro () nil)");
        evaluateString("(define-macro myMacro () T)");

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

    @Test(expected = DottedArgumentListException.class)
    public void testDefineMacroWithDottedLambdaList() {
        evaluateString("(funcall 'define-macro 'x (cons 'a 'b) ())");
    }

    @Test(expected = BadArgumentTypeException.class)
    public void testDefineMacroWithNonSymbolName() {
        evaluateString("(define-macro 1 () ())");
    }

    @Test(expected = BadArgumentTypeException.class)
    public void testDefineMacroWithBadLambdaList() {
        evaluateString("(define-macro x a ())");
    }

    @Test(expected = TooFewArgumentsException.class)
    public void testDefineMacroWithTooFewArguments() {
        evaluateString("(define-macro x)");
    }

}