package table;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static sexpression.Nil.NIL;
import static sexpression.Symbol.T;
import static table.FunctionTable.defineFunction;
import static table.FunctionTable.isAlreadyDefined;
import static table.FunctionTable.lookupFunction;
import static table.FunctionTable.resetFunctionTable;

import java.util.HashSet;
import java.util.Set;

import org.junit.After;
import org.junit.Before;
import org.junit.Test;

import error.ErrorManager;
import function.FunctionNames;
import function.LispFunction;
import sexpression.Cons;
import sexpression.SExpression;
import table.FunctionTable.LispFunctionInstantiationException;

public class FunctionTableTest {

    @FunctionNames({ "GOOD" })
    public static class GoodFunction extends LispFunction {

        public GoodFunction(String name) {}

        @Override
        public SExpression call(Cons argList) {
            return NIL;
        }
    }

    @FunctionNames({ "BAD" })
    public static class BadFunction extends LispFunction {

        @Override
        public SExpression call(Cons argList) {
            return NIL;
        }
    }

    public static class UglyFunction extends LispFunction {

        @Override
        public SExpression call(Cons argList) {
            return NIL;
        }
    }

    private LispFunction createLispFunction() {
        return new LispFunction() {

            @Override
            public SExpression call(Cons argList) {
                return T;
            }
        };
    }

    @Before
    public void setUp() {
        resetFunctionTable();
    }

    @After
    public void tearDown() {
        resetFunctionTable();
    }

    @Test
    public void builtInFunctionIsDefined() {
        assertTrue(isAlreadyDefined("CONS"));
    }

    @Test
    public void undefinedFunctionIsNotDefined() {
        assertFalse(isAlreadyDefined("undefined"));
    }

    @Test
    public void lookupBuiltInFunction_ReturnsFunction() {
        assertNotNull(lookupFunction("CONS"));
    }

    @Test
    public void lookupUndefinedFunction_ReturnsNull() {
        assertNull(lookupFunction("undefined"));
    }

    @Test
    public void defineFunctionWorks() {
        String functionName = "testFunction";
        LispFunction testFunction = createLispFunction();

        assertNull(lookupFunction(functionName));
        assertFalse(isAlreadyDefined(functionName));

        defineFunction(functionName, testFunction);

        assertTrue(isAlreadyDefined(functionName));
        assertEquals(testFunction, lookupFunction(functionName));
    }

    @Test
    public void resetFunctionTableWorks() {
        String functionName = "testFunction";
        LispFunction testFunction = createLispFunction();
        defineFunction(functionName, testFunction);

        resetFunctionTable();

        assertFalse(isAlreadyDefined(functionName));
        assertNull(lookupFunction(functionName));
    }

    @Test
    public void resetWithCustomBuitIns() {
        Set<Class<? extends LispFunction>> goodBuiltIns = new HashSet<>();
        goodBuiltIns.add(GoodFunction.class);

        resetFunctionTable(goodBuiltIns);

        assertTrue(isAlreadyDefined("GOOD"));
        assertNotNull(lookupFunction("GOOD"));
    }

    @Test(expected = LispFunctionInstantiationException.class)
    public void unableToInitializeBuiltIn() {
        Set<Class<? extends LispFunction>> badBuiltIns = new HashSet<>();
        badBuiltIns.add(BadFunction.class);

        resetFunctionTable(badBuiltIns);
    }

    @Test
    public void lispFunctionInstantiationException_HasCorrectAttributes() {
        LispFunctionInstantiationException e = new LispFunctionInstantiationException("Bad");

        assertNotNull(e.getMessage());
        assertTrue(e.getMessage().length() > 0);
        assertEquals(ErrorManager.Severity.CRITICAL, e.getSeverity());
    }

    @Test
    public void namelessBuiltIn_DoesNotCauseNPE() {
        Set<Class<? extends LispFunction>> namelessBuiltins = new HashSet<>();
        namelessBuiltins.add(UglyFunction.class);

        resetFunctionTable(namelessBuiltins);

        assertFalse(isAlreadyDefined("UGLY"));
        assertNull(lookupFunction("UGLY"));
    }

}
