package scanner;

import static error.ErrorManager.Severity.CRITICAL;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import static testutil.TestUtilities.createIOExceptionThrowingInputStream;
import static testutil.TestUtilities.createInputStreamFromString;

import java.io.InputStream;

import org.junit.Test;

import scanner.LispInputStream.MaximumUnreadsExceededException;
import stream.UncheckedIOException;

public class LispCommentRemovingInputStreamTest {

    private String getLispCommentRemovingInputStreamResult(String inputString) {
        return readInputStreamIntoString(createLispInputStream(inputString));
    }

    private LispInputStream createLispInputStream(String inputString) {
        InputStream stringInputStream = createInputStreamFromString(inputString);

        return new LispCommentRemovingInputStream(stringInputStream);
    }

    private String readInputStreamIntoString(LispInputStream inputStream) {
        StringBuilder charactersRead = new StringBuilder();
        for (int c = inputStream.read(); c != -1; c = inputStream.read())
            charactersRead.append((char) c);

        return charactersRead.toString();
    }

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

        assertEquals(input, getLispCommentRemovingInputStreamResult(input));
    }

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

        assertEquals(input, getLispCommentRemovingInputStreamResult(input));
    }

    @Test
    public void allNonCommentCharacters_notRemoved() {
        String input = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
                       + "`1234567890-=~!@#$%^&*()_+[]\\',./{}|:\"<>?";

        assertEquals(input, getLispCommentRemovingInputStreamResult(input));
    }

    @Test
    public void oneComment_Removed() {
        String input = ";comment";
        String expectedResult = "";

        assertEquals(expectedResult, getLispCommentRemovingInputStreamResult(input));
    }

    @Test
    public void multipleComments_Removed() {
        String input = ";comment1\n;comment2\n;comment3";
        String expectedResult = "\n\n";

        assertEquals(expectedResult, getLispCommentRemovingInputStreamResult(input));
    }

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

        assertEquals(expectedResult, getLispCommentRemovingInputStreamResult(input));
    }

    @Test
    public void interiorComment_Removed() {
        String input = "(;this is a comment\n)";
        String expectedResult = "(\n)";

        assertEquals(expectedResult, getLispCommentRemovingInputStreamResult(input));
    }

    @Test
    public void commentInString_NotRemoved() {
        String input = "\"string;this should remain\"";

        assertEquals(input, getLispCommentRemovingInputStreamResult(input));
    }

    @Test
    public void commentInStringWithNewline_NotRemoved() {
        String input = "\"string;this should\n remain\"";

        assertEquals(input, getLispCommentRemovingInputStreamResult(input));
    }

    @Test
    public void commentInStringWithEscapedDoubleQuote_NotRemoved() {
        String input = "\"string \\\" ;this should remain\"";

        assertEquals(input, getLispCommentRemovingInputStreamResult(input));
    }

    @Test
    public void manyCommentsWithStatements_OnlyCommentsRemoved() {
        String input = ";first comment \n '(1 2 3) \n ;second comment \n (defun add1 (x) (+ x 1)) ;third comment";
        String expectedResult = "\n '(1 2 3) \n \n (defun add1 (x) (+ x 1)) ";

        assertEquals(expectedResult, getLispCommentRemovingInputStreamResult(input));
    }

    @Test
    public void unreadOneCharacter_ReturnsSameCharacter() {
        String input = "abc";
        char expectedResult = 'a';
        LispInputStream lispInputStream = createLispInputStream(input);

        lispInputStream.read();
        lispInputStream.unreadLastCharacter();

        assertEquals(expectedResult, lispInputStream.read());
    }

    @Test
    public void unreadAndRereadSameCharacterMultipleTimes_ReturnsSameCharacter() {
        String input = "abc";
        char expectedResult = 'a';
        LispInputStream lispInputStream = createLispInputStream(input);

        lispInputStream.read();
        lispInputStream.unreadLastCharacter();
        lispInputStream.read();
        lispInputStream.unreadLastCharacter();
        lispInputStream.read();
        lispInputStream.unreadLastCharacter();

        assertEquals(expectedResult, lispInputStream.read());
    }

    @Test
    public void unreadOneCharacterAndRereadIt_ReturnsNextCharacterOnNextRead() {
        String input = "abc";
        char expectedResult = 'b';
        LispInputStream lispInputStream = createLispInputStream(input);

        lispInputStream.read();
        lispInputStream.unreadLastCharacter();
        lispInputStream.read();

        assertEquals(expectedResult, lispInputStream.read());
    }

    @Test
    public void unreadNewlineInStringAfterComment_ReturnsNewline() {
        String input = "a;123\n";
        char expectedResult = '\n';
        LispInputStream lispInputStream = createLispInputStream(input);

        lispInputStream.read();
        lispInputStream.read();
        lispInputStream.unreadLastCharacter();

        assertEquals(expectedResult, lispInputStream.read());
    }

    @Test(expected = MaximumUnreadsExceededException.class)
    public void callUnreadMultipleTimes_ThrowsException() {
        String input = "abc";
        LispInputStream lispInputStream = createLispInputStream(input);

        lispInputStream.read();
        lispInputStream.unreadLastCharacter();
        lispInputStream.unreadLastCharacter();
    }

    @Test
    public void callUnreadMultipleTimes_ExceptionHasCorrectSeverity() {
        String input = "abc";
        LispInputStream lispInputStream = createLispInputStream(input);

        lispInputStream.read();
        lispInputStream.unreadLastCharacter();

        try {
            lispInputStream.unreadLastCharacter();
        } catch (MaximumUnreadsExceededException e) {
            assertEquals(CRITICAL, e.getSeverity());
        }
    }

    @Test(expected = UncheckedIOException.class)
    public void underlyingInputStreamThrowsIOException_ConvertsToUncheckedIOException() {
        InputStream ioExceptionThrowingInputStream = createIOExceptionThrowingInputStream();
        LispInputStream lispInputStream = new LispCommentRemovingInputStream(ioExceptionThrowingInputStream);

        lispInputStream.read();
    }

    @Test
    public void underlyingInputStreamThrowsIOException_ExceptionHasCorrectAttributes() {
        InputStream ioExceptionThrowingInputStream = createIOExceptionThrowingInputStream();
        LispInputStream lispInputStream = new LispCommentRemovingInputStream(ioExceptionThrowingInputStream);

        try {
            lispInputStream.read();
        } catch (UncheckedIOException e) {
            String message = e.getMessage();
            assertNotNull(message);
            assertTrue(message.length() >= 0);
            assertEquals(CRITICAL, e.getSeverity());
        }
    }

}
