package function;

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 sexpression.Symbol.T;

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

import function.ArgumentValidator.BadArgumentTypeException;
import function.ArgumentValidator.DottedArgumentListException;
import function.ArgumentValidator.TooFewArgumentsException;
import function.ArgumentValidator.TooManyArgumentsException;
import sexpression.Cons;
import sexpression.LispString;
import sexpression.Nil;
import sexpression.SExpression;
import sexpression.Symbol;

public class ArgumentValidatorTest {

    private ArgumentValidator validator;

    private Cons makeArgumentListOfSize(int size) {
        Cons argumentList = NIL;

        for (int i = 0; i < size; i++)
            argumentList = new Cons(NIL, argumentList);

        return argumentList;
    }

    @Before
    public void setUp() {
        validator = new ArgumentValidator("TEST");
    }

    @Test
    public void noConstraints_DoesNotThrowExceptionWithNoArguments() {
        validator.validate(makeArgumentListOfSize(0));
    }

    @Test
    public void noConstraints_DoesNotThrowExceptionWithOneArgument() {
        validator.validate(makeArgumentListOfSize(1));
    }

    @Test
    public void noConstraints_DoesNotThrowExceptionWithManyArguments() {
        validator.validate(makeArgumentListOfSize(20));
    }

    @Test(expected = TooFewArgumentsException.class)
    public void tooFewArgumentsWithMinimumSet_ThrowsException() {
        validator.setMinimumNumberOfArguments(1);
        validator.validate(makeArgumentListOfSize(0));
    }

    @Test(expected = TooManyArgumentsException.class)
    public void tooManyArgumentsWithMaximumSet_ThrowsException() {
        validator.setMaximumNumberOfArguments(1);
        validator.validate(makeArgumentListOfSize(2));
    }

    @Test
    public void exactNumberOfArguments_DoesNotThrowException() {
        validator.setExactNumberOfArguments(5);
        validator.validate(makeArgumentListOfSize(5));
    }

    @Test(expected = TooFewArgumentsException.class)
    public void tooFewArgumentsWithExactSet_ThrowsException() {
        validator.setExactNumberOfArguments(3);
        validator.validate(makeArgumentListOfSize(2));
    }

    @Test(expected = TooManyArgumentsException.class)
    public void tooManyArgumentsWithExactSet_ThrowsException() {
        validator.setExactNumberOfArguments(3);
        validator.validate(makeArgumentListOfSize(4));
    }

    @Test
    public void tooManyArgumentsException_HasCorrectAttributes() {
        TooManyArgumentsException e = new TooManyArgumentsException("TEST", NIL);

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

    @Test
    public void tooFewArgumentsException_HasCorrectAttributes() {
        TooFewArgumentsException e = new TooFewArgumentsException("TEST", NIL);

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

    @Test
    public void badArgumentTypeException_HasCorrectAttributes() {
        BadArgumentTypeException e = new BadArgumentTypeException("TEST", NIL, SExpression.class);

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

    @Test
    public void correctArgumentType_DoesNotThrowException() {
        validator.setEveryArgumentExpectedType(Nil.class);
        validator.validate(makeArgumentListOfSize(1));
    }

    @Test(expected = BadArgumentTypeException.class)
    public void badArgumentType_ThrowsException() {
        validator.setEveryArgumentExpectedType(LispString.class);
        validator.validate(makeArgumentListOfSize(1));
    }

    @Test
    public void correctFirstAndRestArgumentTypes_DoesNotThrowException() {
        Cons argumentList = new Cons(T, new Cons(NIL, NIL));

        validator.setFirstArgumentExpectedType(Symbol.class);
        validator.setTrailingArgumentExpectedType(Cons.class);
        validator.validate(argumentList);
    }

    @Test(expected = BadArgumentTypeException.class)
    public void badFirstArgumentType_ThrowsException() {
        Cons argumentList = new Cons(T, new Cons(NIL, NIL));

        validator.setFirstArgumentExpectedType(Cons.class);
        validator.setTrailingArgumentExpectedType(Cons.class);
        validator.validate(argumentList);
    }

    @Test(expected = BadArgumentTypeException.class)
    public void badTrailingArgumentType_ThrowsException() {
        Cons argumentList = new Cons(T, new Cons(NIL, NIL));

        validator.setFirstArgumentExpectedType(Symbol.class);
        validator.setTrailingArgumentExpectedType(Symbol.class);
        validator.validate(argumentList);
    }

    @Test
    public void expectedTypeWithNoDisplayName_DoesNotCauseNPE() {
        Cons argumentList = new Cons(T, new Cons(NIL, NIL));
        SExpression withoutDisplayName = new SExpression() {};

        validator.setEveryArgumentExpectedType(withoutDisplayName.getClass());

        try {
            validator.validate(argumentList);
        } catch (BadArgumentTypeException e) {
            assertNotNull(e.getMessage());
            assertTrue(e.getMessage().length() > 0);
        }
    }

    @Test(expected = DottedArgumentListException.class)
    public void givenDottedArgumentList_ThrowsException() {
        Cons argumentList = new Cons(T, T);

        validator.validate(argumentList);
    }

    @Test(expected = DottedArgumentListException.class)
    public void givenLargeDottedArgumentList_ThrowsException() {
        Cons argumentList = new Cons(T, new Cons(T, T));

        validator.validate(argumentList);
    }

    @Test
    public void dottedArgumentListException_HasCorrectAttributes() {
        DottedArgumentListException e = new DottedArgumentListException("TEST", NIL);

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

    @Test
    public void excludedFirstArgumentType_DoesNotAffectTrailingArguments() {
        validator.setFirstArgumentExcludedType(Nil.class);
        validator.validate(new Cons(T, new Cons(NIL, NIL)));
    }

    @Test
    public void excludedTrailingArgumentType_DoesNotAffectFirstArgument() {
        validator.setTrailingArgumentExcludedType(Nil.class);
        validator.validate(new Cons(NIL, new Cons(T, NIL)));
    }

    @Test(expected = BadArgumentTypeException.class)
    public void excludedFirstArgumentType_ThrowsException() {
        validator.setFirstArgumentExcludedType(Nil.class);
        validator.validate(new Cons(NIL, new Cons(T, NIL)));
    }

    @Test(expected = BadArgumentTypeException.class)
    public void excludedTrailingArgumentType_ThrowsException() {
        validator.setTrailingArgumentExcludedType(Nil.class);
        validator.validate(new Cons(T, new Cons(NIL, NIL)));
    }

    @Test(expected = BadArgumentTypeException.class)
    public void excludedArgumentType_ThrowsExceptionOnFirstArgument() {
        validator.setEveryArgumentExcludedType(Nil.class);
        validator.validate(new Cons(NIL, new Cons(T, NIL)));
    }

    @Test(expected = BadArgumentTypeException.class)
    public void excludedArgumentType_ThrowsExceptionOnTrailingArgument() {
        validator.setEveryArgumentExcludedType(Nil.class);
        validator.validate(new Cons(T, new Cons(NIL, NIL)));
    }

}
