package terminal;

import static com.googlecode.lanterna.input.KeyType.ArrowDown;
import static com.googlecode.lanterna.input.KeyType.ArrowLeft;
import static com.googlecode.lanterna.input.KeyType.ArrowRight;
import static com.googlecode.lanterna.input.KeyType.ArrowUp;
import static com.googlecode.lanterna.input.KeyType.Backspace;
import static com.googlecode.lanterna.input.KeyType.Delete;
import static com.googlecode.lanterna.input.KeyType.Enter;
import static com.googlecode.lanterna.input.KeyType.Escape;
import static terminal.LispTerminal.END_OF_SEGMENT;

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

public class LispTerminalTest {

    private VirtualTerminalInteractor terminal;

    @Before
    public void setUp() {
        terminal = new VirtualTerminalInteractor();
        terminal.start();
    }

    @After
    public void tearDown() {
        terminal.stop();
    }

    @Test
    public void leftArrowDoesNotMovePastOrigin() {
        terminal.pressKey(ArrowLeft);
        terminal.assertCursorPosition(0, 0);
    }

    @Test
    public void leftArrowWorksAfterEnteringCharacters() {
        terminal.enterCharacters("abc");
        terminal.assertCursorPosition(3, 0);
        terminal.pressKey(ArrowLeft);
        terminal.assertCursorPosition(2, 0);
        terminal.pressKey(ArrowLeft);
        terminal.assertCursorPosition(1, 0);
        terminal.pressKey(ArrowLeft);
        terminal.assertCursorPosition(0, 0);
        terminal.pressKey(ArrowLeft);
        terminal.assertCursorPosition(0, 0);
    }

    @Test
    public void leftArrowWorksAcrossRows() {
        terminal.setColumns(5);
        terminal.enterCharacters("123451");
        terminal.assertCursorPosition(1, 1);
        terminal.pressKeyTimes(ArrowLeft, 2);
        terminal.assertCursorPosition(4, 0);
    }

    @Test
    public void rightArrowDoesNotMovePastEndOfInput() {
        terminal.pressKey(ArrowRight);
        terminal.assertCursorPosition(0, 0);
    }

    @Test
    public void rightArrowWorksAfterMovingLeft() {
        terminal.enterCharacters("12");
        terminal.assertCursorPosition(2, 0);
        terminal.pressKey(ArrowLeft);
        terminal.assertCursorPosition(1, 0);
        terminal.pressKey(ArrowRight);
        terminal.assertCursorPosition(2, 0);
        terminal.pressKey(ArrowRight);
        terminal.assertCursorPosition(2, 0);
    }

    @Test
    public void rightArrowWorksAcrossRow() {
        terminal.setColumns(5);
        terminal.enterCharacters("123451");
        terminal.assertCursorPosition(1, 1);
        terminal.pressKeyTimes(ArrowLeft, 3);
        terminal.assertCursorPosition(3, 0);
        terminal.pressKeyTimes(ArrowRight, 3);
        terminal.assertCursorPosition(1, 1);
    }

    @Test
    public void characterKeyIsEchoed() {
        terminal.enterCharacter('a');
        terminal.assertCursorPosition(1, 0);
        terminal.assertScreenText("a");
    }

    @Test
    public void characterIsInserted() {
        terminal.enterCharacters("abcd");
        terminal.pressKeyTimes(ArrowLeft, 2);
        terminal.enterCharacter('x');
        terminal.assertScreenText("abxcd");
    }

    @Test
    public void characterIsInserted_PushesInputToNextRow() {
        terminal.setColumns(4);
        terminal.enterCharacters("abcd");
        terminal.pressKeyTimes(ArrowLeft, 2);
        terminal.enterCharacter('x');
        terminal.assertScreenText("abxc", "d   ");
    }

    @Test
    public void backspaceDoesNothingAtOrigin() {
        terminal.pressKey(Backspace);
        terminal.assertCursorPosition(0, 0);
    }

    @Test
    public void backspaceWorksAfterInput() {
        terminal.enterCharacters("12345");
        terminal.pressKeyTimes(Backspace, 2);
        terminal.assertCursorPosition(3, 0);
        terminal.assertScreenText("123   ");
    }

    @Test
    public void backspaceWorksAcrossRow() {
        terminal.setColumns(4);
        terminal.enterCharacters("1234567");
        terminal.pressKeyTimes(Backspace, 5);
        terminal.assertCursorPosition(2, 0);
        terminal.assertScreenText("12  ", "    ");
    }

    @Test
    public void backspaceWorksInMiddleOfInput() {
        terminal.enterCharacters("12345");
        terminal.pressKeyTimes(ArrowLeft, 2);
        terminal.pressKey(Backspace);
        terminal.assertCursorPosition(2, 0);
        terminal.assertScreenText("1245");
    }

    @Test
    public void deleteDoesNothingAtOrigin() {
        terminal.pressKey(Delete);
        terminal.assertCursorPosition(0, 0);
    }

    @Test
    public void deleteDoesNothingAtEndOfInput() {
        terminal.enterCharacters("del");
        terminal.pressKey(Delete);
        terminal.assertCursorPosition(3, 0);
        terminal.assertScreenText("del");
    }

    @Test
    public void deleteWorksAtStartOfInput() {
        terminal.enterCharacters("del");
        terminal.pressKeyTimes(ArrowLeft, 3);
        terminal.pressKeyTimes(Delete, 3);
        terminal.assertCursorPosition(0, 0);
        terminal.assertScreenText("   ");
    }

    @Test
    public void deleteWorksAcrossRow() {
        terminal.setColumns(4);
        terminal.enterCharacters("delete");
        terminal.pressKeyTimes(ArrowLeft, 5);
        terminal.pressKey(Delete);
        terminal.assertCursorPosition(1, 0);
        terminal.assertScreenText("dlet", "e   ");
    }

    @Test
    public void enterMovesToNextLine() {
        terminal.pressKey(Enter);
        terminal.assertCursorPosition(0, 1);
    }

    @Test
    public void enterWritesLineToPipedStream() {
        terminal.enterCharacters("enter");
        terminal.pressKey(Enter);
        terminal.assertInputWritten("enter\n");
    }

    @Test
    public void enterPressedInMiddleOfInput_WritesEntireLineToPipedStream() {
        terminal.enterCharacters("enter");
        terminal.pressKeyTimes(ArrowLeft, 2);
        terminal.pressKey(Enter);
        terminal.assertInputWritten("enter\n");
    }

    @Test
    public void enterAfterInsertedText_WritesLineToPipedStream() {
        terminal.enterCharacters("enter");
        terminal.pressKeyTimes(ArrowLeft, 2);
        terminal.enterCharacters("||");
        terminal.pressKey(Enter);
        terminal.assertInputWritten("ent||er\n");
    }

    @Test
    public void enterAfterBackspace_WritesLineToPipedStream() {
        terminal.enterCharacters("enter");
        terminal.pressKeyTimes(Backspace, 2);
        terminal.pressKey(Enter);
        terminal.assertInputWritten("ent\n");
    }

    @Test
    public void enterAfterDelete_WritesLineToPipedStream() {
        terminal.enterCharacters("enter");
        terminal.pressKeyTimes(ArrowLeft, 2);
        terminal.pressKeyTimes(Delete, 2);
        terminal.pressKey(Enter);
        terminal.assertInputWritten("ent\n");
    }

    @Test
    public void controlDWorks() {
        terminal.enterCharacters("control-d");
        terminal.enterControlCharacter('d');
        terminal.assertInputStreamClosed();
        terminal.assertInputWritten("control-d\n");
    }

    @Test
    public void controlCWorks() {
        terminal.enterCharacters("ctrl-c");
        terminal.enterControlCharacter('c');
        terminal.produceOutput("");
        terminal.assertInputStreamClosed();
        terminal.assertInputWritten("");
        terminal.assertScreenText("ctrl-c   ", "         ");
    }

    @Test
    public void controlDWorksInMiddleOfInput() {
        terminal.enterCharacters("control-d");
        terminal.pressKeyTimes(ArrowLeft, 2);
        terminal.enterControlCharacter('d');
        terminal.assertInputStreamClosed();
        terminal.assertInputWritten("control-d\n");
    }

    @Test
    public void escapeDoesNothing() {
        terminal.pressKey(Escape);
        terminal.assertCursorPosition(0, 0);
        terminal.assertInputWritten("");
    }

    @Test
    public void controlQDoesNothing() {
        terminal.enterControlCharacter('q');
        terminal.assertCursorPosition(0, 0);
        terminal.assertInputWritten("");
    }

    @Test
    public void controlEnterDoesNothing() {
        terminal.pressControlKey(Enter);
        terminal.assertCursorPosition(0, 0);
        terminal.assertInputWritten("");
    }

    @Test
    public void outputIsWritten() {
        terminal.produceOutput("output");
        terminal.assertCursorPosition(6, 0);
        terminal.assertScreenText("output");
    }

    @Test
    public void endOfSegmentCharacterIsNotPrinted() {
        terminal.produceOutput("> " + END_OF_SEGMENT);
        terminal.assertCursorPosition(2, 0);
        terminal.assertScreenText(">  ");
    }

    @Test
    public void enterTextPastLastLineOfBuffer() {
        terminal.setColumns(3);
        terminal.setRows(2);
        terminal.enterCharacters("01201201");
        terminal.assertCursorPosition(2, 1);
        terminal.assertScreenText("012", "01 ");
    }

    @Test
    public void insertingTextPushesInputPastEndOfBuffer() {
        terminal.setColumns(3);
        terminal.setRows(4);
        terminal.pressKey(Enter);
        terminal.enterCharacters("00011122");
        terminal.pressKeyTimes(ArrowLeft, 4);
        terminal.assertCursorPosition(1, 2);
        terminal.enterCharacters("zz");
        terminal.assertCursorPosition(0, 2);
        terminal.assertScreenText("000", "1zz", "112", "2  ");
    }

    @Test
    public void insertingTextDoesNothingWhenBufferFilled() {
        terminal.setColumns(3);
        terminal.setRows(3);
        terminal.enterCharacters("00011122");
        terminal.pressKeyTimes(ArrowLeft, 4);
        terminal.assertCursorPosition(1, 1);
        terminal.enterCharacters("zz");
        terminal.assertCursorPosition(1, 1);
        terminal.assertScreenText("000", "111", "22 ");
    }

    @Test
    public void appendingTextDoesNothingWhenBufferFilled() {
        terminal.setColumns(3);
        terminal.setRows(3);
        terminal.enterCharacters("000111222333444");
        terminal.assertCursorPosition(2, 2);
        terminal.assertScreenText("000", "111", "22 ");
    }

    @Test
    public void printedOutputToEndOfRow_MovesCursorToNextRow() {
        terminal.setColumns(3);
        terminal.produceOutput("out");
        terminal.assertCursorPosition(0, 1);
    }

    @Test
    public void printedOutputToEndOfBuffer_MovesCursorToNewRow() {
        terminal.setColumns(3);
        terminal.setRows(2);
        terminal.produceOutput("output");
        terminal.assertCursorPosition(0, 1);
        terminal.assertScreenText("put", "   ");
    }

    @Test
    public void outputDoesNotOverwriteInput_AndRedisplaysInput() {
        terminal.setColumns(3);
        terminal.enterCharacters("0123");
        terminal.pressKeyTimes(ArrowLeft, 3);
        terminal.produceOutput("out");
        terminal.assertCursorPosition(2, 3);
        terminal.assertScreenText("012", "3ou", "t01", "23 ", "   ");
    }

    @Test
    public void outputEndsOnSecondToLastColumn_MovesToNewRow() {
        terminal.setColumns(3);
        terminal.enterCharacters("01234");
        terminal.pressKeyTimes(ArrowLeft, 3);
        terminal.produceOutput("out");
        terminal.assertCursorPosition(2, 4);
        terminal.assertScreenText("012", "34o", "ut ", "012", "34 ");
    }

    @Test
    public void outputEndsOnLastColumn_MovesToNewRow() {
        terminal.setColumns(3);
        terminal.enterCharacters("012345");
        terminal.pressKeyTimes(ArrowLeft, 3);
        terminal.produceOutput("out");
        terminal.assertCursorPosition(0, 5);
        terminal.assertScreenText("012", "345", "out", "012", "345", "   ");
    }

    @Test
    public void outputRedisplaysInputAtEndOfBuffer() {
        terminal.setColumns(3);
        terminal.setRows(4);
        terminal.enterCharacters("01234");
        terminal.pressKeyTimes(ArrowLeft, 3);
        terminal.produceOutput("out");
        terminal.assertCursorPosition(2, 3);
        terminal.assertScreenText("34o", "ut ", "012");
    }

    @Test
    public void outputDoesNotOverwriteInput_AfterEnter() {
        terminal.setColumns(3);
        terminal.enterCharacters("01201201");
        terminal.pressKeyTimes(ArrowLeft, 5);
        terminal.pressKey(Enter);
        terminal.produceOutput("out");
        terminal.assertCursorPosition(0, 4);
        terminal.assertScreenText("012", "012", "01 ", "out", "   ");
    }

    @Test
    public void resizeIsHandledGracefully() {
        terminal.enterCharacters("resize");
        terminal.pressKey(Enter);
        terminal.enterCharacters("test");
        terminal.setColumns(3);
        terminal.assertCursorPosition(1, 1);
        terminal.assertScreenText("tes", "t  ");
    }

    @Test
    public void backspaceWorksAfterResize() {
        terminal.enterCharacters("resize");
        terminal.pressKey(Enter);
        terminal.enterCharacters("test");
        terminal.setColumns(3);
        terminal.pressKeyTimes(Backspace, 20);
        terminal.assertCursorPosition(0, 0);
        terminal.assertScreenText("   ", "   ");
    }

    @Test
    public void deleteWorksAfterResize() {
        terminal.enterCharacters("resize");
        terminal.pressKey(Enter);
        terminal.enterCharacters("test");
        terminal.setColumns(3);
        terminal.pressKeyTimes(ArrowLeft, 20);
        terminal.pressKeyTimes(Delete, 20);
        terminal.pressKeyTimes(ArrowRight, 20);
        terminal.assertCursorPosition(0, 0);
        terminal.assertScreenText("   ", "   ");
    }

    @Test
    public void controlSequencesAreNotPrinted() {
        terminal.produceOutput("\u001B[32mcontrol\u001B[0mseq");
        terminal.assertScreenText("controlseq");
    }

    @Test
    public void upArrowWorks() {
        terminal.enterCharacters("(one)");
        terminal.pressKey(Enter);
        terminal.enterCharacters("(two)");
        terminal.pressKey(Enter);
        terminal.pressKey(ArrowUp);
        terminal.assertScreenText("(one)", "(two)", "(two)");
        terminal.assertCursorPosition(5, 2);
    }

    @Test
    public void upArrowErasesCurrentLine() {
        terminal.enterCharacters("(one)");
        terminal.pressKey(Enter);
        terminal.enterCharacters("(two)");
        terminal.pressKey(Enter);
        terminal.enterCharacters("(three)");
        terminal.pressKey(ArrowUp);
        terminal.assertScreenText("(one)", "(two)", "(two)     ");
        terminal.assertCursorPosition(5, 2);
    }

    @Test
    public void upArrowStopsAtFirstLine() {
        terminal.enterCharacters("(one)");
        terminal.pressKey(Enter);
        terminal.enterCharacters("(two)");
        terminal.pressKey(Enter);
        terminal.pressKeyTimes(ArrowUp, 5);
        terminal.assertScreenText("(one)", "(two)", "(one)");
        terminal.assertCursorPosition(5, 2);
    }

    @Test
    public void originIsUpdatedWhenPreviousLineMovesPastEndOfBuffer() {
        terminal.setRows(3);
        terminal.setColumns(3);
        terminal.enterCharacters("12345");
        terminal.pressKey(Enter);
        terminal.pressKey(ArrowUp);
        terminal.assertScreenText("45 ", "123", "45 ");
        terminal.pressKeyTimes(ArrowLeft, 10);
        terminal.assertCursorPosition(0, 1);
    }

    @Test
    public void downArrowWorks() {
        terminal.enterCharacters("(one)");
        terminal.pressKey(Enter);
        terminal.enterCharacters("(two)");
        terminal.pressKey(Enter);
        terminal.enterCharacters("(three)");
        terminal.pressKey(ArrowUp);
        terminal.pressKey(ArrowDown);
        terminal.assertScreenText("(one)", "(two)", "(three)");
        terminal.assertCursorPosition(7, 2);
    }

    @Test
    public void downArrowStopsAtLastLine() {
        terminal.pressKeyTimes(ArrowDown, 5);
        terminal.assertScreenText(" ");
        terminal.assertCursorPosition(0, 0);
    }

}
