package function.builtin;

import static sexpression.Nil.NIL;
import static sexpression.Symbol.T;
import static testutil.TestUtilities.assertIsErrorWithMessage;
import static testutil.TestUtilities.assertSExpressionsMatch;
import static testutil.TestUtilities.makeList;

import org.junit.Test;

import function.ArgumentValidator.DottedArgumentListException;
import function.builtin.BackquoteEvaluator.AtSignNotInCommaException;
import function.builtin.BackquoteEvaluator.AtSignNotListException;
import function.builtin.BackquoteEvaluator.NestedAtSignException;
import function.builtin.BackquoteEvaluator.NestedCommaException;
import sexpression.AtSignExpression;
import sexpression.BackquoteExpression;
import sexpression.CommaExpression;
import sexpression.Cons;
import sexpression.LispNumber;
import sexpression.LispString;
import sexpression.SExpression;
import sexpression.Symbol;

public class BackquoteEvaluatorTest {

    private BackquoteEvaluator createBackquoteEvaluator(SExpression expression) {
        return new BackquoteEvaluator(new BackquoteExpression(expression));
    }

    @Test
    public void evaluateNil() {
        BackquoteEvaluator evaluator = createBackquoteEvaluator(NIL);

        assertSExpressionsMatch(NIL, evaluator.evaluate());
    }

    @Test
    public void evaluateNumber() {
        SExpression input = new LispNumber("99");
        BackquoteEvaluator evaluator = createBackquoteEvaluator(input);

        assertSExpressionsMatch(input, evaluator.evaluate());
    }

    @Test
    public void evaluateList() {
        SExpression input = makeList(new LispNumber("1"), new LispNumber("99"));
        BackquoteEvaluator evaluator = createBackquoteEvaluator(input);

        assertSExpressionsMatch(input, evaluator.evaluate());
    }

    @Test
    public void evaluateComma() {
        SExpression input = new CommaExpression(makeList(new Symbol("+"), new LispNumber("1"), new LispNumber("9")));
        BackquoteEvaluator evaluator = createBackquoteEvaluator(input);

        assertSExpressionsMatch(new LispNumber("10"), evaluator.evaluate());
    }

    @Test
    public void evaluateListWithComma() {
        SExpression input = makeList(new CommaExpression(makeList(new Symbol("+"), new LispNumber("1"),
                                                                  new LispNumber("9"))));
        BackquoteEvaluator evaluator = createBackquoteEvaluator(input);

        assertSExpressionsMatch(makeList(new LispNumber("10")), evaluator.evaluate());
    }

    @Test(expected = NestedCommaException.class)
    public void evaluateListWithNestedComma() {
        SExpression input = makeList(new CommaExpression(new CommaExpression(new Symbol("+"))));
        BackquoteEvaluator evaluator = createBackquoteEvaluator(input);

        evaluator.evaluate();
    }

    @Test(expected = AtSignNotInCommaException.class)
    public void evaluateListWithNoCommaPrecedingAtSign() {
        SExpression input = makeList(new AtSignExpression(makeList(new Symbol("+"))));
        BackquoteEvaluator evaluator = createBackquoteEvaluator(input);

        evaluator.evaluate();
    }

    @Test(expected = AtSignNotInCommaException.class)
    public void evaluateAtSign() {
        SExpression input = new AtSignExpression(makeList(new Symbol("+")));
        BackquoteEvaluator evaluator = createBackquoteEvaluator(input);

        evaluator.evaluate();
    }

    @Test(expected = NestedAtSignException.class)
    public void evaluateListWithNestedAtSigns() {
        SExpression input = makeList(new CommaExpression(new AtSignExpression(new AtSignExpression(makeList(new Symbol("+"))))));
        BackquoteEvaluator evaluator = createBackquoteEvaluator(input);

        evaluator.evaluate();
    }

    @Test(expected = NestedCommaException.class)
    public void evaluateListWithCommaAfterAtSign() {
        SExpression input = makeList(new CommaExpression(new AtSignExpression(new CommaExpression(makeList(new Symbol("+"))))));
        BackquoteEvaluator evaluator = createBackquoteEvaluator(input);

        evaluator.evaluate();
    }

    @Test
    public void evaluateListWithAtSign() {
        SExpression input = makeList(new CommaExpression(new AtSignExpression(makeList(new Symbol("LIST"),
                                                                                       new LispNumber("1"),
                                                                                       new LispNumber("9")))));
        SExpression expected = makeList(new LispNumber("1"), new LispNumber("9"));
        BackquoteEvaluator evaluator = createBackquoteEvaluator(input);

        assertSExpressionsMatch(expected, evaluator.evaluate());
    }

    @Test(expected = AtSignNotListException.class)
    public void atSignDoesNotEvaluateToList() {
        SExpression input = makeList(new CommaExpression(new AtSignExpression(new LispNumber("1"))));
        BackquoteEvaluator evaluator = createBackquoteEvaluator(input);

        evaluator.evaluate();
    }

    @Test
    public void evaluateListWithCommasAndAtSign() {
        Cons list1 = makeList(new Symbol("LIST"), new LispNumber("1"), new LispNumber("9"));
        Cons list2 = makeList(new Symbol("+"), new LispNumber("20"), new LispNumber("5"));
        Cons list3 = makeList(new Symbol("LIST"), new LispNumber("7"), new LispNumber("6"));

        SExpression input = makeList(new LispNumber("78"), new CommaExpression(new AtSignExpression(list1)),
                                     new CommaExpression(list2), new CommaExpression(list3), new LispString("\"sky\""));

        SExpression expected = makeList(new LispNumber("78"), new LispNumber("1"), new LispNumber("9"),
                                        new LispNumber("25"), makeList(new LispNumber("7"), new LispNumber("6")),
                                        new LispString("\"sky\""));

        BackquoteEvaluator evaluator = createBackquoteEvaluator(input);

        assertSExpressionsMatch(expected, evaluator.evaluate());
    }

    @Test(expected = DottedArgumentListException.class)
    public void evaluateDottedList() {
        BackquoteEvaluator evaluator = createBackquoteEvaluator(new Cons(T, T));

        evaluator.evaluate();
    }

    @Test(expected = DottedArgumentListException.class)
    public void atSignWithDottedList() {
        SExpression input = makeList(new CommaExpression(new AtSignExpression(makeList(new Symbol("CONS"), T, T))));
        BackquoteEvaluator evaluator = createBackquoteEvaluator(input);

        evaluator.evaluate();
    }

    @Test
    public void backquoteExceptionsHaveCorrectAttributes() {
        assertIsErrorWithMessage(new NestedCommaException());
        assertIsErrorWithMessage(new NestedAtSignException());
        assertIsErrorWithMessage(new AtSignNotInCommaException());
        assertIsErrorWithMessage(new AtSignNotListException());
    }

}
