#include "putty.h"
#include "terminal.h"

void modalfatalbox(const char *p, ...)
{
    va_list ap;
    fprintf(stderr, "FATAL ERROR: ");
    va_start(ap, p);
    vfprintf(stderr, p, ap);
    va_end(ap);
    fputc('\n', stderr);
    exit(1);
}

const char *const appname = "test_lineedit";

char *platform_default_s(const char *name)
{ return NULL; }
bool platform_default_b(const char *name, bool def)
{ return def; }
int platform_default_i(const char *name, int def)
{ return def; }
FontSpec *platform_default_fontspec(const char *name)
{ return fontspec_new_default(); }
Filename *platform_default_filename(const char *name)
{ return filename_from_str(""); }

struct SpecialRecord {
    SessionSpecialCode code;
    int arg;
};

typedef struct Mock {
    Terminal *term;
    Ldisc *ldisc;
    Conf *conf;
    struct unicode_data ucsdata[1];

    bool echo, edit;
    strbuf *to_terminal, *to_backend;

    struct SpecialRecord *specials;
    size_t nspecials, specialsize;

    strbuf *context;                   /* for printing in failed tests */

    bool any_test_failed;

    TermWin tw;
    Seat seat;
    Backend backend;
} Mock;

static size_t mock_output(Seat *seat, SeatOutputType type,
                          const void *data, size_t len)
{
    Mock *mk = container_of(seat, Mock, seat);
    put_data(mk->to_terminal, data, len);
    return 0;
}

static void mock_send(Backend *be, const char *buf, size_t len)
{
    Mock *mk = container_of(be, Mock, backend);
    put_data(mk->to_backend, buf, len);
}

static void mock_special(Backend *be, SessionSpecialCode code, int arg)
{
    Mock *mk = container_of(be, Mock, backend);
    sgrowarray(mk->specials, mk->specialsize, mk->nspecials);
    mk->specials[mk->nspecials].code = code;
    mk->specials[mk->nspecials].arg = arg;
    mk->nspecials++;
}

static bool mock_ldisc_option_state(Backend *be, int option)
{
    Mock *mk = container_of(be, Mock, backend);
    switch (option) {
      case LD_ECHO:
        return mk->echo;
      case LD_EDIT:
        return mk->edit;
      default:
        unreachable("bad ldisc option");
    }
}

static void mock_provide_ldisc(Backend *be, Ldisc *ldisc)
{
    Mock *mk = container_of(be, Mock, backend);
    mk->ldisc = ldisc;
}

static bool mock_sendok(Backend *be)
{
    Mock *mk = container_of(be, Mock, backend);
    (void)mk;
    /* FIXME: perhaps make this settable, to test the input_queue system? */
    return true;
}

static void mock_set_raw_mouse_mode(TermWin *win, bool enable) {}
static void mock_palette_get_overrides(TermWin *tw, Terminal *term) {}

static const TermWinVtable mock_termwin_vt = {
    .set_raw_mouse_mode = mock_set_raw_mouse_mode,
    .palette_get_overrides = mock_palette_get_overrides,
};

static const SeatVtable mock_seat_vt = {
    .output = mock_output,
    .echoedit_update = nullseat_echoedit_update,
};

static const BackendVtable mock_backend_vt = {
    .sendok = mock_sendok,
    .send = mock_send,
    .special = mock_special,
    .ldisc_option_state = mock_ldisc_option_state,
    .provide_ldisc = mock_provide_ldisc,
    .id = "mock",
};

static Mock *mock_new(void)
{
    Mock *mk = snew(Mock);
    memset(mk, 0, sizeof(*mk));

    mk->conf = conf_new();
    do_defaults(NULL, mk->conf);

    init_ucs_generic(mk->conf, mk->ucsdata);
    mk->ucsdata->line_codepage = CP_437;

    mk->context = strbuf_new();
    mk->to_terminal = strbuf_new();
    mk->to_backend = strbuf_new();

    mk->tw.vt = &mock_termwin_vt;
    mk->seat.vt = &mock_seat_vt;
    mk->backend.vt = &mock_backend_vt;

    return mk;
}

static void mock_free(Mock *mk)
{
    strbuf_free(mk->context);
    strbuf_free(mk->to_terminal);
    strbuf_free(mk->to_backend);
    conf_free(mk->conf);
    term_free(mk->term);
    sfree(mk->specials);
    sfree(mk);
}

static void reset(Mock *mk)
{
    strbuf_clear(mk->context);
    strbuf_clear(mk->to_terminal);
    strbuf_clear(mk->to_backend);
    mk->nspecials = 0;
}

static void test_context(Mock *mk, const char *fmt, ...)
{
    strbuf_clear(mk->context);
    va_list ap;
    va_start(ap, fmt);
    put_fmtv(mk->context, fmt, ap);
    va_end(ap);
}

static void print_context(Mock *mk, const char *file, int line)
{
    printf("%s:%d", file, line);
    if (mk->context->len)
        printf(" (%s)", mk->context->s);
    printf(": ");
}

#define EXPECT(mk, what, ...)                                   \
    expect_ ## what(mk, __FILE__, __LINE__, __VA_ARGS__)

static void expect_backend(Mock *mk, const char *file, int line,
                           ptrlen expected)
{
    ptrlen actual = ptrlen_from_strbuf(mk->to_backend);
    if (!ptrlen_eq_ptrlen(expected, actual)) {
        print_context(mk, file, line);
        printf("expected backend output \"");
        write_c_string_literal(stdout, expected);
        printf("\", got \"");
        write_c_string_literal(stdout, actual);
        printf("\"\n");
        mk->any_test_failed = true;
    }
}

static void expect_terminal(Mock *mk, const char *file, int line,
                            ptrlen expected)
{
    ptrlen actual = ptrlen_from_strbuf(mk->to_terminal);
    if (!ptrlen_eq_ptrlen(expected, actual)) {
        print_context(mk, file, line);
        printf("expected terminal output \"");
        write_c_string_literal(stdout, expected);
        printf("\", got \"");
        write_c_string_literal(stdout, actual);
        printf("\"\n");
        mk->any_test_failed = true;
    }
}

static void expect_specials(Mock *mk, const char *file, int line,
                            size_t nspecials, ...)
{
    va_list ap;

    static const char *const special_names[] = {
#define SPECIAL(x) #x,
#include "specials.h"
#undef SPECIAL
    };

    bool match;
    if (nspecials != mk->nspecials) {
        match = false;
    } else {
        match = true;
        va_start(ap, nspecials);
        for (size_t i = 0; i < nspecials; i++) {
            SessionSpecialCode code = va_arg(ap, SessionSpecialCode);
            int arg = va_arg(ap, int);
            if (code != mk->specials[i].code || arg != mk->specials[i].arg)
                match = false;
        }
        va_end(ap);
    }

    if (!match) {
        print_context(mk, file, line);
        printf("expected specials [");
        va_start(ap, nspecials);
        for (size_t i = 0; i < nspecials; i++) {
            SessionSpecialCode code = va_arg(ap, SessionSpecialCode);
            int arg = va_arg(ap, int);
            printf(" %s.%d", special_names[code], arg);
        }
        va_end(ap);
        printf(" ], got [");
        for (size_t i = 0; i < mk->nspecials; i++) {
            printf(" %s.%d", special_names[mk->specials[i].code],
                   mk->specials[i].arg);
        }
        printf(" ]\n");
        mk->any_test_failed = true;
    }
}

static void test_noedit(Mock *mk)
{
    mk->edit = false;
    mk->echo = false;

    /*
     * In non-echo and non-edit mode, the channel is 8-bit clean
     */
    for (unsigned c = 0; c < 256; c++) {
        char buf[1];

        test_context(mk, "c=%02x", c);
        buf[0] = c;
        ldisc_send(mk->ldisc, buf, 1, false);
        EXPECT(mk, backend, make_ptrlen(buf, 1));
        reset(mk);
    }
    /* ... regardless of the 'interactive' flag */
    for (unsigned c = 0; c < 256; c++) {
        char buf[1];

        test_context(mk, "c=%02x", c);
        buf[0] = c;
        ldisc_send(mk->ldisc, buf, 1, true);
        EXPECT(mk, backend, make_ptrlen(buf, 1));
        reset(mk);
    }
    /* ... and any nonzero character does the same thing even if sent
     * with the magic -1 length flag */
    for (unsigned c = 1; c < 256; c++) {
        char buf[2];

        test_context(mk, "c=%02x", c);
        buf[0] = c;
        buf[1] = '\0';
        ldisc_send(mk->ldisc, buf, -1, true);
        EXPECT(mk, backend, make_ptrlen(buf, 1));
        reset(mk);
    }

    /*
     * Test the special-character cases for Telnet.
     */
    conf_set_int(mk->conf, CONF_protocol, PROT_TELNET);
    conf_set_bool(mk->conf, CONF_telnet_newline, false);
    conf_set_bool(mk->conf, CONF_telnet_keyboard, false);
    ldisc_configure(mk->ldisc, mk->conf);

    /* Without telnet_newline or telnet_keyboard, these all do the
     * normal thing */
    ldisc_send(mk->ldisc, "\x0D", -1, true);
    EXPECT(mk, backend, PTRLEN_LITERAL("\x0D"));
    reset(mk);
    ldisc_send(mk->ldisc, "\x08", -1, true);
    EXPECT(mk, backend, PTRLEN_LITERAL("\x08"));
    reset(mk);
    ldisc_send(mk->ldisc, "\x7F", -1, true);
    EXPECT(mk, backend, PTRLEN_LITERAL("\x7F"));
    reset(mk);
    ldisc_send(mk->ldisc, "\x03", -1, true);
    EXPECT(mk, backend, PTRLEN_LITERAL("\x03"));
    reset(mk);
    ldisc_send(mk->ldisc, "\x1A", -1, true);
    EXPECT(mk, backend, PTRLEN_LITERAL("\x1A"));
    reset(mk);

    /* telnet_newline controls translation of CR into SS_EOL */
    conf_set_bool(mk->conf, CONF_telnet_newline, true);
    ldisc_configure(mk->ldisc, mk->conf);
    ldisc_send(mk->ldisc, "\x0D", -1, true);
    EXPECT(mk, specials, 1, SS_EOL, 0);
    reset(mk);

    /* And telnet_keyboard controls the others */
    conf_set_bool(mk->conf, CONF_telnet_newline, false);
    conf_set_bool(mk->conf, CONF_telnet_keyboard, true);
    ldisc_configure(mk->ldisc, mk->conf);
    ldisc_send(mk->ldisc, "\x08", -1, true);
    EXPECT(mk, specials, 1, SS_EC, 0);
    reset(mk);
    ldisc_send(mk->ldisc, "\x7F", -1, true);
    EXPECT(mk, specials, 1, SS_EC, 0);
    reset(mk);
    ldisc_send(mk->ldisc, "\x03", -1, true);
    EXPECT(mk, specials, 1, SS_IP, 0);
    reset(mk);
    ldisc_send(mk->ldisc, "\x1A", -1, true);
    EXPECT(mk, specials, 1, SS_SUSP, 0);
    reset(mk);

    /*
     * In echo-but-no-edit mode, we also expect that every character
     * is echoed back to the display as a side effect, including when
     * it's sent as a special -1 keystroke.
     *
     * This state only comes up in Telnet, because that has protocol
     * options to independently configure echo and edit. Telnet is
     * also the most complicated of the protocols because of the above
     * special cases, so we stay in Telnet mode for this test.
     */
    mk->echo = true;
    for (unsigned c = 0; c < 256; c++) {
        char buf[1];

        test_context(mk, "c=%02x", c);
        buf[0] = c;
        ldisc_send(mk->ldisc, buf, 1, false);
        EXPECT(mk, terminal, make_ptrlen(buf, 1));
        reset(mk);
    }
    for (unsigned c = 1; c < 256; c++) {
        char buf[2];

        test_context(mk, "c=%02x", c);
        buf[0] = c;
        buf[1] = '\0';
        ldisc_send(mk->ldisc, buf, -1, true);
        EXPECT(mk, terminal, make_ptrlen(buf, 1));
        reset(mk);
    }

    do_defaults(NULL, mk->conf);
    ldisc_configure(mk->ldisc, mk->conf);
}

static void test_edit(Mock *mk, bool echo)
{
    static const char *const ctls = "@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_";

    mk->edit = true;
    mk->echo = echo;

#define EXPECT_TERMINAL(mk, val) do {                           \
        if (!echo) EXPECT(mk, terminal, PTRLEN_LITERAL(""));    \
        else EXPECT(mk, terminal, val);                         \
    } while (0)

    /* ASCII printing characters all print when entered, but don't go
     * to the terminal until Return is pressed */
    for (unsigned c = 0x20; c < 0x7F; c++) {
        char buf[3];

        test_context(mk, "c=%02x", c);
        buf[0] = c;
        ldisc_send(mk->ldisc, buf, 1, false);
        EXPECT(mk, backend, PTRLEN_LITERAL(""));
        EXPECT_TERMINAL(mk, make_ptrlen(buf, 1));
        ldisc_send(mk->ldisc, "\015", 1, false);
        buf[1] = '\015';
        buf[2] = '\012';
        EXPECT(mk, backend, make_ptrlen(buf, 3));
        EXPECT_TERMINAL(mk, make_ptrlen(buf, 3));
        reset(mk);
    }

    /* C0 control characters mostly show up as ^X or similar */
    for (unsigned c = 0; c < 0x1F; c++) {
        char backbuf[3];
        char termbuf[4];

        switch (ctls[c]) {
          case 'D': continue;          /* ^D sends EOF */
          case 'M': continue;          /* ^M is Return */
          case 'R': continue;          /* ^R redisplays line */
          case 'U': continue;          /* ^U deletes the line */
          case 'V': continue;          /* ^V makes the next char literal */
          case 'W': continue;          /* ^W deletes a word */
            /*
             * ^H / ^? are not included here. Those are treated
             * literally if sent as plain input bytes. Only sending
             * them as special via length==-1 causes them to act as
             * backspace, which I think was simply because there _is_
             * a dedicated key that can do that function, so there's
             * no need to also eat the Ctrl+thing combo.
             */

            /*
             * Also, ^C, ^Z and ^\ self-insert (when not in Telnet
             * mode) but have the side effect of erasing the line
             * buffer so far. In this loop, that doesn't show up,
             * because the line buffer is empty already. However, I
             * don't test that, because it's silly, and probably
             * doesn't want to keep happening!
             */
        }

        test_context(mk, "c=%02x", c);
        backbuf[0] = c;
        ldisc_send(mk->ldisc, backbuf, 1, false);
        EXPECT(mk, backend, PTRLEN_LITERAL(""));
        termbuf[0] = '^';
        termbuf[1] = ctls[c];
        EXPECT_TERMINAL(mk, make_ptrlen(termbuf, 2));
        ldisc_send(mk->ldisc, "\015", 1, false);
        backbuf[1] = '\015';
        backbuf[2] = '\012';
        EXPECT(mk, backend, make_ptrlen(backbuf, 3));
        termbuf[2] = '\015';
        termbuf[3] = '\012';
        EXPECT_TERMINAL(mk, make_ptrlen(termbuf, 4));
        reset(mk);
    }

    /* Prefixed with ^V, the same is true of _all_ C0 controls */
    for (unsigned c = 0; c < 0x1F; c++) {
        char backbuf[3];
        char termbuf[4];

        test_context(mk, "c=%02x", c);
        backbuf[0] = 'V' & 0x1F;
        ldisc_send(mk->ldisc, backbuf, 1, false);
        backbuf[0] = c;
        ldisc_send(mk->ldisc, backbuf, 1, false);
        EXPECT(mk, backend, PTRLEN_LITERAL(""));
        termbuf[0] = '^';
        termbuf[1] = ctls[c];
        EXPECT_TERMINAL(mk, make_ptrlen(termbuf, 2));
        ldisc_send(mk->ldisc, "\015", 1, false);
        backbuf[1] = '\015';
        backbuf[2] = '\012';
        EXPECT(mk, backend, make_ptrlen(backbuf, 3));
        termbuf[2] = '\015';
        termbuf[3] = '\012';
        EXPECT_TERMINAL(mk, make_ptrlen(termbuf, 4));
        reset(mk);
    }

    /* Deleting an ASCII character sends a single BSB and deletes just
     * that byte from the buffer */
    ldisc_send(mk->ldisc, "ab", 2, false);
    EXPECT(mk, backend, PTRLEN_LITERAL(""));
    EXPECT_TERMINAL(mk, PTRLEN_LITERAL("ab"));
    ldisc_send(mk->ldisc, "\x08", -1, false);
    EXPECT(mk, backend, PTRLEN_LITERAL(""));
    EXPECT_TERMINAL(mk, PTRLEN_LITERAL("ab\x08 \x08"));
    ldisc_send(mk->ldisc, "\x0D", -1, false);
    EXPECT(mk, backend, PTRLEN_LITERAL("a\x0D\x0A"));
    reset(mk);

    /* Deleting a character written as a ^X code sends two BSBs to
     * wipe out the two-character display sequence */
    ldisc_send(mk->ldisc, "a\x02", 2, false);
    EXPECT(mk, backend, PTRLEN_LITERAL(""));
    EXPECT_TERMINAL(mk, PTRLEN_LITERAL("a^B"));
    ldisc_send(mk->ldisc, "\x7F", -1, false);
    EXPECT(mk, backend, PTRLEN_LITERAL(""));
    EXPECT_TERMINAL(mk, PTRLEN_LITERAL("a^B\x08 \x08\x08 \x08"));
    ldisc_send(mk->ldisc, "\x0D", -1, false);
    EXPECT(mk, backend, PTRLEN_LITERAL("a\x0D\x0A"));
    reset(mk);

    /* ^D sends the line editing buffer without a trailing Return, if
     * it's non-empty */
    ldisc_send(mk->ldisc, "abc\x04", 4, false);
    EXPECT(mk, backend, PTRLEN_LITERAL("abc"));
    EXPECT_TERMINAL(mk, PTRLEN_LITERAL("abc"));
    ldisc_send(mk->ldisc, "\x0D", -1, false);
    EXPECT(mk, backend, PTRLEN_LITERAL("abc\x0D\x0A"));
    reset(mk);

    /* But if the buffer is empty, ^D sends SS_EOF */
    ldisc_send(mk->ldisc, "\x04", 1, false);
    EXPECT(mk, backend, PTRLEN_LITERAL(""));
    EXPECT_TERMINAL(mk, PTRLEN_LITERAL(""));
    EXPECT(mk, specials, 1, SS_EOF, 0);
    reset(mk);

    /* ^M with the special flag is the Return key, and sends the line */
    ldisc_send(mk->ldisc, "abc", 3, false);
    EXPECT(mk, backend, PTRLEN_LITERAL(""));
    EXPECT_TERMINAL(mk, PTRLEN_LITERAL("abc"));
    ldisc_send(mk->ldisc, "\x0D", -1, true);
    EXPECT(mk, backend, PTRLEN_LITERAL("abc\x0D\x0A"));
    EXPECT_TERMINAL(mk, PTRLEN_LITERAL("abc\x0D\x0A"));
    reset(mk);

    /* In non-LE_CRLF_NEWLINE mode, either of ^M or ^J without the
     * special flag also sends the line */
    conf_set_int(mk->conf, CONF_protocol, PROT_SSH);
    ldisc_configure(mk->ldisc, mk->conf);
    ldisc_send(mk->ldisc, "abc", 3, false);
    EXPECT(mk, backend, PTRLEN_LITERAL(""));
    EXPECT_TERMINAL(mk, PTRLEN_LITERAL("abc"));
    ldisc_send(mk->ldisc, "\x0D", 1, true);
    EXPECT(mk, backend, PTRLEN_LITERAL("abc\x0D"));
    EXPECT_TERMINAL(mk, PTRLEN_LITERAL("abc\x0D\x0A"));
    reset(mk);
    ldisc_send(mk->ldisc, "abc", 3, false);
    EXPECT(mk, backend, PTRLEN_LITERAL(""));
    EXPECT_TERMINAL(mk, PTRLEN_LITERAL("abc"));
    ldisc_send(mk->ldisc, "\x0A", 1, true);
    EXPECT(mk, backend, PTRLEN_LITERAL("abc\x0D"));
    EXPECT_TERMINAL(mk, PTRLEN_LITERAL("abc\x0D\x0A"));
    reset(mk);

    /* In LE_CRLF_NEWLINE mode, non-special ^J is just literal */
    conf_set_int(mk->conf, CONF_protocol, PROT_RAW);
    ldisc_configure(mk->ldisc, mk->conf);
    ldisc_send(mk->ldisc, "abc", 3, false);
    EXPECT(mk, backend, PTRLEN_LITERAL(""));
    EXPECT_TERMINAL(mk, PTRLEN_LITERAL("abc"));
    ldisc_send(mk->ldisc, "\x0A", 1, true);
    EXPECT(mk, backend, PTRLEN_LITERAL(""));
    EXPECT_TERMINAL(mk, PTRLEN_LITERAL("abc^J"));
    /* So when we press Return it's sent */
    ldisc_send(mk->ldisc, "\x0D", -1, true);
    EXPECT(mk, backend, PTRLEN_LITERAL("abc\x0A\x0D\x0A"));
    EXPECT_TERMINAL(mk, PTRLEN_LITERAL("abc^J\x0D\x0A"));
    reset(mk);

    /* In LE_CRLF_NEWLINE mode, non-special ^M is literal, but if
     * followed with ^J, they combine into a Return */
    conf_set_int(mk->conf, CONF_protocol, PROT_RAW);
    ldisc_configure(mk->ldisc, mk->conf);
    ldisc_send(mk->ldisc, "abc", 3, false);
    EXPECT(mk, backend, PTRLEN_LITERAL(""));
    EXPECT_TERMINAL(mk, PTRLEN_LITERAL("abc"));
    ldisc_send(mk->ldisc, "\x0D", 1, true);
    EXPECT(mk, backend, PTRLEN_LITERAL(""));
    EXPECT_TERMINAL(mk, PTRLEN_LITERAL("abc^M"));
    /* So when we press Return it's sent */
    ldisc_send(mk->ldisc, "\x0A", 1, true);
    EXPECT(mk, backend, PTRLEN_LITERAL("abc\x0D\x0A"));
    EXPECT_TERMINAL(mk, PTRLEN_LITERAL("abc^M\x08 \x08\x08 \x08\x0D\x0A"));
    reset(mk);

    /* ^R redraws the current line, after printing "^R" at the end of
     * the previous attempt to make it clear that that's what
     * happened */
    ldisc_send(mk->ldisc, "a\x01", 2, false);
    EXPECT(mk, backend, PTRLEN_LITERAL(""));
    EXPECT_TERMINAL(mk, PTRLEN_LITERAL("a^A"));
    ldisc_send(mk->ldisc, "\x12", 1, false);
    EXPECT(mk, backend, PTRLEN_LITERAL(""));
    EXPECT_TERMINAL(mk, PTRLEN_LITERAL("a^A^R\x0D\x0A" "a^A"));
    ldisc_send(mk->ldisc, "\x0D", -1, false);
    reset(mk);

    /* ^U deletes the whole line */
    ldisc_send(mk->ldisc, "a b c", 5, false);
    EXPECT(mk, backend, PTRLEN_LITERAL(""));
    EXPECT_TERMINAL(mk, PTRLEN_LITERAL("a b c"));
    ldisc_send(mk->ldisc, "\x15", 1, false);
    EXPECT(mk, backend, PTRLEN_LITERAL(""));
    EXPECT_TERMINAL(
        mk, PTRLEN_LITERAL(
            "a b c\x08 \x08\x08 \x08\x08 \x08\x08 \x08\x08 \x08"));
    ldisc_send(mk->ldisc, "\x0D", -1, false);
    EXPECT(mk, backend, PTRLEN_LITERAL("\x0D\x0A"));
    EXPECT_TERMINAL(
        mk, PTRLEN_LITERAL(
            "a b c\x08 \x08\x08 \x08\x08 \x08\x08 \x08\x08 \x08\x0D\x0A"));
    reset(mk);
    /* And it still knows that a control character written as ^X takes
     * two BSBs to delete */
    ldisc_send(mk->ldisc, "a\x02" "c", 3, false);
    EXPECT(mk, backend, PTRLEN_LITERAL(""));
    EXPECT_TERMINAL(mk, PTRLEN_LITERAL("a^Bc"));
    ldisc_send(mk->ldisc, "\x15", 1, false);
    EXPECT(mk, backend, PTRLEN_LITERAL(""));
    EXPECT_TERMINAL(
        mk, PTRLEN_LITERAL("a^Bc\x08 \x08\x08 \x08\x08 \x08\x08 \x08"));
    ldisc_send(mk->ldisc, "\x0D", -1, false);
    reset(mk);

    /* ^W deletes a word, which means that it deletes to the most
     * recent boundary with a space on the left and a nonspace on the
     * right. (Or the beginning of the string, whichever comes first.) */
    ldisc_send(mk->ldisc, "hello, world\x17", 13, false);
    EXPECT(mk, backend, PTRLEN_LITERAL(""));
    EXPECT_TERMINAL(
        mk, PTRLEN_LITERAL(
            "hello, world\x08 \x08\x08 \x08\x08 \x08\x08 \x08\x08 \x08"));
    ldisc_send(mk->ldisc, "\x0D", 1, false);
    EXPECT(mk, backend, PTRLEN_LITERAL("hello, \x0D\x0A"));
    reset(mk);
    ldisc_send(mk->ldisc, "hello, world \x17", 14, false);
    EXPECT(mk, backend, PTRLEN_LITERAL(""));
    EXPECT_TERMINAL(
        mk, PTRLEN_LITERAL(
            "hello, world "
            "\x08 \x08\x08 \x08\x08 \x08\x08 \x08\x08 \x08\x08 \x08"));
    ldisc_send(mk->ldisc, "\x0D", 1, false);
    EXPECT(mk, backend, PTRLEN_LITERAL("hello, \x0D\x0A"));
    reset(mk);
    ldisc_send(mk->ldisc, " hello \x17", 8, false);
    EXPECT(mk, backend, PTRLEN_LITERAL(""));
    EXPECT_TERMINAL(
        mk, PTRLEN_LITERAL(
            " hello \x08 \x08\x08 \x08\x08 \x08\x08 \x08\x08 \x08\x08 \x08"));
    ldisc_send(mk->ldisc, "\x0D", 1, false);
    EXPECT(mk, backend, PTRLEN_LITERAL(" \x0D\x0A"));
    reset(mk);
    ldisc_send(mk->ldisc, "hello \x17", 7, false);
    EXPECT(mk, backend, PTRLEN_LITERAL(""));
    EXPECT_TERMINAL(
        mk, PTRLEN_LITERAL(
            "hello \x08 \x08\x08 \x08\x08 \x08\x08 \x08\x08 \x08\x08 \x08"));
    ldisc_send(mk->ldisc, "\x0D", 1, false);
    EXPECT(mk, backend, PTRLEN_LITERAL("\x0D\x0A"));
    reset(mk);
    /* And this too knows that a control character written as ^X takes
     * two BSBs to delete */
    ldisc_send(mk->ldisc, "a\x02" "c", 3, false);
    EXPECT(mk, backend, PTRLEN_LITERAL(""));
    EXPECT_TERMINAL(mk, PTRLEN_LITERAL("a^Bc"));
    ldisc_send(mk->ldisc, "\x17", 1, false);
    EXPECT(mk, backend, PTRLEN_LITERAL(""));
    EXPECT_TERMINAL(
        mk, PTRLEN_LITERAL("a^Bc\x08 \x08\x08 \x08\x08 \x08\x08 \x08"));
    ldisc_send(mk->ldisc, "\x0D", -1, false);
    reset(mk);

    /* Test handling of ^C and friends in non-telnet_keyboard mode */
    ldisc_send(mk->ldisc, "abc\x03", 4, false);
    EXPECT(mk, backend, PTRLEN_LITERAL(""));
    EXPECT_TERMINAL(mk, PTRLEN_LITERAL("abc\x08 \x08\x08 \x08\x08 \x08^C"));
    EXPECT(mk, specials, 1, SS_EL, 0);
    ldisc_send(mk->ldisc, "\x0D", -1, false);
    reset(mk);
    ldisc_send(mk->ldisc, "abc\x1a", 4, false);
    EXPECT(mk, backend, PTRLEN_LITERAL(""));
    EXPECT_TERMINAL(mk, PTRLEN_LITERAL("abc\x08 \x08\x08 \x08\x08 \x08^Z"));
    EXPECT(mk, specials, 1, SS_EL, 0);
    ldisc_send(mk->ldisc, "\x0D", -1, false);
    reset(mk);
    ldisc_send(mk->ldisc, "abc\x1c", 4, false);
    EXPECT(mk, backend, PTRLEN_LITERAL(""));
    EXPECT_TERMINAL(mk, PTRLEN_LITERAL("abc\x08 \x08\x08 \x08\x08 \x08^\\"));
    EXPECT(mk, specials, 1, SS_EL, 0);
    ldisc_send(mk->ldisc, "\x0D", -1, false);
    reset(mk);

    /* And in telnet_keyboard mode */
    conf_set_bool(mk->conf, CONF_telnet_keyboard, true);
    ldisc_configure(mk->ldisc, mk->conf);

    /* FIXME: should we _really_ be sending EL before each of these? */
    ldisc_send(mk->ldisc, "abc\x03", 4, false);
    EXPECT(mk, backend, PTRLEN_LITERAL(""));
    EXPECT_TERMINAL(mk, PTRLEN_LITERAL("abc\x08 \x08\x08 \x08\x08 \x08"));
    EXPECT(mk, specials, 2, SS_EL, 0, SS_IP, 0);
    ldisc_send(mk->ldisc, "\x0D", -1, false);
    reset(mk);
    ldisc_send(mk->ldisc, "abc\x1a", 4, false);
    EXPECT(mk, backend, PTRLEN_LITERAL(""));
    EXPECT_TERMINAL(mk, PTRLEN_LITERAL("abc\x08 \x08\x08 \x08\x08 \x08"));
    EXPECT(mk, specials, 2, SS_EL, 0, SS_SUSP, 0);
    ldisc_send(mk->ldisc, "\x0D", -1, false);
    reset(mk);
    ldisc_send(mk->ldisc, "abc\x1c", 4, false);
    EXPECT(mk, backend, PTRLEN_LITERAL(""));
    EXPECT_TERMINAL(mk, PTRLEN_LITERAL("abc\x08 \x08\x08 \x08\x08 \x08"));
    EXPECT(mk, specials, 2, SS_EL, 0, SS_ABORT, 0);
    ldisc_send(mk->ldisc, "\x0D", -1, false);
    reset(mk);

    conf_set_bool(mk->conf, CONF_telnet_keyboard, false);
    ldisc_configure(mk->ldisc, mk->conf);

    /* Test UTF-8 characters of various lengths and ensure deleting
     * one deletes the whole character from the buffer (by pressing
     * Return and seeing what gets sent) but sends a number of BSBs
     * corresponding to the character's terminal width */
    mk->term->utf = true;

    ldisc_send(mk->ldisc, "\xC2\xA0\xC2\xA1", 4, false);
    EXPECT(mk, backend, PTRLEN_LITERAL(""));
    EXPECT_TERMINAL(mk, PTRLEN_LITERAL("\xC2\xA0\xC2\xA1"));
    ldisc_send(mk->ldisc, "\x08", -1, false);
    EXPECT(mk, backend, PTRLEN_LITERAL(""));
    EXPECT_TERMINAL(mk, PTRLEN_LITERAL("\xC2\xA0\xC2\xA1\x08 \x08"));
    ldisc_send(mk->ldisc, "\x0D", -1, false);
    EXPECT(mk, backend, PTRLEN_LITERAL("\xC2\xA0\x0D\x0A"));
    reset(mk);

    ldisc_send(mk->ldisc, "\xE2\xA0\x80\xE2\xA0\x81", 6, false);
    EXPECT(mk, backend, PTRLEN_LITERAL(""));
    EXPECT_TERMINAL(mk, PTRLEN_LITERAL("\xE2\xA0\x80\xE2\xA0\x81"));
    ldisc_send(mk->ldisc, "\x08", -1, false);
    EXPECT(mk, backend, PTRLEN_LITERAL(""));
    EXPECT_TERMINAL(mk, PTRLEN_LITERAL("\xE2\xA0\x80\xE2\xA0\x81\x08 \x08"));
    ldisc_send(mk->ldisc, "\x0D", -1, false);
    EXPECT(mk, backend, PTRLEN_LITERAL("\xE2\xA0\x80\x0D\x0A"));
    reset(mk);

    ldisc_send(mk->ldisc, "\xF0\x90\x80\x80\xF0\x90\x80\x81", 8, false);
    EXPECT(mk, backend, PTRLEN_LITERAL(""));
    EXPECT_TERMINAL(mk, PTRLEN_LITERAL("\xF0\x90\x80\x80\xF0\x90\x80\x81"));
    ldisc_send(mk->ldisc, "\x08", -1, false);
    EXPECT(mk, backend, PTRLEN_LITERAL(""));
    EXPECT_TERMINAL(mk, PTRLEN_LITERAL("\xF0\x90\x80\x80\xF0\x90\x80\x81"
                                       "\x08 \x08"));
    ldisc_send(mk->ldisc, "\x0D", -1, false);
    EXPECT(mk, backend, PTRLEN_LITERAL("\xF0\x90\x80\x80\x0D\x0A"));
    reset(mk);

    /* Double-width characters (Hangul, as it happens) */
    ldisc_send(mk->ldisc, "\xEA\xB0\x80\xEA\xB0\x81", 6, false);
    EXPECT(mk, backend, PTRLEN_LITERAL(""));
    EXPECT_TERMINAL(mk, PTRLEN_LITERAL("\xEA\xB0\x80\xEA\xB0\x81"));
    ldisc_send(mk->ldisc, "\x08", -1, false);
    EXPECT(mk, backend, PTRLEN_LITERAL(""));
    EXPECT_TERMINAL(mk, PTRLEN_LITERAL("\xEA\xB0\x80\xEA\xB0\x81"
                                       "\x08 \x08\x08 \x08"));
    ldisc_send(mk->ldisc, "\x0D", -1, false);
    EXPECT(mk, backend, PTRLEN_LITERAL("\xEA\xB0\x80\x0D\x0A"));
    reset(mk);

    /* Zero-width characters */
    ldisc_send(mk->ldisc, "\xE2\x80\x8B\xE2\x80\x8B", 6, false);
    EXPECT(mk, backend, PTRLEN_LITERAL(""));
    EXPECT_TERMINAL(mk, PTRLEN_LITERAL("\xE2\x80\x8B\xE2\x80\x8B"));
    ldisc_send(mk->ldisc, "\x08", -1, false);
    EXPECT(mk, backend, PTRLEN_LITERAL(""));
    EXPECT_TERMINAL(mk, PTRLEN_LITERAL("\xE2\x80\x8B\xE2\x80\x8B"));
    ldisc_send(mk->ldisc, "\x0D", -1, false);
    EXPECT(mk, backend, PTRLEN_LITERAL("\xE2\x80\x8B\x0D\x0A"));
    reset(mk);

    /* And reset back to non-UTF-8 mode and expect high-bit-set bytes
     * to be treated individually, as characters in a single-byte
     * charset. (In our case, given the test config, that will be
     * CP437, but it makes no difference to the editing behaviour.) */
    mk->term->utf = false;
    ldisc_send(mk->ldisc, "\xC2\xA0\xC2\xA1", 4, false);
    EXPECT(mk, backend, PTRLEN_LITERAL(""));
    EXPECT_TERMINAL(mk, PTRLEN_LITERAL("\xC2\xA0\xC2\xA1"));
    ldisc_send(mk->ldisc, "\x08", -1, false);
    EXPECT(mk, backend, PTRLEN_LITERAL(""));
    EXPECT_TERMINAL(mk, PTRLEN_LITERAL("\xC2\xA0\xC2\xA1\x08 \x08"));
    ldisc_send(mk->ldisc, "\x0D", -1, false);
    EXPECT(mk, backend, PTRLEN_LITERAL("\xC2\xA0\xC2\x0D\x0A"));
    reset(mk);

    /* Make sure we flush all the terminal contents at the end of this
     * function */
    ldisc_send(mk->ldisc, "\x0D", 1, false);
    reset(mk);

#undef EXPECT_TERMINAL

}

const struct BackendVtable *const backends[] = { &mock_backend_vt, NULL };

int main(void)
{
    Mock *mk = mock_new();
    mk->term = term_init(mk->conf, mk->ucsdata, &mk->tw);
    Ldisc *ldisc = ldisc_create(mk->conf, mk->term, &mk->backend, &mk->seat);
    term_size(mk->term, 80, 24, 0);

    test_noedit(mk);
    test_edit(mk, true);
    test_edit(mk, false);

    ldisc_free(ldisc);

    bool failed = mk->any_test_failed;
    mock_free(mk);

    if (failed) {
        printf("Test suite FAILED!\n");
        return 1;
    } else {
        printf("Test suite passed\n");
        return 0;
    }
}
