package sexpression;

import static error.ErrorManager.Severity.ERROR;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import static sexpression.Nil.NIL;
import static testutil.TestUtilities.assertSExpressionsMatch;
import static testutil.TestUtilities.makeList;

import java.math.BigInteger;

import org.junit.Test;

import function.UserDefinedFunction;
import sexpression.LispNumber.InvalidNumberException;

public class SExpressionTest {

    private void assertSExpressionMatchesString(String expected, SExpression sExpression) {
        assertEquals(expected, sExpression.toString());
    }

    @Test
    public void nil_ToString() {
        String input = "NIL";

        assertSExpressionMatchesString(input, NIL);
    }

    @Test
    public void number_ToString() {
        String input = "12";

        assertSExpressionMatchesString(input, new LispNumber(input));
    }

    @Test
    public void numberValue_ToString() {
        String expected = "12";

        assertSExpressionMatchesString(expected, new LispNumber("12"));
    }

    @Test
    public void string_ToString() {
        String input = "\"hi\"";

        assertSExpressionMatchesString(input, new LispString(input));
    }

    @Test
    public void symbol_ToString() {
        String input = "symbol";

        assertSExpressionMatchesString(input.toUpperCase(), new Symbol(input));
    }

    @Test
    public void simpleCons_ToString() {
        String expected = "(1)";
        Cons cons = makeList(new LispNumber("1"));

        assertSExpressionMatchesString(expected, cons);
    }

    @Test
    public void complexCons_ToString() {
        String expected = "(1 A \"string\")";
        Cons list = makeList(new LispNumber("1"), new Symbol("a"), new LispString("\"string\""));

        assertSExpressionMatchesString(expected, list);
    }

    @Test
    public void improperList_ToString() {
        String expected = "(A . B)";
        Cons list = new Cons(new Symbol("A"), new Symbol("B"));

        assertSExpressionMatchesString(expected, list);
    }

    @Test
    public void lambdaExpression_ToString() {
        String expected = "(LAMBDA)";
        LambdaExpression lambda = new LambdaExpression(makeList(new Symbol("lambda")), null);

        assertSExpressionMatchesString(expected, lambda);
    }

    @Test
    public void lambdaExpression_GetLambdaExpression() {
        String expected = "(LAMBDA)";
        LambdaExpression lambda = new LambdaExpression(makeList(new Symbol("lambda")), null);

        assertSExpressionMatchesString(expected, lambda.getLambdaExpression());
    }

    @Test
    public void lambdaExpression_GetFunction() {
        String expected = "(LAMBDA)";
        UserDefinedFunction function = new UserDefinedFunction(expected, NIL, NIL);
        LambdaExpression lambda = new LambdaExpression(makeList(new Symbol("lambda")), function);

        assertEquals(function, lambda.getFunction());
    }

    @Test
    public void firstOfNilIsNil() {
        assertEquals(NIL, NIL.getFirst());
    }

    @Test
    public void restOfNilIsNil() {
        assertEquals(NIL, NIL.getRest());
    }

    @Test
    public void afterSettingFirstOfNil_ShouldStillBeNil() {
        Cons nil = NIL;
        nil.setFirst(new LispNumber("2"));

        assertEquals(NIL, nil.getFirst());
    }

    @Test
    public void afterSettingRestOfNil_ShouldStillBeNil() {
        Cons nil = NIL;
        nil.setRest(new LispNumber("2"));

        assertEquals(NIL, nil.getRest());
    }

    @Test
    public void numberValue() {
        BigInteger value = new BigInteger("12");
        LispNumber number = new LispNumber(value.toString());

        assertEquals(value, number.getValue());
    }

    @Test(expected = InvalidNumberException.class)
    public void invalidNumberText_ThrowsException() {
        new LispNumber("a");
    }

    @Test
    public void invalidNumberException_HasCorrectAttributes() {
        try {
            new LispNumber("a");
        } catch (InvalidNumberException e) {
            String message = e.getMessage();

            assertEquals(ERROR, e.getSeverity());
            assertNotNull(message);
            assertTrue(message.length() > 0);
        }
    }

    @Test
    public void lispNumberConstants() {
        assertEquals(BigInteger.ZERO, LispNumber.ZERO.getValue());
        assertEquals(BigInteger.ONE, LispNumber.ONE.getValue());
    }

    @Test
    public void backTickExpression_ToString() {
        String expected = "`(TEST)";
        SExpression backTick = new BackquoteExpression(makeList(new Symbol("TEST")));

        assertSExpressionMatchesString(expected, backTick);
    }

    @Test
    public void commaExpression_ToString() {
        String expected = ",A";
        SExpression comma = new CommaExpression(new Symbol("A"));

        assertSExpressionMatchesString(expected, comma);
    }

    @Test
    public void atSignExpression_ToString() {
        String expected = "@A";
        SExpression atSign = new AtSignExpression(new Symbol("A"));

        assertSExpressionMatchesString(expected, atSign);
    }

    @Test
    public void complexBackTickExpression_ToString() {
        String expected = "`(LIST ,A ,@B)";
        SExpression backTick = new BackquoteExpression(makeList(new Symbol("LIST"),
                                                                new CommaExpression(new Symbol("A")),
                                                                new CommaExpression(new AtSignExpression(new Symbol("B")))));

        assertSExpressionMatchesString(expected, backTick);
    }

    @Test
    public void backTickExpression_GetExpression() {
        SExpression expression = makeList(new Symbol("TEST"));
        BackquoteExpression backTick = new BackquoteExpression(expression);

        assertSExpressionsMatch(expression, backTick.getExpression());
    }

    @Test
    public void commaExpression_GetExpression() {
        SExpression expression = new Symbol("A");
        CommaExpression comma = new CommaExpression(expression);

        assertSExpressionsMatch(expression, comma.getExpression());
    }

    @Test
    public void atSignExpression_GetExpression() {
        SExpression expression = new Symbol("A");
        AtSignExpression atSign = new AtSignExpression(expression);

        assertSExpressionsMatch(expression, atSign.getExpression());
    }

}
