/*
    SPDX-FileCopyrightText: 2018 Kurt Hindenburg <kurt.hindenburg@gmail.com>

    SPDX-License-Identifier: GPL-2.0-or-later
*/

// Own
#include "Vt102EmulationTest.h"

#include <QSignalSpy>
#include <QTest>

// The below is to verify the old #defines match the new constexprs
// Just copy/paste for now from Vt102Emulation.cpp
/* clang-format off */
#define TY_CONSTRUCT(T, A, N) (((((int)(N)) & 0xffff) << 16) | ((((int)(A)) & 0xff) << 8) | (((int)(T)) & 0xff))
#define TY_CHR()        TY_CONSTRUCT(0, 0, 0)
#define TY_CTL(A)       TY_CONSTRUCT(1, A, 0)
#define TY_ESC(A)       TY_CONSTRUCT(2, A, 0)
#define TY_ESC_CS(A, B) TY_CONSTRUCT(3, A, B)
#define TY_ESC_DE(A)    TY_CONSTRUCT(4, A, 0)
#define TY_CSI_PS(A, N) TY_CONSTRUCT(5, A, N)
#define TY_CSI_PN(A)    TY_CONSTRUCT(6, A, 0)
#define TY_CSI_PR(A, N) TY_CONSTRUCT(7, A, N)
#define TY_VT52(A)      TY_CONSTRUCT(8, A, 0)
#define TY_CSI_PG(A)    TY_CONSTRUCT(9, A, 0)
#define TY_CSI_PE(A)    TY_CONSTRUCT(10, A, 0)
/* clang-format on */

using namespace Konsole;

constexpr int token_construct(int t, int a, int n)
{
    return (((n & 0xffff) << 16) | ((a & 0xff) << 8) | (t & 0xff));
}
constexpr int token_chr()
{
    return token_construct(0, 0, 0);
}
constexpr int token_ctl(int a)
{
    return token_construct(1, a, 0);
}
constexpr int token_esc(int a)
{
    return token_construct(2, a, 0);
}
constexpr int token_esc_cs(int a, int b)
{
    return token_construct(3, a, b);
}
constexpr int token_esc_de(int a)
{
    return token_construct(4, a, 0);
}
constexpr int token_csi_ps(int a, int n)
{
    return token_construct(5, a, n);
}
constexpr int token_csi_pn(int a)
{
    return token_construct(6, a, 0);
}
constexpr int token_csi_pr(int a, int n)
{
    return token_construct(7, a, n);
}
constexpr int token_vt52(int a)
{
    return token_construct(8, a, 0);
}
constexpr int token_csi_pg(int a)
{
    return token_construct(9, a, 0);
}
constexpr int token_csi_pe(int a)
{
    return token_construct(10, a, 0);
}
constexpr int token_csi_sp(int a)
{
    return token_construct(11, a, 0);
}
constexpr int token_csi_psp(int a, int n)
{
    return token_construct(12, a, n);
}
constexpr int token_csi_pq(int a)
{
    return token_construct(13, a, 0);
}

void Vt102EmulationTest::sendAndCompare(TestEmulation *em, const char *input, size_t inputLen, const QString &expectedPrint, const QByteArray &expectedSent)
{
    em->_currentScreen->clearEntireScreen();

    em->receiveData(input, inputLen);
    QString printed = em->_currentScreen->text(0, em->_currentScreen->getColumns(), Screen::PlainText);
    printed.chop(2); // Remove trailing space and newline
    QCOMPARE(printed, expectedPrint);
    QCOMPARE(em->lastSent, expectedSent);
}

void Vt102EmulationTest::testParse()
{
    TestEmulation em;
    em.reset();
    em.setCodec(TestEmulation::Utf8Codec);
    Q_ASSERT(em._currentScreen != nullptr);

    sendAndCompare(&em, "a", 1, QStringLiteral("a"), "");

    const char tertiaryDeviceAttributes[] = {0x1b, '[', '=', '0', 'c'};
    sendAndCompare(&em, tertiaryDeviceAttributes, sizeof tertiaryDeviceAttributes, QStringLiteral(""), "\033P!|7E4B4445\033\\");
}

Q_DECLARE_METATYPE(std::vector<TestEmulation::Item>)

struct ItemToString {
    QString operator()(const TestEmulation::ProcessToken &item)
    {
        return QStringLiteral("processToken(0x%0, %1, %2)").arg(QString::number(item.code, 16), QString::number(item.p), QString::number(item.q));
    }

    QString operator()(TestEmulation::ProcessSessionAttributeRequest)
    {
        return QStringLiteral("ProcessSessionAttributeRequest");
    }

    QString operator()(TestEmulation::ProcessChecksumRequest)
    {
        return QStringLiteral("ProcessChecksumRequest");
    }

    QString operator()(TestEmulation::DecodingError)
    {
        return QStringLiteral("DecodingError");
    }
};

namespace QTest
{
template<>
char *toString(const std::vector<TestEmulation::Item> &items)
{
    QStringList res;
    for (auto item : items) {
        res.append(std::visit(ItemToString{}, item));
    }
    return toString(res.join(QStringLiteral(",")));
}
}

void Vt102EmulationTest::testTokenizing_data()
{
    QTest::addColumn<QVector<uint>>("input");
    QTest::addColumn<std::vector<TestEmulation::Item>>("expectedItems");

    using ProcessToken = TestEmulation::ProcessToken;

    using C = QVector<uint>;
    using I = std::vector<TestEmulation::Item>;

    /* clang-format off */
    QTest::newRow("NUL") << C{'@' - '@'} << I{ProcessToken{token_ctl('@'), 0, 0}};
    QTest::newRow("SOH") << C{'A' - '@'} << I{ProcessToken{token_ctl('A'), 0, 0}};
    QTest::newRow("STX") << C{'B' - '@'} << I{ProcessToken{token_ctl('B'), 0, 0}};
    QTest::newRow("ETX") << C{'C' - '@'} << I{ProcessToken{token_ctl('C'), 0, 0}};
    QTest::newRow("EOT") << C{'D' - '@'} << I{ProcessToken{token_ctl('D'), 0, 0}};
    QTest::newRow("ENQ") << C{'E' - '@'} << I{ProcessToken{token_ctl('E'), 0, 0}};
    QTest::newRow("ACK") << C{'F' - '@'} << I{ProcessToken{token_ctl('F'), 0, 0}};
    QTest::newRow("BEL") << C{'G' - '@'} << I{ProcessToken{token_ctl('G'), 0, 0}};
    QTest::newRow("BS")  << C{'H' - '@'} << I{ProcessToken{token_ctl('H'), 0, 0}};
    QTest::newRow("TAB") << C{'I' - '@'} << I{ProcessToken{token_ctl('I'), 0, 0}};
    QTest::newRow("LF")  << C{'J' - '@'} << I{ProcessToken{token_ctl('J'), 0, 0}};
    QTest::newRow("VT")  << C{'K' - '@'} << I{ProcessToken{token_ctl('K'), 0, 0}};
    QTest::newRow("FF")  << C{'L' - '@'} << I{ProcessToken{token_ctl('L'), 0, 0}};
    QTest::newRow("CR")  << C{'M' - '@'} << I{ProcessToken{token_ctl('M'), 0, 0}};
    QTest::newRow("SO")  << C{'N' - '@'} << I{ProcessToken{token_ctl('N'), 0, 0}};
    QTest::newRow("SI")  << C{'O' - '@'} << I{ProcessToken{token_ctl('O'), 0, 0}};

    QTest::newRow("DLE") << C{'P' - '@'} << I{ProcessToken{token_ctl('P'), 0, 0}};
    QTest::newRow("XON") << C{'Q' - '@'} << I{ProcessToken{token_ctl('Q'), 0, 0}};
    QTest::newRow("DC2") << C{'R' - '@'} << I{ProcessToken{token_ctl('R'), 0, 0}};
    QTest::newRow("XOFF") << C{'S' - '@'} << I{ProcessToken{token_ctl('S'), 0, 0}};
    QTest::newRow("DC4") << C{'T' - '@'} << I{ProcessToken{token_ctl('T'), 0, 0}};
    QTest::newRow("NAK") << C{'U' - '@'} << I{ProcessToken{token_ctl('U'), 0, 0}};
    QTest::newRow("SYN") << C{'V' - '@'} << I{ProcessToken{token_ctl('V'), 0, 0}};
    QTest::newRow("ETB") << C{'W' - '@'} << I{ProcessToken{token_ctl('W'), 0, 0}};
    QTest::newRow("CAN") << C{'X' - '@'} << I{ProcessToken{token_ctl('X'), 0, 0}};
    QTest::newRow("EM")  << C{'Y' - '@'} << I{ProcessToken{token_ctl('Y'), 0, 0}};
    QTest::newRow("SUB") << C{'Z' - '@'} << I{ProcessToken{token_ctl('Z'), 0, 0}};
    // [ is ESC and parsed differently
    QTest::newRow("FS")  << C{'\\' - '@'} << I{ProcessToken{token_ctl('\\'), 0, 0}};
    QTest::newRow("GS")  << C{']' - '@'} << I{ProcessToken{token_ctl(']'), 0, 0}};
    QTest::newRow("RS")  << C{'^' - '@'} << I{ProcessToken{token_ctl('^'), 0, 0}};
    QTest::newRow("US")  << C{'_' - '@'} << I{ProcessToken{token_ctl('_'), 0, 0}};

    QTest::newRow("DEL") << C{127} << I{};

    const uint ESC = '\033';

    QTest::newRow("ESC 7") << C{ESC, '7'} << I{ProcessToken{token_esc('7'), 0, 0}};
    QTest::newRow("ESC 8") << C{ESC, '8'} << I{ProcessToken{token_esc('8'), 0, 0}};
    QTest::newRow("ESC D") << C{ESC, 'D'} << I{ProcessToken{token_esc('D'), 0, 0}};
    QTest::newRow("ESC E") << C{ESC, 'E'} << I{ProcessToken{token_esc('E'), 0, 0}};
    QTest::newRow("ESC H") << C{ESC, 'H'} << I{ProcessToken{token_esc('H'), 0, 0}};
    QTest::newRow("ESC M") << C{ESC, 'M'} << I{ProcessToken{token_esc('M'), 0, 0}};
    QTest::newRow("ESC Z") << C{ESC, 'Z'} << I{ProcessToken{token_esc('Z'), 0, 0}};

    QTest::newRow("ESC c") << C{ESC, 'c'} << I{ProcessToken{token_esc('c'), 0, 0}};

    QTest::newRow("ESC n") << C{ESC, 'n'} << I{ProcessToken{token_esc('n'), 0, 0}};
    QTest::newRow("ESC o") << C{ESC, 'o'} << I{ProcessToken{token_esc('o'), 0, 0}};
    QTest::newRow("ESC >") << C{ESC, '>'} << I{ProcessToken{token_esc('>'), 0, 0}};
    QTest::newRow("ESC <") << C{ESC, '<'} << I{ProcessToken{token_esc('<'), 0, 0}};
    QTest::newRow("ESC =") << C{ESC, '='} << I{ProcessToken{token_esc('='), 0, 0}};

    QTest::newRow("ESC #3") << C{ESC, '#', '3'} << I{ProcessToken{token_esc_de('3'), 0, 0}};
    QTest::newRow("ESC #4") << C{ESC, '#', '4'} << I{ProcessToken{token_esc_de('4'), 0, 0}};
    QTest::newRow("ESC #5") << C{ESC, '#', '5'} << I{ProcessToken{token_esc_de('5'), 0, 0}};
    QTest::newRow("ESC #6") << C{ESC, '#', '6'} << I{ProcessToken{token_esc_de('6'), 0, 0}};
    QTest::newRow("ESC #8") << C{ESC, '#', '8'} << I{ProcessToken{token_esc_de('8'), 0, 0}};

    QTest::newRow("ESC %G") << C{ESC, '%', 'G'} << I{ProcessToken{token_esc_cs('%', 'G'), 0, 0}};
    QTest::newRow("ESC %@") << C{ESC, '%', '@'} << I{ProcessToken{token_esc_cs('%', '@'), 0, 0}};

    QTest::newRow("ESC (0") << C{ESC, '(', '0'} << I{ProcessToken{token_esc_cs('(', '0'), 0, 0}};
    QTest::newRow("ESC (A") << C{ESC, '(', 'A'} << I{ProcessToken{token_esc_cs('(', 'A'), 0, 0}};
    QTest::newRow("ESC (B") << C{ESC, '(', 'B'} << I{ProcessToken{token_esc_cs('(', 'B'), 0, 0}};

    QTest::newRow("ESC )0") << C{ESC, ')', '0'} << I{ProcessToken{token_esc_cs(')', '0'), 0, 0}};
    QTest::newRow("ESC )A") << C{ESC, ')', 'A'} << I{ProcessToken{token_esc_cs(')', 'A'), 0, 0}};
    QTest::newRow("ESC )B") << C{ESC, ')', 'B'} << I{ProcessToken{token_esc_cs(')', 'B'), 0, 0}};

    QTest::newRow("ESC *0") << C{ESC, '*', '0'} << I{ProcessToken{token_esc_cs('*', '0'), 0, 0}};
    QTest::newRow("ESC *A") << C{ESC, '*', 'A'} << I{ProcessToken{token_esc_cs('*', 'A'), 0, 0}};
    QTest::newRow("ESC *B") << C{ESC, '*', 'B'} << I{ProcessToken{token_esc_cs('*', 'B'), 0, 0}};

    QTest::newRow("ESC +0") << C{ESC, '+', '0'} << I{ProcessToken{token_esc_cs('+', '0'), 0, 0}};
    QTest::newRow("ESC +A") << C{ESC, '+', 'A'} << I{ProcessToken{token_esc_cs('+', 'A'), 0, 0}};
    QTest::newRow("ESC +B") << C{ESC, '+', 'B'} << I{ProcessToken{token_esc_cs('+', 'B'), 0, 0}};

    QTest::newRow("ESC [8;12;45t") << C{ESC, '[', '8', ';', '1', '2', ';', '4', '5', 't'} << I{ProcessToken{token_csi_ps('t', 8), 12, 45}};
    QTest::newRow("ESC [18t")      << C{ESC, '[', '1', '8', 't'} << I{ProcessToken{token_csi_ps('t', 18), 0, 0}};
    QTest::newRow("ESC [18;1;2t")  << C{ESC, '[', '1', '8', ';', '1', ';', '2', 't'} << I{ProcessToken{token_csi_ps('t', 18), 1, 2}};

    QTest::newRow("ESC [K")  << C{ESC, '[', 'K'} << I{ProcessToken{token_csi_ps('K', 0), 0, 0}};
    QTest::newRow("ESC [0K") << C{ESC, '[', '0', 'K'} << I{ProcessToken{token_csi_ps('K', 0), 0, 0}};
    QTest::newRow("ESC [1K") << C{ESC, '[', '1', 'K'} << I{ProcessToken{token_csi_ps('K', 1), 0, 0}};

    QTest::newRow("ESC [@")   << C{ESC, '[', '@'} << I{ProcessToken{token_csi_pn('@'), 0, 0}};
    QTest::newRow("ESC [12@") << C{ESC, '[', '1', '2', '@'} << I{ProcessToken{token_csi_pn('@'), 12, 0}};
    QTest::newRow("ESC [H")   << C{ESC, '[', 'H'} << I{ProcessToken{token_csi_pn('H'), 0, 0}};
    QTest::newRow("ESC [24H") << C{ESC, '[', '2', '4', 'H'} << I{ProcessToken{token_csi_pn('H'), 24, 0}};
    QTest::newRow("ESC [32,13H") << C{ESC, '[', '3', '2', ';', '1', '3', 'H'} << I{ProcessToken{token_csi_pn('H'), 32, 13}};

    QTest::newRow("ESC [m")   << C{ESC, '[', 'm'} << I{ProcessToken{token_csi_ps('m', 0), 0, 0}};
    QTest::newRow("ESC [1m")  << C{ESC, '[', '1', 'm'} << I{ProcessToken{token_csi_ps('m', 1), 0, 0}};
    QTest::newRow("ESC [1;2m") << C{ESC, '[', '1', ';', '2', 'm'} << I{ProcessToken{token_csi_ps('m', 1), 0, 0}, ProcessToken{token_csi_ps('m', 2), 0, 0}};
    QTest::newRow("ESC [38;2;193;202;218m") << C{ESC, '[', '3', '8', ';', '2', ';', '1', '9', '3', ';', '2', '0', '2', ';', '2', '1', '8', 'm'}
                                            << I{ProcessToken{token_csi_ps('m', 38), 4, 0xC1CADA}};
    QTest::newRow("ESC [38;2;193;202;218;2m") << C{ESC, '[', '3', '8', ';', '2', ';', '1', '9', '3', ';', '2', '0', '2', ';', '2', '1', '8', ';', '2', 'm'}
                                              << I{ProcessToken{token_csi_ps('m', 38), 4, 0xC1CADA}, ProcessToken{token_csi_ps('m', 2), 0, 0}};
    QTest::newRow("ESC [38:2:193:202:218m") << C{ESC, '[', '3', '8', ':', '2', ':', '1', '9', '3', ':', '2', '0', '2', ':', '2', '1', '8', 'm'}
                                            << I{ProcessToken{token_csi_ps('m', 38), 4, 0xC1CADA}};
    QTest::newRow("ESC [38:2:193:202:218;2m") << C{ESC, '[', '3', '8', ':', '2', ':', '1', '9', '3', ':', '2', '0', '2', ':', '2', '1', '8', ';', '2', 'm'}
                                              << I{ProcessToken{token_csi_ps('m', 38), 4, 0xC1CADA}, ProcessToken{token_csi_ps('m', 2), 0, 0}};
    QTest::newRow("ESC [38:2:1:193:202:218m") << C{ESC, '[', '3', '8', ':', '2', ':', '1', ':', '1', '9', '3', ':', '2', '0', '2', ':', '2', '1', '8', 'm'}
                                              << I{ProcessToken{token_csi_ps('m', 38), 4, 0xC1CADA}};
    QTest::newRow("ESC [38;5;255;2m") << C{ESC, '[', '3', '8', ';', '5', ';', '2', '5', '5', ';', '2', 'm'}
                                      << I{ProcessToken{token_csi_ps('m', 38), 3, 255}, ProcessToken{token_csi_ps('m', 2), 0, 0}};
    QTest::newRow("ESC [38:5:255m") << C{ESC, '[', '3', '8', ':', '5', ':', '2', '5', '5', 'm'}
                                    << I{ProcessToken{token_csi_ps('m', 38), 3, 255}};

    QTest::newRow("ESC [5n")  << C{ESC, '[', '5', 'n'} << I{ProcessToken{token_csi_ps('n', 5), 0, 0}};

    QTest::newRow("ESC [?1h") << C{ESC, '[', '?', '1', 'h'} << I{ProcessToken{token_csi_pr('h', 1), 0, 0}};
    QTest::newRow("ESC [?1l") << C{ESC, '[', '?', '1', 'l'} << I{ProcessToken{token_csi_pr('l', 1), 0, 0}};
    QTest::newRow("ESC [?1r") << C{ESC, '[', '?', '1', 'r'} << I{ProcessToken{token_csi_pr('r', 1), 0, 0}};
    QTest::newRow("ESC [?1s") << C{ESC, '[', '?', '1', 's'} << I{ProcessToken{token_csi_pr('s', 1), 0, 0}};

    QTest::newRow("ESC [?1;2h") << C{ESC, '[', '?', '1', ';', '2', 'h'}
                                << I{ProcessToken{token_csi_pr('h', 1), 0, 0}, ProcessToken{token_csi_pr('h', 2), 1, 0}};
    QTest::newRow("ESC [?1;2l") << C{ESC, '[', '?', '1', ';', '2', 'l'}
                                << I{ProcessToken{token_csi_pr('l', 1), 0, 0}, ProcessToken{token_csi_pr('l', 2), 1, 0}};
    QTest::newRow("ESC [?1;2r") << C{ESC, '[', '?', '1', ';', '2', 'r'}
                                << I{ProcessToken{token_csi_pr('r', 1), 0, 0}, ProcessToken{token_csi_pr('r', 2), 1, 0}};
    QTest::newRow("ESC [?1;2s") << C{ESC, '[', '?', '1', ';', '2', 's'}
                                << I{ProcessToken{token_csi_pr('s', 1), 0, 0}, ProcessToken{token_csi_pr('s', 2), 1, 0}};

    QTest::newRow("ESC [? q")  << C{ESC, '[', ' ', 'q'} << I{ProcessToken{token_csi_sp('q'), 0, 0}};
    QTest::newRow("ESC [? 1q") << C{ESC, '[', '1', ' ', 'q'} << I{ProcessToken{token_csi_psp('q', 1), 0, 0}};

    QTest::newRow("ESC [!p") << C{ESC, '[', '!', 'p'} << I{ProcessToken{token_csi_pe('p'), 0, 0}};
    QTest::newRow("ESC [=c") << C{ESC, '[', '=', 'p'} << I{ProcessToken{token_csi_pq('p'), 0, 0}};
    QTest::newRow("ESC [>c") << C{ESC, '[', '>', 'p'} << I{ProcessToken{token_csi_pg('p'), 0, 0}};
    /* clang-format on */
}

void Vt102EmulationTest::testTokenizing()
{
    QFETCH(QVector<uint>, input);
    QFETCH(std::vector<TestEmulation::Item>, expectedItems);

    TestEmulation em;
    em.reset();
    em.blockFurtherProcessing = true;

    em._currentScreen->clearEntireScreen();

    em.receiveChars(input);
    QString printed = em._currentScreen->text(0, em._currentScreen->getColumns(), Screen::PlainText);
    printed.chop(2); // Remove trailing space and newline

    QCOMPARE(printed, QStringLiteral(""));
    QCOMPARE(em.items, expectedItems);
}

void Vt102EmulationTest::testTokenizingVT52_data()
{
    QTest::addColumn<QVector<uint>>("input");
    QTest::addColumn<std::vector<TestEmulation::Item>>("expectedItems");

    using ProcessToken = TestEmulation::ProcessToken;

    using C = QVector<uint>;
    using I = std::vector<TestEmulation::Item>;

    /* clang-format off */
    QTest::newRow("NUL") << C{'@' - '@'} << I{ProcessToken{token_ctl('@'), 0, 0}};
    QTest::newRow("SOH") << C{'A' - '@'} << I{ProcessToken{token_ctl('A'), 0, 0}};
    QTest::newRow("STX") << C{'B' - '@'} << I{ProcessToken{token_ctl('B'), 0, 0}};
    QTest::newRow("ETX") << C{'C' - '@'} << I{ProcessToken{token_ctl('C'), 0, 0}};
    QTest::newRow("EOT") << C{'D' - '@'} << I{ProcessToken{token_ctl('D'), 0, 0}};
    QTest::newRow("ENQ") << C{'E' - '@'} << I{ProcessToken{token_ctl('E'), 0, 0}};
    QTest::newRow("ACK") << C{'F' - '@'} << I{ProcessToken{token_ctl('F'), 0, 0}};
    QTest::newRow("BEL") << C{'G' - '@'} << I{ProcessToken{token_ctl('G'), 0, 0}};
    QTest::newRow("BS")  << C{'H' - '@'} << I{ProcessToken{token_ctl('H'), 0, 0}};
    QTest::newRow("TAB") << C{'I' - '@'} << I{ProcessToken{token_ctl('I'), 0, 0}};
    QTest::newRow("LF")  << C{'J' - '@'} << I{ProcessToken{token_ctl('J'), 0, 0}};
    QTest::newRow("VT")  << C{'K' - '@'} << I{ProcessToken{token_ctl('K'), 0, 0}};
    QTest::newRow("FF")  << C{'L' - '@'} << I{ProcessToken{token_ctl('L'), 0, 0}};
    QTest::newRow("CR")  << C{'M' - '@'} << I{ProcessToken{token_ctl('M'), 0, 0}};
    QTest::newRow("SO")  << C{'N' - '@'} << I{ProcessToken{token_ctl('N'), 0, 0}};
    QTest::newRow("SI")  << C{'O' - '@'} << I{ProcessToken{token_ctl('O'), 0, 0}};

    QTest::newRow("DLE") << C{'P' - '@'} << I{ProcessToken{token_ctl('P'), 0, 0}};
    QTest::newRow("XON") << C{'Q' - '@'} << I{ProcessToken{token_ctl('Q'), 0, 0}};
    QTest::newRow("DC2") << C{'R' - '@'} << I{ProcessToken{token_ctl('R'), 0, 0}};
    QTest::newRow("XOFF") << C{'S' - '@'} << I{ProcessToken{token_ctl('S'), 0, 0}};
    QTest::newRow("DC4") << C{'T' - '@'} << I{ProcessToken{token_ctl('T'), 0, 0}};
    QTest::newRow("NAK") << C{'U' - '@'} << I{ProcessToken{token_ctl('U'), 0, 0}};
    QTest::newRow("SYN") << C{'V' - '@'} << I{ProcessToken{token_ctl('V'), 0, 0}};
    QTest::newRow("ETB") << C{'W' - '@'} << I{ProcessToken{token_ctl('W'), 0, 0}};
    QTest::newRow("CAN") << C{'X' - '@'} << I{ProcessToken{token_ctl('X'), 0, 0}};
    QTest::newRow("EM")  << C{'Y' - '@'} << I{ProcessToken{token_ctl('Y'), 0, 0}};
    QTest::newRow("SUB") << C{'Z' - '@'} << I{ProcessToken{token_ctl('Z'), 0, 0}};
    // [ is ESC and parsed differently
    QTest::newRow("FS")  << C{'\\' - '@'} << I{ProcessToken{token_ctl('\\'), 0, 0}};
    QTest::newRow("GS")  << C{']' - '@'} << I{ProcessToken{token_ctl(']'), 0, 0}};
    QTest::newRow("RS")  << C{'^' - '@'} << I{ProcessToken{token_ctl('^'), 0, 0}};
    QTest::newRow("US")  << C{'_' - '@'} << I{ProcessToken{token_ctl('_'), 0, 0}};

    QTest::newRow("DEL") << C{127} << I{};

    const uint ESC = '\033';

    QTest::newRow("ESC A") << C{ESC, 'A'} << I{ProcessToken{token_vt52('A'), 0, 0}};
    QTest::newRow("ESC B") << C{ESC, 'B'} << I{ProcessToken{token_vt52('B'), 0, 0}};
    QTest::newRow("ESC C") << C{ESC, 'C'} << I{ProcessToken{token_vt52('C'), 0, 0}};
    QTest::newRow("ESC D") << C{ESC, 'D'} << I{ProcessToken{token_vt52('D'), 0, 0}};
    QTest::newRow("ESC F") << C{ESC, 'F'} << I{ProcessToken{token_vt52('F'), 0, 0}};
    QTest::newRow("ESC G") << C{ESC, 'G'} << I{ProcessToken{token_vt52('G'), 0, 0}};
    QTest::newRow("ESC H") << C{ESC, 'H'} << I{ProcessToken{token_vt52('H'), 0, 0}};
    QTest::newRow("ESC I") << C{ESC, 'I'} << I{ProcessToken{token_vt52('I'), 0, 0}};
    QTest::newRow("ESC J") << C{ESC, 'J'} << I{ProcessToken{token_vt52('J'), 0, 0}};
    QTest::newRow("ESC K") << C{ESC, 'K'} << I{ProcessToken{token_vt52('K'), 0, 0}};
    QTest::newRow("ESC Yab") << C{ESC, 'Y', 'a', 'b'} << I{ProcessToken{token_vt52('Y'), 'a', 'b'}};
    QTest::newRow("ESC Z") << C{ESC, 'Z'} << I{ProcessToken{token_vt52('Z'), 0, 0}};
    QTest::newRow("ESC <") << C{ESC, '<'} << I{ProcessToken{token_vt52('<'), 0, 0}};
    QTest::newRow("ESC =") << C{ESC, '='} << I{ProcessToken{token_vt52('='), 0, 0}};
    QTest::newRow("ESC >") << C{ESC, '>'} << I{ProcessToken{token_vt52('>'), 0, 0}};
    /* clang-format on */
}

void Vt102EmulationTest::testTokenizingVT52()
{
    QFETCH(QVector<uint>, input);
    QFETCH(std::vector<TestEmulation::Item>, expectedItems);

    TestEmulation em;
    em.reset();
    em.resetMode(MODE_Ansi);
    em.blockFurtherProcessing = true;

    em._currentScreen->clearEntireScreen();

    em.receiveChars(input);
    QString printed = em._currentScreen->text(0, em._currentScreen->getColumns(), Screen::PlainText);
    printed.chop(2); // Remove trailing space and newline

    QCOMPARE(printed, QStringLiteral(""));
    QCOMPARE(em.items, expectedItems);
}

void Vt102EmulationTest::testTokenFunctions()
{
    QCOMPARE(token_construct(0, 0, 0), TY_CONSTRUCT(0, 0, 0));
    QCOMPARE(token_chr(), TY_CHR());
    QCOMPARE(token_ctl(8 + '@'), TY_CTL(8 + '@'));
    QCOMPARE(token_ctl('G'), TY_CTL('G'));
    QCOMPARE(token_csi_pe('p'), TY_CSI_PE('p'));
    QCOMPARE(token_csi_pg('c'), TY_CSI_PG('c'));
    QCOMPARE(token_csi_pn(8), TY_CSI_PN(8));
    QCOMPARE(token_csi_pn('N'), TY_CSI_PN('N'));
    QCOMPARE(token_csi_pr('r', 2), TY_CSI_PR('r', 2));
    QCOMPARE(token_csi_pr('s', 1000), TY_CSI_PR('s', 1000));
    QCOMPARE(token_csi_ps('m', 8), TY_CSI_PS('m', 8));
    QCOMPARE(token_csi_ps('m', 48), TY_CSI_PS('m', 48));
    QCOMPARE(token_csi_ps('K', 2), TY_CSI_PS('K', 2));
    QCOMPARE(token_esc(8), TY_ESC(8));
    QCOMPARE(token_esc('='), TY_ESC('='));
    QCOMPARE(token_esc('>'), TY_ESC('>'));
    QCOMPARE(token_esc_cs(8, 0), TY_ESC_CS(8, 0));
    QCOMPARE(token_esc_cs('(', '0'), TY_ESC_CS('(', '0'));
    QCOMPARE(token_esc_cs(')', 'B'), TY_ESC_CS(')', 'B'));
    QCOMPARE(token_esc_de(8), TY_ESC_DE(8));
    QCOMPARE(token_esc_de('3'), TY_ESC_DE('3'));
    QCOMPARE(token_vt52('A'), TY_VT52('A'));
    QCOMPARE(token_vt52('Z'), TY_VT52('Z'));
    QCOMPARE(token_vt52('='), TY_VT52('='));
    QCOMPARE(token_vt52('>'), TY_VT52('>'));
}

void Vt102EmulationTest::testBufferedUpdates()
{
    TestEmulation em;
    em.reset();

    QSignalSpy outputChangedSpy(&em, &TestEmulation::outputChanged);

    // Test the normal buffered update behaviour.
    em.receiveChars({'h', 'e', 'l', 'l', 'o', '!'});

    QCOMPARE(outputChangedSpy.count(), 0);
    QVERIFY(outputChangedSpy.wait(15));
    outputChangedSpy.clear();

    const uint ESC = '\033';

    // Test that synchronized updates can time out.
    em.receiveChars({ESC, '[', '?', '2', '0', '2', '6', 'h'});

    QVERIFY(!outputChangedSpy.wait(800));
    QVERIFY(outputChangedSpy.wait(300));
    outputChangedSpy.clear();

    // Test that synchronized updates work.
    em.receiveChars({ESC, '[', '?', '2', '0', '2', '6', 'h'});
    em.receiveChars({ESC, '[', '?', '2', '0', '2', '6', 'l'});

    QCOMPARE(outputChangedSpy.count(), 2);
}

void Vt102EmulationTest::testKittyKeyboardPushPopQuery()
{
    TestEmulation em;
    em.reset();
    em.setCodec(TestEmulation::Utf8Codec);

    // Initially, flags should be 0
    // Query: CSI ? u  → should respond CSI ? 0 u
    const char query[] = "\033[?u";
    em.receiveData(query, sizeof(query) - 1);
    QCOMPARE(em.lastSent, QByteArray("\033[?0u"));

    // Push flags=1 (disambiguate): CSI > 1 u
    const char push1[] = "\033[>1u";
    em.receiveData(push1, sizeof(push1) - 1);

    // Query again — should be 1
    em.receiveData(query, sizeof(query) - 1);
    QCOMPARE(em.lastSent, QByteArray("\033[?1u"));

    // Push flags=3: CSI > 3 u
    const char push3[] = "\033[>3u";
    em.receiveData(push3, sizeof(push3) - 1);

    // Query — should be 3 (top of stack)
    em.receiveData(query, sizeof(query) - 1);
    QCOMPARE(em.lastSent, QByteArray("\033[?3u"));

    // Pop 1 entry: CSI < 1 u
    const char pop1[] = "\033[<1u";
    em.receiveData(pop1, sizeof(pop1) - 1);

    // Query — should be back to 1
    em.receiveData(query, sizeof(query) - 1);
    QCOMPARE(em.lastSent, QByteArray("\033[?1u"));

    // Pop 1 more entry
    em.receiveData(pop1, sizeof(pop1) - 1);

    // Query — should be 0 (empty stack)
    em.receiveData(query, sizeof(query) - 1);
    QCOMPARE(em.lastSent, QByteArray("\033[?0u"));

    // Pop from empty stack — should be harmless
    em.receiveData(pop1, sizeof(pop1) - 1);
    em.receiveData(query, sizeof(query) - 1);
    QCOMPARE(em.lastSent, QByteArray("\033[?0u"));
}

void Vt102EmulationTest::testKittyKeyboardSet()
{
    TestEmulation em;
    em.reset();
    em.setCodec(TestEmulation::Utf8Codec);

    const char query[] = "\033[?u";

    // Push initial flags=0
    const char push0[] = "\033[>0u";
    em.receiveData(push0, sizeof(push0) - 1);

    // Set flags=5, mode=1 (replace): CSI = 5 ; 1 u
    const char set_replace[] = "\033[=5;1u";
    em.receiveData(set_replace, sizeof(set_replace) - 1);
    em.receiveData(query, sizeof(query) - 1);
    QCOMPARE(em.lastSent, QByteArray("\033[?5u"));

    // Set flags=2, mode=2 (OR): CSI = 2 ; 2 u → 5 | 2 = 7
    const char set_or[] = "\033[=2;2u";
    em.receiveData(set_or, sizeof(set_or) - 1);
    em.receiveData(query, sizeof(query) - 1);
    QCOMPARE(em.lastSent, QByteArray("\033[?7u"));

    // Set flags=4, mode=3 (AND-NOT): CSI = 4 ; 3 u → 7 & ~4 = 3
    const char set_andnot[] = "\033[=4;3u";
    em.receiveData(set_andnot, sizeof(set_andnot) - 1);
    em.receiveData(query, sizeof(query) - 1);
    QCOMPARE(em.lastSent, QByteArray("\033[?3u"));
}

void Vt102EmulationTest::testKittyKeyboardReset()
{
    TestEmulation em;
    em.reset();
    em.setCodec(TestEmulation::Utf8Codec);

    const char query[] = "\033[?u";

    // Push some flags
    const char push5[] = "\033[>5u";
    em.receiveData(push5, sizeof(push5) - 1);
    em.receiveData(query, sizeof(query) - 1);
    QCOMPARE(em.lastSent, QByteArray("\033[?5u"));

    // Reset should clear stacks
    em.reset();
    em.setCodec(TestEmulation::Utf8Codec);
    em.receiveData(query, sizeof(query) - 1);
    QCOMPARE(em.lastSent, QByteArray("\033[?0u"));
}

void Vt102EmulationTest::testKittyKeyboardDisambiguate()
{
    TestEmulation em;
    em.reset();
    em.setCodec(TestEmulation::Utf8Codec);

    // Push flags=1 (disambiguate)
    const char push1[] = "\033[>1u";
    em.receiveData(push1, sizeof(push1) - 1);

    // Escape key with Ctrl modifier should use CSI u encoding
    QKeyEvent escCtrl(QEvent::KeyPress, Qt::Key_Escape, Qt::ControlModifier, QStringLiteral("\x1b"));
    em.sendKeyEvent(&escCtrl);
    // Should send CSI 27;5u (modifier 5 = 1 + ctrl:4)
    QCOMPARE(em.lastSent, QByteArray("\033[27;5u"));
}

void Vt102EmulationTest::testKittyKeyboardEventTypes()
{
    TestEmulation em;
    em.reset();
    em.setCodec(TestEmulation::Utf8Codec);

    // Push flags=3 (disambiguate + report event types)
    const char push3[] = "\033[>3u";
    em.receiveData(push3, sizeof(push3) - 1);

    // Key release event for 'a'
    QKeyEvent release(QEvent::KeyRelease, Qt::Key_A, Qt::NoModifier, QStringLiteral("a"));
    em.sendKeyEvent(&release);
    // Should include event type 3 (release): CSI 97;1:3u
    QCOMPARE(em.lastSent, QByteArray("\033[97;1:3u"));
}

void Vt102EmulationTest::testKittyKeyboardReportAllKeys()
{
    TestEmulation em;
    em.reset();
    em.setCodec(TestEmulation::Utf8Codec);

    // Push flags=9 (disambiguate + report all keys as escape codes)
    const char push9[] = "\033[>9u";
    em.receiveData(push9, sizeof(push9) - 1);

    // Plain 'a' key should be reported
    QKeyEvent press(QEvent::KeyPress, Qt::Key_A, Qt::NoModifier, QStringLiteral("a"));
    em.sendKeyEvent(&press);
    // Should send CSI 97 u (keycode=97='a', no mods)
    QCOMPARE(em.lastSent, QByteArray("\033[97u"));

    // Escape key (no modifiers) with flag 8 should use CSI u
    QKeyEvent escPress(QEvent::KeyPress, Qt::Key_Escape, Qt::NoModifier, QStringLiteral("\x1b"));
    em.sendKeyEvent(&escPress);
    QCOMPARE(em.lastSent, QByteArray("\033[27u"));
}

void Vt102EmulationTest::testKittyKeyboardLegacyKeys()
{
    TestEmulation em;
    em.reset();
    em.setCodec(TestEmulation::Utf8Codec);

    // Push flags=1 (disambiguate)
    const char push1[] = "\033[>1u";
    em.receiveData(push1, sizeof(push1) - 1);

    // Up arrow with no modifiers — should use legacy encoding
    QKeyEvent upPress(QEvent::KeyPress, Qt::Key_Up, Qt::NoModifier);
    em.sendKeyEvent(&upPress);
    QCOMPARE(em.lastSent, QByteArray("\033[A"));

    // Up arrow with Ctrl — should use legacy encoding with modifiers
    QKeyEvent upCtrl(QEvent::KeyPress, Qt::Key_Up, Qt::ControlModifier);
    em.sendKeyEvent(&upCtrl);
    QCOMPARE(em.lastSent, QByteArray("\033[1;5A"));

    // F5 with no modifiers — legacy tilde encoding
    QKeyEvent f5Press(QEvent::KeyPress, Qt::Key_F5, Qt::NoModifier);
    em.sendKeyEvent(&f5Press);
    QCOMPARE(em.lastSent, QByteArray("\033[15~"));

    // F1 with no modifiers — SS3 encoding
    QKeyEvent f1Press(QEvent::KeyPress, Qt::Key_F1, Qt::NoModifier);
    em.sendKeyEvent(&f1Press);
    QCOMPARE(em.lastSent, QByteArray("\033OP"));

    // F1 with Shift — CSI 1;2 P
    QKeyEvent f1Shift(QEvent::KeyPress, Qt::Key_F1, Qt::ShiftModifier);
    em.sendKeyEvent(&f1Shift);
    QCOMPARE(em.lastSent, QByteArray("\033[1;2P"));

    // Delete key — CSI 3 ~
    QKeyEvent delPress(QEvent::KeyPress, Qt::Key_Delete, Qt::NoModifier);
    em.sendKeyEvent(&delPress);
    QCOMPARE(em.lastSent, QByteArray("\033[3~"));
}

void Vt102EmulationTest::testKittyKeyboardCtrlLetters()
{
    TestEmulation em;
    em.reset();
    em.setCodec(TestEmulation::Utf8Codec);

    // Push flags=1 (disambiguate)
    const char push1[] = "\033[>1u";
    em.receiveData(push1, sizeof(push1) - 1);

    // Ctrl+C: key=Qt::Key_C, modifiers=ControlModifier, text="\x03"
    QKeyEvent ctrlC(QEvent::KeyPress, Qt::Key_C, Qt::ControlModifier, QStringLiteral("\x03"));
    em.sendKeyEvent(&ctrlC);
    // Should send CSI 99;5u (keycode=99='c', modifier=5=1+ctrl:4)
    QCOMPARE(em.lastSent, QByteArray("\033[99;5u"));

    // Ctrl+D: key=Qt::Key_D, modifiers=ControlModifier, text="\x04"
    QKeyEvent ctrlD(QEvent::KeyPress, Qt::Key_D, Qt::ControlModifier, QStringLiteral("\x04"));
    em.sendKeyEvent(&ctrlD);
    // Should send CSI 100;5u (keycode=100='d', modifier=5=1+ctrl:4)
    QCOMPARE(em.lastSent, QByteArray("\033[100;5u"));
}

void Vt102EmulationTest::testKittyKeyboardTextKeys()
{
    // --- Flag 1 (disambiguate): text keys with non-shift modifiers use CSI u ---
    {
        TestEmulation em;
        em.reset();
        em.setCodec(TestEmulation::Utf8Codec);

        const char push1[] = "\033[>1u";
        em.receiveData(push1, sizeof(push1) - 1);

        // Ctrl+A → CSI 97;5u
        QKeyEvent ctrlA(QEvent::KeyPress, Qt::Key_A, Qt::ControlModifier, QStringLiteral("\x01"));
        em.sendKeyEvent(&ctrlA);
        QCOMPARE(em.lastSent, QByteArray("\033[97;5u"));

        // Alt+A → CSI 97;3u
        QKeyEvent altA(QEvent::KeyPress, Qt::Key_A, Qt::AltModifier, QStringLiteral("a"));
        em.sendKeyEvent(&altA);
        QCOMPARE(em.lastSent, QByteArray("\033[97;3u"));

        // Ctrl+Shift+A → CSI 97;6u
        QKeyEvent ctrlShiftA(QEvent::KeyPress, Qt::Key_A, Qt::ControlModifier | Qt::ShiftModifier, QStringLiteral("\x01"));
        em.sendKeyEvent(&ctrlShiftA);
        QCOMPARE(em.lastSent, QByteArray("\033[97;6u"));
    }

    // --- Flag 1 (disambiguate): text keys with only shift or no modifiers use legacy ---
    {
        TestEmulation em;
        em.reset();
        em.setCodec(TestEmulation::Utf8Codec);

        const char push1[] = "\033[>1u";
        em.receiveData(push1, sizeof(push1) - 1);

        // Plain 'a' → falls through to legacy (no CSI u sent)
        em.lastSent.clear();
        QKeyEvent plainA(QEvent::KeyPress, Qt::Key_A, Qt::NoModifier, QStringLiteral("a"));
        em.sendKeyEvent(&plainA);
        QVERIFY2(!em.lastSent.contains("\033["), "plain 'a' with flag 1 should use legacy, not CSI u");

        // Shift+A → falls through to legacy (shift consumed by uppercase)
        em.lastSent.clear();
        QKeyEvent shiftA(QEvent::KeyPress, Qt::Key_A, Qt::ShiftModifier, QStringLiteral("A"));
        em.sendKeyEvent(&shiftA);
        QVERIFY2(!em.lastSent.contains("\033["), "Shift+A with flag 1 should use legacy, not CSI u");

        // Space → falls through to legacy
        em.lastSent.clear();
        QKeyEvent space(QEvent::KeyPress, Qt::Key_Space, Qt::NoModifier, QStringLiteral(" "));
        em.sendKeyEvent(&space);
        QVERIFY2(!em.lastSent.contains("\033["), "Space with flag 1 should use legacy, not CSI u");
    }

    // --- Flag 8 (report all keys): all text keys use CSI u ---
    {
        TestEmulation em;
        em.reset();
        em.setCodec(TestEmulation::Utf8Codec);

        const char push8[] = "\033[>8u";
        em.receiveData(push8, sizeof(push8) - 1);

        // Plain 'a' → CSI 97 u
        QKeyEvent plainA(QEvent::KeyPress, Qt::Key_A, Qt::NoModifier, QStringLiteral("a"));
        em.sendKeyEvent(&plainA);
        QCOMPARE(em.lastSent, QByteArray("\033[97u"));

        // Shift+A → CSI 97;2u
        QKeyEvent shiftA(QEvent::KeyPress, Qt::Key_A, Qt::ShiftModifier, QStringLiteral("A"));
        em.sendKeyEvent(&shiftA);
        QCOMPARE(em.lastSent, QByteArray("\033[97;2u"));

        // Space → CSI 32 u
        QKeyEvent space(QEvent::KeyPress, Qt::Key_Space, Qt::NoModifier, QStringLiteral(" "));
        em.sendKeyEvent(&space);
        QCOMPARE(em.lastSent, QByteArray("\033[32u"));
    }
}

QTEST_GUILESS_MAIN(Vt102EmulationTest)

#include "moc_Vt102EmulationTest.cpp"
