/*
 * This file is part of lanterna (https://github.com/mabe02/lanterna).
 *
 * lanterna is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 *
 * Copyright (C) 2010-2020 Martin Berglund
 */
package com.googlecode.lanterna.terminal.virtual;

import com.googlecode.lanterna.*;
import com.googlecode.lanterna.input.KeyStroke;
import com.googlecode.lanterna.terminal.Terminal;
import org.junit.Test;

import java.util.Arrays;
import java.util.Collections;
import java.util.TreeSet;
import java.util.concurrent.atomic.AtomicInteger;

import static org.junit.Assert.*;

public class DefaultVirtualTerminalTest {
    private static final TextCharacter DEFAULT_CHARACTER = TextCharacter.DEFAULT_CHARACTER;
    private final DefaultVirtualTerminal virtualTerminal;

    public DefaultVirtualTerminalTest() {
        this.virtualTerminal = new DefaultVirtualTerminal();
    }

    @Test
    public void initialTerminalStateIsAsExpected() {
        assertEquals(TerminalPosition.TOP_LEFT_CORNER, virtualTerminal.getCursorPosition());
        TerminalSize terminalSize = virtualTerminal.getTerminalSize();
        assertEquals(new TerminalSize(80, 24), terminalSize);

        for(int row = 0; row < terminalSize.getRows(); row++) {
            for(int column = 0; column < terminalSize.getColumns(); column++) {
                assertEquals(DEFAULT_CHARACTER, virtualTerminal.getCharacter(column, row));
            }
        }
    }

    @Test
    public void simpleTestOutputTest() {
        String testString = "Hello World!";
        for(char c: testString.toCharArray()) {
            virtualTerminal.putCharacter(c);
        }
        assertLineEquals(testString, 0);
        assertLineEquals("", 1);
        assertEquals(new TerminalPosition(testString.length(), 0), virtualTerminal.getCursorPosition());
    }

    @Test
    public void multiLineTextTest() {
        String[] toPrint = new String[] {
                "Hello",
                "Hallo",
                "Hallå",
                "こんにちは"
        };
        for(String string: toPrint) {
            for(char c : string.toCharArray()) {
                virtualTerminal.putCharacter(c);
            }
            virtualTerminal.putCharacter('\n');
        }
        for(int i = 0; i < toPrint.length; i++) {
            assertLineEquals(toPrint[i], i);
        }
        assertEquals(new TerminalPosition(0, toPrint.length), virtualTerminal.getCursorPosition());
    }

    @Test
    public void singleLineWriteAndReadBackWorks() {
        assertEquals(TerminalPosition.TOP_LEFT_CORNER, virtualTerminal.getCursorPosition());
        virtualTerminal.putCharacter(new TextCharacter('H'));
        virtualTerminal.putCharacter(new TextCharacter('E'));
        virtualTerminal.putCharacter(new TextCharacter('L'));
        virtualTerminal.putCharacter(new TextCharacter('L'));
        virtualTerminal.putCharacter(new TextCharacter('O'));
        assertEquals(TerminalPosition.TOP_LEFT_CORNER.withColumn(5), virtualTerminal.getCursorPosition());
        assertEquals('H', virtualTerminal.getCharacter(new TerminalPosition(0, 0)).getCharacter());
        assertEquals('E', virtualTerminal.getCharacter(new TerminalPosition(1, 0)).getCharacter());
        assertEquals('L', virtualTerminal.getCharacter(new TerminalPosition(2, 0)).getCharacter());
        assertEquals('L', virtualTerminal.getCharacter(new TerminalPosition(3, 0)).getCharacter());
        assertEquals('O', virtualTerminal.getCharacter(new TerminalPosition(4, 0)).getCharacter());

        assertFalse(virtualTerminal.isWholeBufferDirtyThenReset());
        assertEquals(new TreeSet<>(Arrays.asList(
                new TerminalPosition(0, 0),
                new TerminalPosition(1, 0),
                new TerminalPosition(2, 0),
                new TerminalPosition(3, 0),
                new TerminalPosition(4, 0))),
                virtualTerminal.getAndResetDirtyCells());

        // Make sure it's reset
        assertEquals(Collections.emptySet(), virtualTerminal.getAndResetDirtyCells());
    }

    @Test
    public void clearAllMarksEverythingAsDirtyAndEverythingInTheTerminalIsReplacedWithDefaultCharacter() {
        virtualTerminal.setTerminalSize(new TerminalSize(10, 5));
        assertEquals(TerminalPosition.TOP_LEFT_CORNER, virtualTerminal.getCursorPosition());
        virtualTerminal.putCharacter(new TextCharacter('H'));
        virtualTerminal.putCharacter(new TextCharacter('E'));
        virtualTerminal.putCharacter(new TextCharacter('L'));
        virtualTerminal.putCharacter(new TextCharacter('L'));
        virtualTerminal.putCharacter(new TextCharacter('O'));
        virtualTerminal.clearScreen();

        assertTrue(virtualTerminal.isWholeBufferDirtyThenReset());
        assertEquals(Collections.emptySet(), virtualTerminal.getAndResetDirtyCells());

        assertEquals(TerminalPosition.TOP_LEFT_CORNER, virtualTerminal.getCursorPosition());
        assertEquals(TextCharacter.DEFAULT_CHARACTER, virtualTerminal.getCharacter(new TerminalPosition(0, 0)));
        assertEquals(TextCharacter.DEFAULT_CHARACTER, virtualTerminal.getCharacter(new TerminalPosition(1, 0)));
        assertEquals(TextCharacter.DEFAULT_CHARACTER, virtualTerminal.getCharacter(new TerminalPosition(2, 0)));
        assertEquals(TextCharacter.DEFAULT_CHARACTER, virtualTerminal.getCharacter(new TerminalPosition(3, 0)));
        assertEquals(TextCharacter.DEFAULT_CHARACTER, virtualTerminal.getCharacter(new TerminalPosition(4, 0)));
    }

    @Test
    public void replacingAllContentTriggersWholeTerminalIsDirty() {
        virtualTerminal.setTerminalSize(new TerminalSize(5, 3));
        assertEquals(TerminalPosition.TOP_LEFT_CORNER, virtualTerminal.getCursorPosition());
        virtualTerminal.putCharacter(new TextCharacter('H'));
        virtualTerminal.putCharacter(new TextCharacter('E'));
        virtualTerminal.putCharacter(new TextCharacter('L'));
        virtualTerminal.putCharacter(new TextCharacter('L'));
        virtualTerminal.putCharacter(new TextCharacter('O'));
        virtualTerminal.putCharacter(new TextCharacter('H'));
        virtualTerminal.putCharacter(new TextCharacter('E'));
        virtualTerminal.putCharacter(new TextCharacter('L'));
        virtualTerminal.putCharacter(new TextCharacter('L'));
        virtualTerminal.putCharacter(new TextCharacter('O'));
        virtualTerminal.putCharacter(new TextCharacter('B'));
        virtualTerminal.putCharacter(new TextCharacter('Y'));
        virtualTerminal.putCharacter(new TextCharacter('E'));
        virtualTerminal.putCharacter(new TextCharacter('!'));

        assertTrue(virtualTerminal.isWholeBufferDirtyThenReset());
        assertEquals(Collections.emptySet(), virtualTerminal.getAndResetDirtyCells());
    }

    @Test
    public void tooLongLinesWrap() {
        virtualTerminal.setTerminalSize(new TerminalSize(5, 5));
        assertEquals(TerminalPosition.TOP_LEFT_CORNER, virtualTerminal.getCursorPosition());
        virtualTerminal.putCharacter(new TextCharacter('H'));
        virtualTerminal.putCharacter(new TextCharacter('E'));
        virtualTerminal.putCharacter(new TextCharacter('L'));
        virtualTerminal.putCharacter(new TextCharacter('L'));
        virtualTerminal.putCharacter(new TextCharacter('O'));
        virtualTerminal.putCharacter(new TextCharacter('!'));
        assertEquals(TerminalPosition.OFFSET_1x1, virtualTerminal.getCursorPosition());

        // Expected layout:
        // |HELLO|
        // |!    |
        // where the cursor is one column after the '!'
    }

    @Test
    public void makeSureDoubleWidthCharactersWrapProperly() {
        virtualTerminal.setTerminalSize(new TerminalSize(9, 5));
        assertEquals(TerminalPosition.TOP_LEFT_CORNER, virtualTerminal.getCursorPosition());
        virtualTerminal.putCharacter(new TextCharacter('こ'));
        virtualTerminal.putCharacter(new TextCharacter('ん'));
        virtualTerminal.putCharacter(new TextCharacter('に'));
        virtualTerminal.putCharacter(new TextCharacter('ち'));
        virtualTerminal.putCharacter(new TextCharacter('は'));
        virtualTerminal.putCharacter(new TextCharacter('!'));
        assertEquals(new TerminalPosition(3, 1), virtualTerminal.getCursorPosition());

        // Expected layout:
        // |こんにち|
        // |は!    |
        // where the cursor is one column after the '!' (2 + 1 = 3rd column)

        // Make sure there's a default padding character at 8x0
        assertEquals(TextCharacter.DEFAULT_CHARACTER, virtualTerminal.getCharacter(new TerminalPosition(8, 0)));
    }

    @Test
    public void overwritingDoubleWidthCharactersEraseTheOtherHalf() {
        virtualTerminal.setTerminalSize(new TerminalSize(5, 5));
        virtualTerminal.putCharacter(new TextCharacter('画'));
        virtualTerminal.putCharacter(new TextCharacter('面'));

        assertEquals('画', virtualTerminal.getCharacter(new TerminalPosition(0, 0)).getCharacter());
        assertEquals('画', virtualTerminal.getCharacter(new TerminalPosition(1, 0)).getCharacter());
        assertEquals('面', virtualTerminal.getCharacter(new TerminalPosition(2, 0)).getCharacter());
        assertEquals('面', virtualTerminal.getCharacter(new TerminalPosition(3, 0)).getCharacter());

        virtualTerminal.setCursorPosition(new TerminalPosition(0, 0));
        virtualTerminal.putCharacter(new TextCharacter('Y'));

        assertEquals('Y', virtualTerminal.getCharacter(new TerminalPosition(0, 0)).getCharacter());
        assertEquals(TextCharacter.DEFAULT_CHARACTER, virtualTerminal.getCharacter(new TerminalPosition(1, 0)));

        virtualTerminal.setCursorPosition(new TerminalPosition(3, 0));
        virtualTerminal.putCharacter(new TextCharacter('V'));

        assertEquals(TextCharacter.DEFAULT_CHARACTER, virtualTerminal.getCharacter(new TerminalPosition(2, 0)));
        assertEquals('V', virtualTerminal.getCharacter(new TerminalPosition(3, 0)).getCharacter());
    }

    @Test
    public void testCursorPositionUpdatesWhenTerminalSizeChanges() {
        virtualTerminal.setTerminalSize(new TerminalSize(3, 3));
        virtualTerminal.putCharacter('\n');
        virtualTerminal.putCharacter('\n');
        assertEquals(new TerminalPosition(0, 2), virtualTerminal.getCursorPosition());
        virtualTerminal.putCharacter('\n');
        assertEquals(new TerminalPosition(0, 2), virtualTerminal.getCursorPosition());
        virtualTerminal.putCharacter('\n');
        assertEquals(new TerminalPosition(0, 2), virtualTerminal.getCursorPosition());

        // Shrink viewport
        virtualTerminal.setTerminalSize(new TerminalSize(3, 2));
        assertEquals(new TerminalPosition(0, 1), virtualTerminal.getCursorPosition());

        // Restore
        virtualTerminal.setTerminalSize(new TerminalSize(3, 3));
        assertEquals(new TerminalPosition(0, 2), virtualTerminal.getCursorPosition());

        // Enlarge
        virtualTerminal.setTerminalSize(new TerminalSize(3, 4));
        assertEquals(new TerminalPosition(0, 3), virtualTerminal.getCursorPosition());
        virtualTerminal.setTerminalSize(new TerminalSize(3, 5));
        assertEquals(new TerminalPosition(0, 4), virtualTerminal.getCursorPosition());

        // We've reached the total size of the buffer, enlarging it further shouldn't affect the cursor position
        virtualTerminal.setTerminalSize(new TerminalSize(3, 6));
        assertEquals(new TerminalPosition(0, 4), virtualTerminal.getCursorPosition());
        virtualTerminal.setTerminalSize(new TerminalSize(3, 7));
        assertEquals(new TerminalPosition(0, 4), virtualTerminal.getCursorPosition());
    }

    @Test
    public void textScrollingOutOfTheBacklogDisappears() {
        virtualTerminal.setTerminalSize(new TerminalSize(10, 3));
        // Backlog of 1, meaning viewport size + 1 row
        virtualTerminal.setBacklogSize(1);
        putString("Line 1\n");
        assertEquals(new TerminalPosition(0, 1), virtualTerminal.getCursorPosition());
        assertEquals(virtualTerminal.getCursorPosition(), virtualTerminal.getCursorBufferPosition());
        putString("Line 2\n");
        putString("Line 3\n");
        putString("Line 4\n"); // This should knock out "Line 1"

        // Expected content:
        //(|Line 1    | <- discarded)
        // ------------
        // |Line 2    | <- backlog
        // ------------
        // |Line 3    | <- viewport
        // |Line 4    | <- viewport
        // |          | <- viewport

        assertBufferLineEquals("Line 2", 0);
        assertBufferLineEquals("Line 3", 1);
        assertLineEquals("Line 3", 0);
        assertLineEquals("Line 4", 1);
        assertLineEquals("", 2);
        assertEquals(new TerminalPosition(0, 2), virtualTerminal.getCursorPosition());
        assertEquals(new TerminalPosition(0, 3), virtualTerminal.getCursorBufferPosition());

        // Make terminal bigger
        virtualTerminal.setTerminalSize(new TerminalSize(10, 4));

        // Now "Line 2" should be the top row
        assertLineEquals("Line 2", 0);
        assertLineEquals("Line 3", 1);
        assertLineEquals("Line 4", 2);
        assertLineEquals("", 3);
        assertEquals(new TerminalPosition(0, 3), virtualTerminal.getCursorPosition());
        assertEquals(new TerminalPosition(0, 3), virtualTerminal.getCursorBufferPosition());

        // Make it even bigger
        virtualTerminal.setTerminalSize(new TerminalSize(10, 5));

        // Should make no difference, the viewport will add an empty row at the end, because there is nothing in the
        // backlog to insert at the top
        assertLineEquals("Line 2", 0);
        assertLineEquals("Line 3", 1);
        assertLineEquals("Line 4", 2);
        assertLineEquals("", 3);
        assertLineEquals("", 4);
        assertEquals(new TerminalPosition(0, 3), virtualTerminal.getCursorPosition());
        assertEquals(new TerminalPosition(0, 3), virtualTerminal.getCursorBufferPosition());
    }

    @Test
    public void backlogTrimmingAdjustsCursorPositionAndDirtyCells() {
        virtualTerminal.setTerminalSize(new TerminalSize(80, 3));
        virtualTerminal.setBacklogSize(0);
        virtualTerminal.putCharacter(fromChar('A'));
        virtualTerminal.setCursorPosition(new TerminalPosition(1, 1));
        virtualTerminal.putCharacter(fromChar('B'));
        virtualTerminal.setCursorPosition(new TerminalPosition(2, 2));
        virtualTerminal.putCharacter(fromChar('C'));

        assertLineEquals("A", 0);
        assertLineEquals(" B", 1);
        assertLineEquals("  C", 2);

        // Dirty positions should now be these
        assertEquals(new TreeSet<>(Arrays.asList(
                new TerminalPosition(0, 0),
                new TerminalPosition(1, 1),
                new TerminalPosition(2, 2))), virtualTerminal.getDirtyCells());
        assertEquals(new TerminalPosition(3, 2), virtualTerminal.getCursorPosition());

        // Add one more row to shift out the first line
        virtualTerminal.putCharacter('\n');

        // Dirty positions should now be adjusted
        assertEquals(new TreeSet<>(Arrays.asList(
                new TerminalPosition(1, 0),
                new TerminalPosition(2, 1))), virtualTerminal.getDirtyCells());
        assertEquals(new TerminalPosition(0, 2), virtualTerminal.getCursorPosition());
    }

    @Test
    public void testPrivateMode() {
        final int ROWS = 5;
        virtualTerminal.setTerminalSize(new TerminalSize(20, ROWS));
        for(int i = 1; i <= ROWS + 2; i++) {
            putString("Line " + i + "\n");
        }
        assertEquals(new TerminalPosition(0, ROWS - 1), virtualTerminal.getCursorPosition());
        assertEquals(new TerminalPosition(0, ROWS + 2), virtualTerminal.getCursorBufferPosition());

        virtualTerminal.enterPrivateMode();
        assertEquals(new TerminalPosition(0, 0), virtualTerminal.getCursorPosition());
        assertEquals(new TerminalPosition(0, 0), virtualTerminal.getCursorBufferPosition());
        for(int i = 0; i < ROWS; i++) {
            assertLineEquals("", i);
        }

        // There should be no backlog in private mode
        for(int i = 1; i <= ROWS + 4; i++) {
            putString("Line " + i + "\n");
        }
        for(int i = 0; i < ROWS - 1; i++) {
            assertLineEquals("Line " + (i + 6), i);
        }
        assertLineEquals("", ROWS - 1);
        assertEquals(5, virtualTerminal.getBufferLineCount());

        virtualTerminal.exitPrivateMode();
        for(int i = 0; i < ROWS - 1; i++) {
            assertLineEquals("Line " + (i+4), i);
        }
        assertLineEquals("", ROWS - 1);
    }

    @Test
    public void testForEachLine() {
        final int ROWS = 40;
        virtualTerminal.setTerminalSize(new TerminalSize(10, 5));
        for(int i = 1; i <= ROWS; i++) {
            putString("Line " + i + "\n");
        }
        virtualTerminal.forEachLine(0, ROWS, (rowNumber, bufferLine) -> {
            if(rowNumber == ROWS) {
                assertLineEquals("", bufferLine);
            }
            else {
                assertLineEquals("Line " + (rowNumber + 1), bufferLine);
            }
        });
    }

    @Test
    public void testColorAndSGR() {
        virtualTerminal.putCharacter('A');
        virtualTerminal.setBackgroundColor(TextColor.ANSI.BLUE);
        virtualTerminal.setForegroundColor(TextColor.ANSI.WHITE);
        virtualTerminal.putCharacter('B');
        virtualTerminal.enableSGR(SGR.BOLD);
        virtualTerminal.enableSGR(SGR.UNDERLINE);
        virtualTerminal.putCharacter('C');
        virtualTerminal.disableSGR(SGR.BOLD);
        virtualTerminal.putCharacter('D');
        virtualTerminal.resetColorAndSGR();
        virtualTerminal.putCharacter('E');

        assertEquals(TextCharacter.DEFAULT_CHARACTER.withCharacter('A'), virtualTerminal.getCharacter(0, 0));
        assertEquals(new TextCharacter('B', TextColor.ANSI.WHITE, TextColor.ANSI.BLUE), virtualTerminal.getCharacter(1, 0));
        assertEquals(new TextCharacter('C', TextColor.ANSI.WHITE, TextColor.ANSI.BLUE, SGR.BOLD, SGR.UNDERLINE), virtualTerminal.getCharacter(2, 0));
        assertEquals(new TextCharacter('D', TextColor.ANSI.WHITE, TextColor.ANSI.BLUE, SGR.UNDERLINE), virtualTerminal.getCharacter(3, 0));
        assertEquals(TextCharacter.DEFAULT_CHARACTER.withCharacter('E'), virtualTerminal.getCharacter(4, 0));
    }

    @Test
    public void testTabExpansion() {
        putString("XXXXXXXXXXXXXXXXXX");
        virtualTerminal.setCursorPosition(0, 0);
        virtualTerminal.putCharacter('\t');
        assertLineEquals("    XXXXXXXXXXXXXX", 0);

        virtualTerminal.clearScreen();
        putString("XXXXXXXXXXXXXXXXXX");
        virtualTerminal.setCursorPosition(1, 0);
        virtualTerminal.putCharacter('\t');
        assertLineEquals("X   XXXXXXXXXXXXXX", 0);

        virtualTerminal.clearScreen();
        putString("XXXXXXXXXXXXXXXXXX");
        virtualTerminal.setCursorPosition(2, 0);
        virtualTerminal.putCharacter('\t');
        assertLineEquals("XX  XXXXXXXXXXXXXX", 0);

        virtualTerminal.clearScreen();
        putString("XXXXXXXXXXXXXXXXXX");
        virtualTerminal.setCursorPosition(3, 0);
        virtualTerminal.putCharacter('\t');
        assertLineEquals("XXX XXXXXXXXXXXXXX", 0);

        virtualTerminal.clearScreen();
        putString("XXXXXXXXXXXXXXXXXX");
        virtualTerminal.setCursorPosition(4, 0);
        virtualTerminal.putCharacter('\t');
        assertLineEquals("XXXX    XXXXXXXXXX", 0);

        virtualTerminal.clearScreen();
        putString("XXXXXXXXXXXXXXXXXX");
        virtualTerminal.setCursorPosition(5, 0);
        virtualTerminal.putCharacter('\t');
        assertLineEquals("XXXXX   XXXXXXXXXX", 0);
    }

    @Test
    public void testInput() {
        KeyStroke keyStroke1 = new KeyStroke('A', false, false);
        KeyStroke keyStroke2 = new KeyStroke('B', false, false);
        virtualTerminal.addInput(keyStroke1);
        virtualTerminal.addInput(keyStroke2);
        assertEquals(keyStroke1, virtualTerminal.pollInput());
        assertEquals(keyStroke2, virtualTerminal.readInput());
    }

    @Test
    public void testVirtualTerminalListener() {
        final AtomicInteger flushCounter = new AtomicInteger(0);
        final AtomicInteger bellCounter = new AtomicInteger(0);
        final AtomicInteger resizeCounter = new AtomicInteger(0);
        final AtomicInteger closeCounter = new AtomicInteger(0);

        VirtualTerminalListener listener = new VirtualTerminalListener() {
            @Override
            public void onFlush() {
                flushCounter.incrementAndGet();
            }

            @Override
            public void onBell() {
                bellCounter.incrementAndGet();
            }

            @Override
            public void onResized(Terminal terminal, TerminalSize newSize) {
                resizeCounter.incrementAndGet();
            }

            @Override
            public void onClose() {
                closeCounter.incrementAndGet();
            }
        };

        virtualTerminal.flush();
        virtualTerminal.bell();
        virtualTerminal.setTerminalSize(new TerminalSize(40, 10));
        assertEquals(0, flushCounter.get());
        assertEquals(0, bellCounter.get());
        assertEquals(0, resizeCounter.get());
        assertEquals(0, closeCounter.get());

        virtualTerminal.addVirtualTerminalListener(listener);
        virtualTerminal.flush();
        virtualTerminal.bell();
        virtualTerminal.setTerminalSize(new TerminalSize(80, 20));
        assertEquals(1, flushCounter.get());
        assertEquals(1, bellCounter.get());
        assertEquals(1, resizeCounter.get());
        assertEquals(0, closeCounter.get());

        virtualTerminal.close();
        assertEquals(1, closeCounter.get());

        virtualTerminal.removeVirtualTerminalListener(listener);
        virtualTerminal.flush();
        virtualTerminal.bell();
        virtualTerminal.setTerminalSize(new TerminalSize(40, 10));
        virtualTerminal.close();
        assertEquals(1, flushCounter.get());
        assertEquals(1, bellCounter.get());
        assertEquals(1, resizeCounter.get());
        assertEquals(1, closeCounter.get());
    }

    @Test
    public void settingCursorOutsideOfTerminalWindowWillBeAdjusted() {
        virtualTerminal.setTerminalSize(new TerminalSize(10, 5));
        virtualTerminal.setCursorPosition(20, 10);
        assertEquals(new TerminalPosition(9, 4), virtualTerminal.getCursorPosition());

        virtualTerminal.setCursorPosition(0, 10);
        assertEquals(new TerminalPosition(0, 4), virtualTerminal.getCursorPosition());

        virtualTerminal.setCursorPosition(20, 0);
        assertEquals(new TerminalPosition(9, 0), virtualTerminal.getCursorPosition());
    }

    @Test
    public void puttingCharacterInLastColumnDoesntMoveCursorToNextLine() {
        virtualTerminal.setTerminalSize(new TerminalSize(10, 5));
        virtualTerminal.setCursorPosition(8, 2);
        assertEquals(new TerminalPosition(8, 2), virtualTerminal.getCursorPosition());
        virtualTerminal.putCharacter('A');
        assertEquals(new TerminalPosition(9, 2), virtualTerminal.getCursorPosition());
        virtualTerminal.putCharacter('B');
        assertEquals(new TerminalPosition(10, 2), virtualTerminal.getCursorPosition());
        virtualTerminal.putCharacter('C');
        assertEquals(new TerminalPosition(1, 3), virtualTerminal.getCursorPosition());
        assertEquals(DEFAULT_CHARACTER.withCharacter('C'), virtualTerminal.getCharacter(0, 3));
    }

    private void putString(String string) {
        for(char c: string.toCharArray()) {
            virtualTerminal.putCharacter(c);
        }
    }

    private TextCharacter fromChar(char c) {
        return new TextCharacter(c);
    }

    private void assertLineEquals(String expectedLineContent, int rowNumber) {
        int column = 0;
        for(char c: expectedLineContent.toCharArray()) {
            assertEquals(DEFAULT_CHARACTER.withCharacter(c), virtualTerminal.getCharacter(column++, rowNumber));
            if(TerminalTextUtils.isCharDoubleWidth(c)) {
                column++;
            }
        }
        while(column < virtualTerminal.getTerminalSize().getColumns()) {
            assertEquals(DEFAULT_CHARACTER, virtualTerminal.getCharacter(column++, rowNumber));
        }
    }

    private void assertBufferLineEquals(String expectedBufferLineContent, int rowNumber) {
        int column = 0;
        for(char c: expectedBufferLineContent.toCharArray()) {
            assertEquals(DEFAULT_CHARACTER.withCharacter(c), virtualTerminal.getBufferCharacter(new TerminalPosition(column++, rowNumber)));
            if(TerminalTextUtils.isCharDoubleWidth(c)) {
                column++;
            }
        }
        while(column < virtualTerminal.getTerminalSize().getColumns()) {
            assertEquals(DEFAULT_CHARACTER, virtualTerminal.getBufferCharacter(column++, rowNumber));
        }
    }

    private void assertLineEquals(String expectedLineContent, VirtualTerminal.BufferLine line) {
        int column = 0;
        for(char c: expectedLineContent.toCharArray()) {
            assertEquals(DEFAULT_CHARACTER.withCharacter(c), line.getCharacterAt(column++));
            if(TerminalTextUtils.isCharDoubleWidth(c)) {
                column++;
            }
        }
        while(column < virtualTerminal.getTerminalSize().getColumns()) {
            assertEquals(DEFAULT_CHARACTER, line.getCharacterAt(column++));
        }
    }
}