package scanner;

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 testutil.TestUtilities.createInputStreamFromString;

import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;

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

import scanner.LispScanner.UnterminatedStringException;
import token.AtSign;
import token.Backquote;
import token.Comma;
import token.Eof;
import token.Identifier;
import token.LeftParenthesis;
import token.Number;
import token.QuoteMark;
import token.QuotedString;
import token.RightParenthesis;
import token.Token;
import token.TokenFactory.BadCharacterException;

public class LispScannerTypeTest {

    private List<Class<? extends Token>> expectedTypes;

    private void assertTokenTypesMatch(String input) {
        InputStream stringInputStream = createInputStreamFromString(input);
        LispScanner lispScanner = new LispScanner(stringInputStream, "testFile");

        for (Class<? extends Token> type : expectedTypes)
            assertTrue(type.isInstance(lispScanner.getNextToken()));

        assertTrue(lispScanner.getNextToken() instanceof Eof);
    }

    @Before
    public void setUp() {
        expectedTypes = new ArrayList<>();
    }

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

        assertTokenTypesMatch(input);
    }

    @Test(expected = BadCharacterException.class)
    public void givenBadCharacter_ThrowsException() {
        String input = "[";

        assertTokenTypesMatch(input);
    }

    @Test
    public void givenBadCharacter_ExceptionContainsCorrectSeverity() {
        String input = "abc\ndef[";
        expectedTypes.add(Identifier.class);
        expectedTypes.add(Identifier.class);

        try {
            assertTokenTypesMatch(input);
        } catch (BadCharacterException e) {
            assertEquals(ERROR, e.getSeverity());
        }
    }

    @Test
    public void givenBadCharacter_ExceptionContainsMessage() {
        String input = "abc\ndef[";
        expectedTypes.add(Identifier.class);
        expectedTypes.add(Identifier.class);

        try {
            assertTokenTypesMatch(input);
        } catch (BadCharacterException e) {
            String message = e.getMessage();
            assertNotNull(message);
            assertTrue(message.length() > 0);
        }
    }

    @Test
    public void givenNil_ReturnsCorrectTypes() {
        String input = "()";
        expectedTypes.add(LeftParenthesis.class);
        expectedTypes.add(RightParenthesis.class);

        assertTokenTypesMatch(input);
    }

    @Test
    public void givenListOfNumbers_ReturnsCorrectTypes() {
        String input = "(1 2)";
        expectedTypes.add(LeftParenthesis.class);
        expectedTypes.add(Number.class);
        expectedTypes.add(Number.class);
        expectedTypes.add(RightParenthesis.class);

        assertTokenTypesMatch(input);
    }

    @Test
    public void givenString_ReturnsCorrectTypes() {
        String input = "\"string\"";
        expectedTypes.add(QuotedString.class);

        assertTokenTypesMatch(input);
    }

    @Test
    public void givenStringWithEscapedDoubleQuote_ReturnsCorrectTypes() {
        String input = "\"string \n hi \\\" bye\"";
        expectedTypes.add(QuotedString.class);

        assertTokenTypesMatch(input);
    }

    @Test
    public void givenStringWithEscapedDoubleQuoteAndComment_ReturnsCorrectTypes() {
        String input = "\"string \n hi \\\" ; bye\"";
        expectedTypes.add(QuotedString.class);

        assertTokenTypesMatch(input);
    }

    @Test(expected = UnterminatedStringException.class)
    public void givenUnterminatedString_ThrowsException() {
        String input = "\"oh no!";
        expectedTypes.add(QuotedString.class);

        assertTokenTypesMatch(input);
    }

    @Test()
    public void givenUnterminatedString_ExceptionHasCorrectSeverity() {
        String input = "\"oh no!";
        expectedTypes.add(QuotedString.class);

        try {
            assertTokenTypesMatch(input);
        } catch (LispScanner.UnterminatedStringException e) {
            assertEquals(ERROR, e.getSeverity());
        }
    }

    @Test()
    public void givenUnterminatedString_ExceptionContainsMessage() {
        String input = "\"oh no!";
        expectedTypes.add(QuotedString.class);

        try {
            assertTokenTypesMatch(input);
        } catch (LispScanner.UnterminatedStringException e) {
            String message = e.getMessage();
            assertNotNull(message);
            assertTrue(message.length() > 0);
        }
    }

    @Test
    public void givenIdentifier_ReturnsCorrectTypes() {
        String input = "abcdefgHIJKLMNOP1234";
        expectedTypes.add(Identifier.class);

        assertTokenTypesMatch(input);
    }

    @Test
    public void givenPrefixedIdentifiers_ReturnsCorrectTypes() {
        String input = "-a +b";
        expectedTypes.add(Identifier.class);
        expectedTypes.add(Identifier.class);

        assertTokenTypesMatch(input);
    }

    @Test
    public void givenSingleDigitNumber_ReturnsCorrectTypes() {
        String input = "1";
        expectedTypes.add(Number.class);

        assertTokenTypesMatch(input);
    }

    @Test
    public void givenPrefixedNegativeNumber_ReturnsCorrectTypes() {
        String input = "-1";
        expectedTypes.add(Number.class);

        assertTokenTypesMatch(input);
    }

    @Test
    public void givenPrefixedNumber_ReturnsCorrectTypes() {
        String input = "+1";
        expectedTypes.add(Number.class);

        assertTokenTypesMatch(input);
    }

    @Test
    public void givenPrefixedNegativeNumberAndIdentifier_ReturnsCorrectTypes() {
        String input = "-1apple";
        expectedTypes.add(Number.class);
        expectedTypes.add(Identifier.class);

        assertTokenTypesMatch(input);
    }

    @Test
    public void givenPrefixedNumberAndIdentifier_ReturnsCorrectTypes() {
        String input = "+1apple";
        expectedTypes.add(Number.class);
        expectedTypes.add(Identifier.class);

        assertTokenTypesMatch(input);
    }

    @Test
    public void givenPrefixedNegativeNumberAndString_ReturnsCorrectTypes() {
        String input = "-1\"apple\"";
        expectedTypes.add(Number.class);
        expectedTypes.add(QuotedString.class);

        assertTokenTypesMatch(input);
    }

    @Test
    public void givenPrefixedNumberAndString_ReturnsCorrectTypes() {
        String input = "+1\"apple\"";
        expectedTypes.add(Number.class);
        expectedTypes.add(QuotedString.class);

        assertTokenTypesMatch(input);
    }

    @Test
    public void givenMultipleDigitNumber_ReturnsCorrectTypes() {
        String input = "1234567890";
        expectedTypes.add(Number.class);

        assertTokenTypesMatch(input);
    }

    @Test
    public void givenQuote_ReturnsCorrectTypes() {
        String input = "'";
        expectedTypes.add(QuoteMark.class);

        assertTokenTypesMatch(input);
    }

    @Test
    public void givenManyTypesWithNoWhitespace_ReturnsCorrectTypes() {
        String input = "xxx\"hi\"999()'aaa";
        expectedTypes.add(Identifier.class);
        expectedTypes.add(QuotedString.class);
        expectedTypes.add(Number.class);
        expectedTypes.add(LeftParenthesis.class);
        expectedTypes.add(RightParenthesis.class);
        expectedTypes.add(QuoteMark.class);
        expectedTypes.add(Identifier.class);

        assertTokenTypesMatch(input);
    }

    @Test
    public void givenFunctionCall_ReturnsCorrectTypes() {
        String input = "(defun myFunction (x)\n  (print x))";
        expectedTypes.add(LeftParenthesis.class);
        expectedTypes.add(Identifier.class);
        expectedTypes.add(Identifier.class);
        expectedTypes.add(LeftParenthesis.class);
        expectedTypes.add(Identifier.class);
        expectedTypes.add(RightParenthesis.class);
        expectedTypes.add(LeftParenthesis.class);
        expectedTypes.add(Identifier.class);
        expectedTypes.add(Identifier.class);
        expectedTypes.add(RightParenthesis.class);
        expectedTypes.add(RightParenthesis.class);

        assertTokenTypesMatch(input);
    }

    @Test
    public void givenBackTickExpression_ReturnsCorrectTypes() {
        String input = "`(list ,a ,@b)";
        expectedTypes.add(Backquote.class);
        expectedTypes.add(LeftParenthesis.class);
        expectedTypes.add(Identifier.class);
        expectedTypes.add(Comma.class);
        expectedTypes.add(Identifier.class);
        expectedTypes.add(Comma.class);
        expectedTypes.add(AtSign.class);
        expectedTypes.add(Identifier.class);
        expectedTypes.add(RightParenthesis.class);

        assertTokenTypesMatch(input);
    }

}
