package parser;

import static error.ErrorManager.Severity.ERROR;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import static testutil.TestUtilities.createIOExceptionThrowingInputStream;
import static testutil.TestUtilities.createInputStreamFromString;
import static testutil.TypeAssertions.assertAtSignExpression;
import static testutil.TypeAssertions.assertBackTickExpression;
import static testutil.TypeAssertions.assertCommaExpression;
import static testutil.TypeAssertions.assertList;
import static testutil.TypeAssertions.assertNil;
import static testutil.TypeAssertions.assertNumber;
import static testutil.TypeAssertions.assertString;
import static testutil.TypeAssertions.assertSymbol;

import java.io.InputStream;

import org.junit.Test;

import error.LispException;
import scanner.LispScanner.UnterminatedStringException;
import stream.UncheckedIOException;
import token.Eof.EofEncounteredException;
import token.RightParenthesis.StartsWithRightParenthesisException;
import token.TokenFactory.BadCharacterException;

public class LispParserTest {

    private LispParser createLispParser(String input) {
        InputStream stringInputStream = createInputStreamFromString(input);
        return new LispParser(stringInputStream, "testFile");
    }

    private LispParser createIOExceptionThrowingLispParser() {
        InputStream stringInputStream = createIOExceptionThrowingInputStream();
        return new LispParser(stringInputStream, "testFile");
    }

    @Test
    public void eofMethod_ReturnsTrueWithNoInput() {
        String input = "";
        LispParser parser = createLispParser(input);

        assertTrue(parser.isEof());
    }

    @Test
    public void eofMethod_ReturnsFalseWithSomeInput() {
        String input = "abc";
        LispParser parser = createLispParser(input);

        assertFalse(parser.isEof());
    }

    @Test
    public void eofMethod_ReturnsTrueAfterSomeInput() {
        String input = "(yyz 9 9 9)";
        LispParser parser = createLispParser(input);

        parser.getNextSExpression();
        assertTrue(parser.isEof());
    }

    @Test
    public void eofMethod_ReturnsFalseAfterMultipleExpressions() {
        String input = "()()()";
        LispParser parser = createLispParser(input);

        assertFalse(parser.isEof());
        parser.getNextSExpression();
        assertFalse(parser.isEof());
        parser.getNextSExpression();
        assertFalse(parser.isEof());
    }

    @Test
    public void eofMethod_ReturnsTrueAfterMultipleExpressions() {
        String input = "()()()";
        LispParser parser = createLispParser(input);

        assertFalse(parser.isEof());
        parser.getNextSExpression();
        assertFalse(parser.isEof());
        parser.getNextSExpression();
        assertFalse(parser.isEof());
        parser.getNextSExpression();
        assertTrue(parser.isEof());
    }

    @Test
    public void givenNil_CreatesCorrectSExpression() {
        String input = "()";
        LispParser parser = createLispParser(input);

        assertNil(parser.getNextSExpression());
    }

    @Test
    public void givenNumber_CreatesCorrectSExpression() {
        String input = "12";
        LispParser parser = createLispParser(input);

        assertNumber(parser.getNextSExpression());
    }

    @Test
    public void givenIdentifier_CreatesCorrectSExpression() {
        String input = "identifier1";
        LispParser parser = createLispParser(input);

        assertSymbol(parser.getNextSExpression());
    }

    @Test
    public void givenString_CreatesCorrectSExpression() {
        String input = "\"string\"";
        LispParser parser = createLispParser(input);

        assertString(parser.getNextSExpression());
    }

    @Test
    public void givenList_CreatesCorrectSExpression() {
        String input = "(1 2)";
        LispParser parser = createLispParser(input);

        assertList(parser.getNextSExpression());
    }

    @Test
    public void givenQuotedIdentifier_CreatesCorrectSExpression() {
        String input = "'quoted";
        LispParser parser = createLispParser(input);

        assertList(parser.getNextSExpression());
        assertTrue(parser.isEof());
    }

    @Test
    public void givenComplexList_CreatesCorrectSExpression() {
        String input = "(defun f (x) \n (print \n (list \"x is \" x) \n ) \n )";
        LispParser parser = createLispParser(input);

        assertList(parser.getNextSExpression());
        assertTrue(parser.isEof());
    }

    @Test
    public void givenMultipleComplexLists_CreatesCorrectSExpressions() {
        String input = "(defun f (x) \n (print \n (list \"x is \" x) \n ) \n )"
                       + "(defun f (x) \n (print \n (list \"x is \" x) \n ) \n )";
        LispParser parser = createLispParser(input);

        assertList(parser.getNextSExpression());
        assertList(parser.getNextSExpression());
        assertTrue(parser.isEof());
    }

    @Test
    public void givenMultipleExpressions_CreatesCorrectSExpressions() {
        String input = "(setq x 2) x \"hi\" () 29";
        LispParser parser = createLispParser(input);

        assertList(parser.getNextSExpression());
        assertSymbol(parser.getNextSExpression());
        assertString(parser.getNextSExpression());
        assertNil(parser.getNextSExpression());
        assertNumber(parser.getNextSExpression());
        assertTrue(parser.isEof());
    }

    @Test
    public void givenNil_CreatesCorrectSExpressionAfterEofCalls() {
        String input = "()";
        LispParser parser = createLispParser(input);

        parser.isEof();
        parser.isEof();

        assertNil(parser.getNextSExpression());
        assertTrue(parser.isEof());
    }

    @Test(expected = BadCharacterException.class)
    public void givenBadToken_ThrowsException() {
        String input = "[";
        LispParser parser = createLispParser(input);

        parser.getNextSExpression();
    }

    @Test
    public void givenBadToken_ExceptionHasCorrectAttributes() {
        String input = "[";
        LispParser parser = createLispParser(input);

        try {
            parser.getNextSExpression();
        } catch (BadCharacterException e) {
            String message = e.getMessage();

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

    @Test(expected = UnterminatedStringException.class)
    public void givenUnterminatedString_ThrowsException() {
        String input = "\"string";
        LispParser parser = createLispParser(input);

        parser.getNextSExpression();
    }

    @Test(expected = EofEncounteredException.class)
    public void givenUnterminatedList_ThrowsException() {
        String input = "(bad list";
        LispParser parser = createLispParser(input);

        parser.getNextSExpression();
    }

    @Test
    public void givenUnterminatedList_ExceptionHasCorrectAttributes() {
        String input = "(bad list";
        LispParser parser = createLispParser(input);

        try {
            parser.getNextSExpression();
        } catch (EofEncounteredException e) {
            String message = e.getMessage();

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

    @Test(expected = StartsWithRightParenthesisException.class)
    public void givenUnmatchedRightParenthesis_ThrowsException() {
        String input = ")";
        LispParser parser = createLispParser(input);

        parser.getNextSExpression();
    }

    @Test
    public void givenUnmatchedRightParenthesis_ExceptionHasCorrectAttributes() {
        String input = ")";
        LispParser parser = createLispParser(input);

        try {
            parser.getNextSExpression();
        } catch (StartsWithRightParenthesisException e) {
            String message = e.getMessage();

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

    @Test(expected = BadCharacterException.class)
    public void givenBadCharacter_ThrowsExceptionAfterEofCalled() {
        String input = "[";
        LispParser parser = createLispParser(input);

        try {
            parser.isEof();
        } catch (LispException e) {
            fail("Exception thrown too early");
        }

        parser.getNextSExpression();
    }

    @Test(expected = BadCharacterException.class)
    public void givenBadCharacterAfterValidToken_ThrowsExceptionAtTheCorrectTime() {
        String input = "id[";
        LispParser parser = createLispParser(input);

        try {
            parser.getNextSExpression();
            parser.isEof();
        } catch (LispException e) {
            fail("Exception thrown too early");
        }

        parser.getNextSExpression();
    }

    @Test
    public void afterException_ReturnsEofCorrectly() {
        String input = "id[";
        LispParser parser = createLispParser(input);
        parser.getNextSExpression();
        parser.isEof();

        try {
            parser.getNextSExpression();
            fail("Expected LispException");
        } catch (LispException e) {}

        assertTrue(parser.isEof());
    }

    @Test(expected = UncheckedIOException.class)
    public void handlesIOExceptionCorrectly() {
        LispParser parser = createIOExceptionThrowingLispParser();

        try {
            parser.isEof();
        } catch (LispException e) {
            fail("Exception thrown too early");
        }

        parser.getNextSExpression();
    }

    @Test
    public void givenBackTickExpression_CreatesCorrectSExpression() {
        String input = "`(list ,a ,@b)";
        LispParser parser = createLispParser(input);

        assertBackTickExpression(parser.getNextSExpression());
        assertTrue(parser.isEof());
    }

    @Test
    public void givenComma_CreatesCorrectSExpression() {
        String input = ",a";
        LispParser parser = createLispParser(input);

        assertCommaExpression(parser.getNextSExpression());
        assertTrue(parser.isEof());
    }

    @Test
    public void givenAtSignExpression_CreatesCorrectSExpression() {
        String input = "@b";
        LispParser parser = createLispParser(input);

        assertAtSignExpression(parser.getNextSExpression());
        assertTrue(parser.isEof());
    }

    @Test
    public void backTickIsNotPartOfIdentifier() {
        String input = "id`ab";
        LispParser parser = createLispParser(input);

        assertSymbol(parser.getNextSExpression());
        assertBackTickExpression(parser.getNextSExpression());
        assertTrue(parser.isEof());
    }

    @Test
    public void commaIsNotPartOfIdentifier() {
        String input = "id,ab";
        LispParser parser = createLispParser(input);

        assertSymbol(parser.getNextSExpression());
        assertCommaExpression(parser.getNextSExpression());
        assertTrue(parser.isEof());
    }

    @Test
    public void atSignIsNotPartOfIdentifier() {
        String input = "id@ab";
        LispParser parser = createLispParser(input);

        assertSymbol(parser.getNextSExpression());
        assertAtSignExpression(parser.getNextSExpression());
        assertTrue(parser.isEof());
    }

}
