local t = require('test.unit.testutil')
local itp = t.gen_itp(it)
local bit = require('bit')

--- @class vterm
--- @field ENC_UTF8 integer
--- @field VTERM_ATTR_BLINK integer
--- @field VTERM_ATTR_BOLD integer
--- @field VTERM_ATTR_FONT integer
--- @field VTERM_ATTR_ITALIC integer
--- @field VTERM_ATTR_REVERSE integer
--- @field VTERM_ATTR_UNDERLINE integer
--- @field VTERM_BASELINE_RAISE integer
--- @field VTERM_KEY_ENTER integer
--- @field VTERM_KEY_FUNCTION_0 integer
--- @field VTERM_KEY_KP_0 integer
--- @field VTERM_KEY_NONE integer
--- @field VTERM_KEY_TAB integer
--- @field VTERM_KEY_UP integer
--- @field VTERM_KEY_BACKSPACE integer
--- @field VTERM_KEY_ESCAPE integer
--- @field VTERM_KEY_DEL integer
--- @field VTERM_MOD_ALT integer
--- @field VTERM_MOD_CTRL integer
--- @field VTERM_MOD_SHIFT integer
--- @field parser_apc function
--- @field parser_csi function
--- @field parser_dcs function
--- @field parser_osc function
--- @field parser_pm function
--- @field parser_sos function
--- @field parser_text function
--- @field print_color function
--- @field schar_get fun(any, any):integer
--- @field screen_sb_clear function
--- @field screen_sb_popline function
--- @field screen_sb_pushline function
--- @field selection_query function
--- @field selection_set function
--- @field state_erase function
--- @field state_movecursor function
--- @field state_moverect function
--- @field state_pos function
--- @field state_putglyph function
--- @field state_sb_clear function
--- @field state_scrollrect function
--- @field state_setpenattr function
--- @field state_settermprop function
--- @field term_output function
--- @field utf_ptr2char fun(any):integer
--- @field utf_ptr2len fun(any):integer
--- @field vterm_input_write function
--- @field vterm_keyboard_end_paste function
--- @field vterm_keyboard_key function
--- @field vterm_keyboard_start_paste function
--- @field vterm_keyboard_unichar function
--- @field vterm_lookup_encoding fun(any, any):any
--- @field vterm_mouse_button function
--- @field vterm_mouse_move function
--- @field vterm_new fun(any, any):any
--- @field vterm_obtain_screen fun(any):any
--- @field vterm_obtain_state fun(any): any
--- @field vterm_output_set_callback function
--- @field vterm_parser_set_callbacks fun(any, any, any):any
--- @field vterm_screen_convert_color_to_rgb function
--- @field vterm_screen_enable_altscreen function
--- @field vterm_screen_enable_reflow function
--- @field vterm_screen_get_attrs_extent function
--- @field vterm_screen_get_cell function
--- @field vterm_screen_get_text fun(any, any, any, any):any
--- @field vterm_screen_is_eol fun(any, any):any
--- @field vterm_screen_reset function
--- @field vterm_screen_set_callbacks function
--- @field vterm_set_size function
--- @field vterm_set_utf8 fun(any, any, any):any
--- @field vterm_state_focus_in function
--- @field vterm_state_focus_out function
--- @field vterm_state_get_cursorpos fun(any, any)
--- @field vterm_state_get_lineinfo fun(any, any):any
--- @field vterm_state_get_penattr function
--- @field vterm_state_reset function
--- @field vterm_state_set_bold_highbright function
--- @field vterm_state_set_callbacks function
--- @field vterm_state_set_selection_callbacks function
--- @field vterm_state_set_unrecognised_fallbacks function
local vterm = t.cimport(
  './src/nvim/grid.h',
  './src/nvim/mbyte.h',
  './src/nvim/vterm/encoding.h',
  './src/nvim/vterm/keyboard.h',
  './src/nvim/vterm/mouse.h',
  './src/nvim/vterm/parser.h',
  './src/nvim/vterm/pen.h',
  './src/nvim/vterm/screen.h',
  './src/nvim/vterm/state.h',
  './src/nvim/vterm/vterm.h',
  './src/nvim/vterm/vterm_internal_defs.h',
  './test/unit/fixtures/vterm_test.h'
)

-- libvterm stores pointers to these callback structs instead of copying them.
-- Keep the cdata alive for as long as the owning handle exists, and let the
-- weak-key table release everything once the handle itself becomes unreachable.
local callback_structs = setmetatable({}, { __mode = 'k' })

---@param owner any
---@param ... any
local function keep_callback_structs(owner, ...)
  local refs = callback_structs[owner]
  if refs == nil then
    refs = {}
    callback_structs[owner] = refs
  end

  for i = 1, select('#', ...) do
    local ref = select(i, ...)
    if ref ~= nil then
      refs[#refs + 1] = ref
    end
  end
end

--- @return string
local function read_rm()
  local f = assert(io.open(t.paths.vterm_test_file, 'rb'))
  local text = f:read('*a')
  f:close()
  vim.fs.rm(t.paths.vterm_test_file, { force = true })
  return text
end

local function append(str)
  local f = assert(io.open(t.paths.vterm_test_file, 'a'))
  f:write(str)
  f:close()
  return 1
end

local function parser_control(control)
  return append(string.format('control %02x\n', control))
end

local function parser_escape(bytes)
  return append(string.format('escape %s\n', t.ffi.string(bytes)))
end

local function wantparser(vt)
  assert(vt)

  local parser_cbs = t.ffi.new('VTermParserCallbacks')
  parser_cbs['text'] = vterm.parser_text
  parser_cbs['control'] = parser_control
  parser_cbs['escape'] = parser_escape
  parser_cbs['csi'] = vterm.parser_csi
  parser_cbs['osc'] = vterm.parser_osc
  parser_cbs['dcs'] = vterm.parser_dcs
  parser_cbs['apc'] = vterm.parser_apc
  parser_cbs['pm'] = vterm.parser_pm
  parser_cbs['sos'] = vterm.parser_sos

  vterm.vterm_parser_set_callbacks(vt, parser_cbs, nil)
  keep_callback_structs(vt, parser_cbs)
end

--- @return any
local function init()
  local vt = vterm.vterm_new(25, 80)
  vterm.vterm_output_set_callback(vt, vterm.term_output, nil)
  vterm.vterm_set_utf8(vt, true)
  return vt
end

local function state_setlineinfo()
  return 1
end

--- @return any
local function wantstate(vt, opts)
  opts = opts or {}
  assert(vt)
  local state = vterm.vterm_obtain_state(vt)

  local state_cbs = t.ffi.new('VTermStateCallbacks')
  state_cbs['putglyph'] = vterm.state_putglyph
  state_cbs['movecursor'] = vterm.state_movecursor
  state_cbs['scrollrect'] = vterm.state_scrollrect
  state_cbs['moverect'] = vterm.state_moverect
  state_cbs['erase'] = vterm.state_erase
  state_cbs['setpenattr'] = vterm.state_setpenattr
  state_cbs['settermprop'] = vterm.state_settermprop
  state_cbs['setlineinfo'] = state_setlineinfo
  state_cbs['sb_clear'] = vterm.state_sb_clear

  local selection_cbs = t.ffi.new('VTermSelectionCallbacks')
  selection_cbs['set'] = vterm.selection_set
  selection_cbs['query'] = vterm.selection_query

  vterm.vterm_state_set_callbacks(state, state_cbs, nil)

  -- In some tests we want to check the behaviour of overflowing the buffer, so make it nicely small
  vterm.vterm_state_set_selection_callbacks(state, selection_cbs, nil, nil, 16)
  vterm.vterm_state_set_bold_highbright(state, 1)
  vterm.vterm_state_reset(state, 1)

  local fallback_cbs
  if opts.f then
    fallback_cbs = t.ffi.new('VTermStateFallbacks')
    fallback_cbs['control'] = parser_control
    fallback_cbs['csi'] = vterm.parser_csi
    fallback_cbs['osc'] = vterm.parser_osc
    fallback_cbs['dcs'] = vterm.parser_dcs
    fallback_cbs['apc'] = vterm.parser_apc
    fallback_cbs['pm'] = vterm.parser_pm
    fallback_cbs['sos'] = vterm.parser_sos
  end

  vterm.want_state_scrollback = opts.b or false
  vterm.want_state_erase = opts.e or false
  vterm.vterm_state_set_unrecognised_fallbacks(state, fallback_cbs, nil)
  vterm.want_state_putglyph = opts.g or false
  vterm.want_state_moverect = opts.m or false
  vterm.want_state_settermprop = opts.p or false
  vterm.want_state_scrollrect = opts.s or false

  keep_callback_structs(state, state_cbs, selection_cbs, fallback_cbs)
  return state
end

--- @return any
local function wantscreen(vt, opts)
  opts = opts or {}
  local screen = vterm.vterm_obtain_screen(vt)
  local screen_cbs = t.ffi.new('VTermScreenCallbacks')

  -- TODO(dundargoc): fix
  -- screen_cbs['damage']      = vterm.screen_damage
  screen_cbs['moverect'] = vterm.state_moverect
  screen_cbs['movecursor'] = vterm.state_movecursor
  screen_cbs['settermprop'] = vterm.state_settermprop
  screen_cbs['sb_pushline'] = vterm.screen_sb_pushline
  screen_cbs['sb_popline'] = vterm.screen_sb_popline
  screen_cbs['sb_clear'] = vterm.screen_sb_clear

  vterm.vterm_screen_set_callbacks(screen, screen_cbs, nil)

  if opts.a then
    vterm.vterm_screen_enable_altscreen(screen, 1)
  end
  vterm.want_screen_scrollback = opts.b or false
  vterm.want_state_movecursor = opts.c or false
  -- TODO(dundargoc): fix
  -- vterm.want_screen_damage = opts.d or opts.D or false
  -- vterm.want_screen_cells = opts.D or false
  vterm.want_state_moverect = opts.m or false
  vterm.want_state_settermprop = opts.p or false
  if opts.r then
    vterm.vterm_screen_enable_reflow(screen, true)
  end

  keep_callback_structs(screen, screen_cbs)
  return screen
end

local function reset(state, screen)
  if state then
    vterm.vterm_state_reset(state, 1)
    vterm.vterm_state_get_cursorpos(state, vterm.state_pos)
  end
  if screen then
    vterm.vterm_screen_reset(screen, 1)
  end
end

local function push(input, vt)
  vterm.vterm_input_write(vt, input, string.len(input))
end

-- vterm_input_write() can synchronously invoke the Lua callbacks installed
-- above. LuaJIT must not JIT-compile an FFI call that re-enters Lua, or the
-- test can panic with "bad callback".
if jit then
  jit.off(push, true)
end

local function expect(expected)
  local actual = read_rm()
  t.eq(expected .. '\n', actual)
end

local function expect_output(expected_preformat)
  local actual = read_rm()
  local expected = 'output '

  for c in string.gmatch(expected_preformat, '.') do
    if expected ~= 'output ' then
      expected = expected .. ','
    end
    expected = string.format('%s%x', expected, string.byte(c))
  end

  t.eq(expected .. '\n', actual)
end

local function cursor(row, col, state)
  local pos = t.ffi.new('VTermPos') --- @type {row: integer, col: integer}
  vterm.vterm_state_get_cursorpos(state, pos)
  t.eq(row, pos.row)
  t.eq(col, pos.col)
end

local function lineinfo(row, expected, state)
  local info = vterm.vterm_state_get_lineinfo(state, row)
  local dwl = info.doublewidth == 1
  local dhl = info.doubleheight == 1
  local cont = info.continuation == 1

  t.eq(dwl, expected.dwl or false)
  t.eq(dhl, expected.dhl or false)
  t.eq(cont, expected.cont or false)
end

local function pen(attribute, expected, state)
  local is_bool =
    { bold = true, italic = true, blink = true, reverse = true, dim = true, overline = true }
  local vterm_attribute = {
    bold = vterm.VTERM_ATTR_BOLD,
    underline = vterm.VTERM_ATTR_UNDERLINE,
    italic = vterm.VTERM_ATTR_ITALIC,
    blink = vterm.VTERM_ATTR_BLINK,
    reverse = vterm.VTERM_ATTR_REVERSE,
    font = vterm.VTERM_ATTR_FONT,
    dim = vterm.VTERM_ATTR_DIM,
    overline = vterm.VTERM_ATTR_OVERLINE,
  }

  local val = t.ffi.new('VTermValue') --- @type {boolean: integer}
  vterm.vterm_state_get_penattr(state, vterm_attribute[attribute], val)
  local actual = val.boolean --- @type integer|boolean
  if is_bool[attribute] then
    actual = val.boolean == 1
  end
  t.eq(expected, actual)
end

local function resize(rows, cols, vt)
  vterm.vterm_set_size(vt, rows, cols)
end

local function screen_chars(start_row, start_col, end_row, end_col, expected, screen)
  local rect = t.ffi.new('VTermRect')
  rect['start_row'] = start_row
  rect['start_col'] = start_col
  rect['end_row'] = end_row
  rect['end_col'] = end_col

  local len = vterm.vterm_screen_get_text(screen, nil, 0, rect)

  local text = t.ffi.new('unsigned char[?]', len)
  vterm.vterm_screen_get_text(screen, text, len, rect)

  local actual = t.ffi.string(text, len)
  t.eq(expected, actual)
end

local function screen_text(start_row, start_col, end_row, end_col, expected, screen)
  local rect = t.ffi.new('VTermRect')
  rect['start_row'] = start_row
  rect['start_col'] = start_col
  rect['end_row'] = end_row
  rect['end_col'] = end_col

  local len = vterm.vterm_screen_get_text(screen, nil, 0, rect)

  local text = t.ffi.new('unsigned char[?]', len)
  vterm.vterm_screen_get_text(screen, text, len, rect)

  local actual = ''
  for i = 0, tonumber(len) - 1 do
    actual = string.format('%s%02x,', actual, text[i])
  end
  actual = actual:sub(1, -2)

  t.eq(expected, actual)
end

--- @param row integer
local function screen_row(row, expected, screen, end_col)
  local rect = t.ffi.new('VTermRect')
  rect['start_row'] = row
  rect['start_col'] = 0
  rect['end_row'] = row + 1
  rect['end_col'] = end_col or 80

  local len = vterm.vterm_screen_get_text(screen, nil, 0, rect)

  local text = t.ffi.new('unsigned char[?]', len)
  vterm.vterm_screen_get_text(screen, text, len, rect)

  t.eq(expected, t.ffi.string(text, len))
end

local function screen_cell(row, col, expected, screen)
  local pos = t.ffi.new('VTermPos')
  pos['row'] = row
  pos['col'] = col

  local cell = t.ffi.new('VTermScreenCell') ---@type any
  vterm.vterm_screen_get_cell(screen, pos, cell)

  local buf = t.ffi.new('unsigned char[32]')
  vterm.schar_get(buf, cell.schar)

  local actual = '{'
  local i = 0
  while buf[i] > 0 do
    local char = vterm.utf_ptr2char(buf + i)
    local charlen = vterm.utf_ptr2len(buf + i)
    if i > 0 then
      actual = actual .. ','
    end
    local invalid = char >= 128 and charlen == 1
    actual = string.format('%s%s%02x', actual, invalid and '?' or '', char)
    i = i + charlen
  end
  actual = string.format('%s} width=%d attrs={', actual, cell['width'])
  actual = actual .. (cell['attrs'].bold ~= 0 and 'B' or '')
  actual = actual .. (cell['attrs'].dim ~= 0 and 'D' or '')
  actual = actual
    .. (cell['attrs'].underline ~= 0 and string.format('U%d', cell['attrs'].underline) or '')
  actual = actual .. (cell['attrs'].italic ~= 0 and 'I' or '')
  actual = actual .. (cell['attrs'].blink ~= 0 and 'K' or '')
  actual = actual .. (cell['attrs'].reverse ~= 0 and 'R' or '')
  actual = actual .. (cell['attrs'].overline ~= 0 and 'O' or '')
  actual = actual .. (cell['attrs'].font ~= 0 and string.format('F%d', cell['attrs'].font) or '')
  actual = actual .. (cell['attrs'].small ~= 0 and 'S' or '')
  if cell['attrs'].baseline ~= 0 then
    actual = actual .. (cell['attrs'].baseline == vterm.VTERM_BASELINE_RAISE and '^' or '_')
  end
  actual = actual .. '} '

  actual = actual .. (cell['attrs'].dwl ~= 0 and 'dwl ' or '')
  if cell['attrs'].dhl ~= 0 then
    actual = actual .. string.format('dhl-%s ', cell['attrs'].dhl == 2 and 'bottom' or 'top')
  end

  actual = string.format('%sfg=', actual)
  vterm.vterm_screen_convert_color_to_rgb(screen, cell['fg'])
  vterm.print_color(cell['fg'])

  actual = actual .. read_rm()
  actual = actual .. ' bg='

  vterm.vterm_screen_convert_color_to_rgb(screen, cell['bg'])
  vterm.print_color(cell['bg'])

  actual = actual .. read_rm()

  t.eq(expected, actual)
end

local function screen_eol(row, col, expected, screen)
  local pos = t.ffi.new('VTermPos')
  pos['row'] = row
  pos['col'] = col

  local is_eol = vterm.vterm_screen_is_eol(screen, pos)
  t.eq(expected, is_eol)
end

local function screen_attrs_extent(row, col, expected, screen)
  local pos = t.ffi.new('VTermPos')
  pos['row'] = row
  pos['col'] = col

  local rect = t.ffi.new('VTermRect')
  rect['start_col'] = 0
  rect['end_col'] = -1
  vterm.vterm_screen_get_attrs_extent(screen, rect, pos, 1)

  local actual = string.format(
    '%d,%d-%d,%d',
    rect['start_row'],
    rect['start_col'],
    rect['end_row'],
    rect['end_col']
  )

  t.eq(expected, actual)
end

local function wantencoding()
  local encoding = t.ffi.new('VTermEncodingInstance')
  encoding['enc'] = vterm.vterm_lookup_encoding(vterm.ENC_UTF8, string.byte('u'))
  if encoding.enc.init then
    encoding.enc.init(encoding.enc, encoding['data'])
  end
  return encoding
end

local function encin(input, encoding)
  local len = string.len(input)

  local cp = t.ffi.new('uint32_t[?]', len)
  local cpi = t.ffi.new('int[1]')
  local pos = t.ffi.new('size_t[1]', 0)

  encoding.enc.decode(encoding.enc, encoding.data, cp, cpi, len, input, pos, len)

  local f = assert(io.open(t.paths.vterm_test_file, 'w'))
  if tonumber(cpi[0]) > 0 then
    f:write('encout ')
    for i = 0, cpi[0] - 1 do
      if i == 0 then
        f:write(string.format('%x', cp[i]))
      else
        f:write(string.format(',%x', cp[i]))
      end
    end
    f:write('\n')
  end
  f:close()
end

local function strpe_modifiers(input_mod)
  local mod = t.ffi.new('VTermModifier') ---@type any
  if input_mod.C then
    mod = bit.bor(mod, vterm.VTERM_MOD_CTRL)
  end
  if input_mod.S then
    mod = bit.bor(mod, vterm.VTERM_MOD_SHIFT)
  end
  if input_mod.A then
    mod = bit.bor(mod, vterm.VTERM_MOD_ALT)
  end
  return mod
end

local function strp_key(input_key)
  if input_key == 'up' then
    return vterm.VTERM_KEY_UP
  end

  if input_key == 'tab' then
    return vterm.VTERM_KEY_TAB
  end

  if input_key == 'enter' then
    return vterm.VTERM_KEY_ENTER
  end

  if input_key == 'bs' then
    return vterm.VTERM_KEY_BACKSPACE
  end

  if input_key == 'del' then
    return vterm.VTERM_KEY_DEL
  end

  if input_key == 'esc' then
    return vterm.VTERM_KEY_ESCAPE
  end

  if input_key == 'f1' then
    return vterm.VTERM_KEY_FUNCTION_0 + 1
  end

  if input_key == 'kp0' then
    return vterm.VTERM_KEY_KP_0
  end

  return vterm.VTERM_KEY_NONE
end

local function mousemove(row, col, vt, input_mod)
  input_mod = input_mod or {}
  local mod = strpe_modifiers(input_mod)
  vterm.vterm_mouse_move(vt, row, col, mod)
end

local function mousebtn(press, button, vt, input_mod)
  input_mod = input_mod or {}
  local mod = strpe_modifiers(input_mod)
  local flag = press == 'd' or press == 'D'
  vterm.vterm_mouse_button(vt, button, flag, mod)
end

local function inchar(c, vt, input_mod)
  input_mod = input_mod or {}
  local mod = strpe_modifiers(input_mod)
  vterm.vterm_keyboard_unichar(vt, c, mod)
end

local function inkey(input_key, vt, input_mod)
  input_mod = input_mod or {}
  local mod = strpe_modifiers(input_mod)
  local key = strp_key(input_key)
  vterm.vterm_keyboard_key(vt, key, mod)
end

before_each(function()
  vim.fs.rm(t.paths.vterm_test_file, { force = true })
end)

describe('vterm', function()
  itp('02parser', function()
    local vt = init()
    vterm.vterm_set_utf8(vt, false)
    wantparser(vt)

    -- Basic text
    push('hello', vt)
    expect('text 68,65,6c,6c,6f')

    -- C0
    push('\x03', vt)
    expect('control 03')
    push('\x1f', vt)
    expect('control 1f')

    -- C1 8bit
    push('\x83', vt)
    expect('control 83')
    push('\x99', vt)
    expect('control 99')

    -- C1 7bit
    push('\x1b\x43', vt)
    expect('control 83')
    push('\x1b\x59', vt)
    expect('control 99')

    -- High bytes
    push('\xa0\xcc\xfe', vt)
    expect('text a0,cc,fe')

    -- Mixed
    push('1\n2', vt)
    expect('text 31\ncontrol 0a\ntext 32')

    -- Escape
    push('\x1b=', vt)
    expect('escape =')

    -- Escape 2-byte
    push('\x1b(X', vt)
    expect('escape (X')

    -- Split write Escape
    push('\x1b(', vt)
    push('Y', vt)
    expect('escape (Y')

    -- Escape cancels Escape, starts another
    push('\x1b(\x1b)Z', vt)
    expect('escape )Z')

    -- CAN cancels Escape, returns to normal mode
    push('\x1b(\x18AB', vt)
    expect('text 41,42')

    -- C0 in Escape interrupts and continues
    push('\x1b(\nX', vt)
    expect('control 0a\nescape (X')

    -- CSI 0 args
    push('\x1b[a', vt)
    expect('csi 61 *')

    -- CSI 1 arg
    push('\x1b[9b', vt)
    expect('csi 62 9')

    -- CSI 2 args
    push('\x1b[3;4c', vt)
    expect('csi 63 3,4')

    -- CSI 1 arg 1 sub
    push('\x1b[1:2c', vt)
    expect('csi 63 1+,2')

    -- CSI many digits
    push('\x1b[678d', vt)
    expect('csi 64 678')

    -- CSI leading zero
    push('\x1b[007e', vt)
    expect('csi 65 7')

    -- CSI qmark
    push('\x1b[?2;7f', vt)
    expect('csi 66 L=3f 2,7')

    -- CSI greater
    push('\x1b[>c', vt)
    expect('csi 63 L=3e *')

    -- CSI SP
    push('\x1b[12 q', vt)
    expect('csi 71 12 I=20')

    -- Mixed CSI
    push('A\x1b[8mB', vt)
    expect('text 41\ncsi 6d 8\ntext 42')

    -- Split write
    push('\x1b', vt)
    push('[a', vt)
    expect('csi 61 *')
    push('foo\x1b[', vt)
    expect('text 66,6f,6f')
    push('4b', vt)
    expect('csi 62 4')
    push('\x1b[12;', vt)
    push('3c', vt)
    expect('csi 63 12,3')

    -- Escape cancels CSI, starts Escape
    push('\x1b[123\x1b9', vt)
    expect('escape 9')

    -- CAN cancels CSI, returns to normal mode
    push('\x1b[12\x18AB', vt)
    expect('text 41,42')

    -- C0 in Escape interrupts and continues
    push('\x1b(\nX', vt)
    expect('control 0a\nescape (X')

    -- OSC BEL
    push('\x1b]1;Hello\x07', vt)
    expect('osc [1;Hello]')

    -- OSC ST (7bit)
    push('\x1b]1;Hello\x1b\\', vt)
    expect('osc [1;Hello]')

    -- OSC ST (8bit)
    push('\x9d1;Hello\x9c', vt)
    expect('osc [1;Hello]')

    -- OSC in parts
    push('\x1b]52;abc', vt)
    expect('osc [52;abc')
    push('def', vt)
    expect('osc def')
    push('ghi\x1b\\', vt)
    expect('osc ghi]')

    -- OSC BEL without semicolon
    push('\x1b]1234\x07', vt)
    expect('osc [1234;]')

    -- OSC ST without semicolon
    push('\x1b]1234\x1b\\', vt)
    expect('osc [1234;]')

    -- Escape cancels OSC, starts Escape
    push('\x1b]Something\x1b9', vt)
    expect('escape 9')

    -- CAN cancels OSC, returns to normal mode
    push('\x1b]12\x18AB', vt)
    expect('text 41,42')

    -- C0 in OSC interrupts and continues
    push('\x1b]2;\nBye\x07', vt)
    expect('osc [2;\ncontrol 0a\nosc Bye]')

    -- DCS BEL
    push('\x1bPHello\x07', vt)
    expect('dcs [Hello]')

    -- DCS ST (7bit)
    push('\x1bPHello\x1b\\', vt)
    expect('dcs [Hello]')

    -- DCS ST (8bit)
    push('\x90Hello\x9c', vt)
    expect('dcs [Hello]')

    -- Split write of 7bit ST
    push('\x1bPABC\x1b', vt)
    expect('dcs [ABC')
    push('\\', vt)
    expect('dcs ]')

    -- Escape cancels DCS, starts Escape
    push('\x1bPSomething\x1b9', vt)
    expect('escape 9')

    -- CAN cancels DCS, returns to normal mode
    push('\x1bP12\x18AB', vt)
    expect('text 41,42')

    -- C0 in OSC interrupts and continues
    push('\x1bPBy\ne\x07', vt)
    expect('dcs [By\ncontrol 0a\ndcs e]')

    -- APC BEL
    push('\x1b_Hello\x07', vt)
    expect('apc [Hello]')

    -- APC ST (7bit)
    push('\x1b_Hello\x1b\\', vt)
    expect('apc [Hello]')

    -- APC ST (8bit)
    push('\x9fHello\x9c', vt)
    expect('apc [Hello]')

    -- PM BEL
    push('\x1b^Hello\x07', vt)
    expect('pm [Hello]')

    -- PM ST (7bit)
    push('\x1b^Hello\x1b\\', vt)
    expect('pm [Hello]')

    -- PM ST (8bit)
    push('\x9eHello\x9c', vt)
    expect('pm [Hello]')

    -- SOS BEL
    push('\x1bXHello\x07', vt)
    expect('sos [Hello]')

    -- SOS ST (7bit)
    push('\x1bXHello\x1b\\', vt)
    expect('sos [Hello]')

    -- SOS ST (8bit)
    push('\x98Hello\x9c', vt)
    expect('sos [Hello]')

    push('\x1bXABC\x01DEF\x1b\\', vt)
    expect('sos [ABC\x01DEF]')
    push('\x1bXABC\x99DEF\x1b\\', vt)
    expect('sos [ABC\x99DEF]')

    -- NUL ignored
    push('\x00', vt)

    -- NUL ignored within CSI
    push('\x1b[12\x003m', vt)
    expect('csi 6d 123')

    -- DEL ignored
    push('\x7f', vt)

    -- DEL ignored within CSI
    push('\x1b[12\x7f3m', vt)
    expect('csi 6d 123')

    -- DEL inside text"
    push('AB\x7fC', vt)
    expect('text 41,42\ntext 43')
  end)

  itp('03encoding_utf8', function()
    local encoding = wantencoding()

    -- Low
    encin('123', encoding)
    expect('encout 31,32,33')

    -- We want to prove the UTF-8 parser correctly handles all the sequences.
    -- Easy way to do this is to check it does low/high boundary cases, as that
    -- leaves only two for each sequence length
    --
    -- These ranges are therefore:
    --
    -- Two bytes:
    -- U+0080 = 000 10000000 =>    00010   000000
    --                       => 11000010 10000000 = C2 80
    -- U+07FF = 111 11111111 =>    11111   111111
    --                       => 11011111 10111111 = DF BF
    --
    -- Three bytes:
    -- U+0800 = 00001000 00000000 =>     0000   100000   000000
    --                            => 11100000 10100000 10000000 = E0 A0 80
    -- U+FFFD = 11111111 11111101 =>     1111   111111   111101
    --                            => 11101111 10111111 10111101 = EF BF BD
    -- (We avoid U+FFFE and U+FFFF as they're invalid codepoints)
    --
    -- Four bytes:
    -- U+10000  = 00001 00000000 00000000 =>      000   010000   000000   000000
    --                                    => 11110000 10010000 10000000 10000000 = F0 90 80 80
    -- U+1FFFFF = 11111 11111111 11111111 =>      111   111111   111111   111111
    --                                    => 11110111 10111111 10111111 10111111 = F7 BF BF BF

    -- 2 byte
    encin('\xC2\x80\xDF\xBF', encoding)
    expect('encout 80,7ff')

    -- 3 byte
    encin('\xE0\xA0\x80\xEF\xBF\xBD', encoding)
    expect('encout 800,fffd')

    -- 4 byte
    encin('\xF0\x90\x80\x80\xF7\xBF\xBF\xBF', encoding)
    expect('encout 10000,1fffff')

    -- Next up, we check some invalid sequences
    --  + Early termination (back to low bytes too soon)
    --  + Early restart (another sequence introduction before the previous one was finished)

    -- Early termination
    encin('\xC2!', encoding)
    expect('encout fffd,21')

    encin('\xE0!\xE0\xA0!', encoding)
    expect('encout fffd,21,fffd,21')

    encin('\xF0!\xF0\x90!\xF0\x90\x80!', encoding)
    expect('encout fffd,21,fffd,21,fffd,21')

    -- Early restart
    encin('\xC2\xC2\x90', encoding)
    expect('encout fffd,90')

    encin('\xE0\xC2\x90\xE0\xA0\xC2\x90', encoding)
    expect('encout fffd,90,fffd,90')

    encin('\xF0\xC2\x90\xF0\x90\xC2\x90\xF0\x90\x80\xC2\x90', encoding)
    expect('encout fffd,90,fffd,90,fffd,90')

    -- Test the overlong sequences by giving an overlong encoding of U+0000 and
    -- an encoding of the highest codepoint still too short
    --
    -- Two bytes:
    -- U+0000 = C0 80
    -- U+007F = 000 01111111 =>    00001   111111 =>
    --                       => 11000001 10111111 => C1 BF
    --
    -- Three bytes:
    -- U+0000 = E0 80 80
    -- U+07FF = 00000111 11111111 =>     0000   011111   111111
    --                            => 11100000 10011111 10111111 = E0 9F BF
    --
    -- Four bytes:
    -- U+0000 = F0 80 80 80
    -- U+FFFF = 11111111 11111111 =>      000   001111   111111   111111
    --                            => 11110000 10001111 10111111 10111111 = F0 8F BF BF

    -- Overlong
    encin('\xC0\x80\xC1\xBF', encoding)
    expect('encout fffd,fffd')

    encin('\xE0\x80\x80\xE0\x9F\xBF', encoding)
    expect('encout fffd,fffd')

    encin('\xF0\x80\x80\x80\xF0\x8F\xBF\xBF', encoding)
    expect('encout fffd,fffd')

    -- UTF-16 surrogates U+D800 and U+DFFF
    -- UTF-16 Surrogates
    encin('\xED\xA0\x80\xED\xBF\xBF', encoding)
    expect('encout fffd,fffd')

    -- Split write
    encin('\xC2', encoding)
    encin('\xA0', encoding)
    expect('encout a0')

    encin('\xE0', encoding)
    encin('\xA0\x80', encoding)
    expect('encout 800')
    encin('\xE0\xA0', encoding)
    encin('\x80', encoding)
    expect('encout 800')

    encin('\xF0', encoding)
    encin('\x90\x80\x80', encoding)
    expect('encout 10000')
    encin('\xF0\x90', encoding)
    encin('\x80\x80', encoding)
    expect('encout 10000')
    encin('\xF0\x90\x80', encoding)
    encin('\x80', encoding)
    expect('encout 10000')
  end)

  itp('10state_putglyph', function()
    local vt = init()
    local state = wantstate(vt, { g = true })

    -- Low
    reset(state, nil)
    push('ABC', vt)
    expect('putglyph 41 1 0,0\nputglyph 42 1 0,1\nputglyph 43 1 0,2')

    -- UTF-8 1 char
    -- U+00C1 = 0xC3 0x81  name: LATIN CAPITAL LETTER A WITH ACUTE
    -- U+00E9 = 0xC3 0xA9  name: LATIN SMALL LETTER E WITH ACUTE
    reset(state, nil)
    push('\xC3\x81\xC3\xA9', vt)
    expect('putglyph c1 1 0,0\nputglyph e9 1 0,1')

    -- UTF-8 split writes
    reset(state, nil)
    push('\xC3', vt)
    push('\x81', vt)
    expect('putglyph c1 1 0,0')

    -- UTF-8 wide char
    -- U+FF10 = EF BC 90  name: FULLWIDTH DIGIT ZERO
    reset(state, nil)
    push('\xEF\xBC\x90 ', vt)
    expect('putglyph ff10 2 0,0\nputglyph 20 1 0,2')

    -- UTF-8 emoji wide char
    -- U+1F600 = F0 9F 98 80  name: GRINNING FACE
    reset(state, nil)
    push('\xF0\x9F\x98\x80 ', vt)
    expect('putglyph 1f600 2 0,0\nputglyph 20 1 0,2')

    -- UTF-8 combining chars
    -- U+0301 = CC 81  name: COMBINING ACUTE
    reset(state, nil)
    push('e\xCC\x81Z', vt)
    expect('putglyph 65,301 1 0,0\nputglyph 5a 1 0,1')

    -- Combining across buffers
    reset(state, nil)
    push('e', vt)
    expect('putglyph 65 1 0,0')
    push('\xCC\x81Z', vt)
    expect('putglyph 65,301 1 0,0\nputglyph 5a 1 0,1')

    -- Spare combining chars get truncated
    reset(state, nil)
    push('e' .. string.rep('\xCC\x81', 20), vt)
    expect('putglyph 65,301,301,301,301,301,301,301,301,301,301,301,301,301,301 1 0,0') -- and nothing more

    -- Combining across buffers multiple times
    reset(state, nil)
    push('e', vt)
    expect('putglyph 65 1 0,0')
    push('\xCC\x81', vt)
    expect('putglyph 65,301 1 0,0')
    push('\xCC\x82', vt)
    expect('putglyph 65,301,302 1 0,0')

    -- Combining across buffers at right edge
    reset(state, nil)
    push('\x1b[5;80H', vt)
    push('e', vt)
    expect('putglyph 65 1 4,79')
    push('\xCC\x81', vt)
    expect('putglyph 65,301 1 4,79')
    push('\xCC\x82Z', vt)
    expect('putglyph 65,301,302 1 4,79\nputglyph 5a 1 5,0')

    -- Combining regional indicators
    reset(state, nil)
    push('\x1b[5;77H', vt)
    push('🇦', vt)
    expect('putglyph 1f1e6 2 4,76')
    push('🇩', vt)
    expect('putglyph 1f1e6,1f1e9 2 4,76')
    push('🇱', vt)
    expect('putglyph 1f1f1 2 4,78')
    push('🇮', vt)
    expect('putglyph 1f1f1,1f1ee 2 4,78')
    push('🇲', vt)
    expect('putglyph 1f1f2 2 5,0')
    push('🇨', vt)
    expect('putglyph 1f1f2,1f1e8 2 5,0')

    -- emoji with ZWJ and variant selectors, as one chunk
    reset(state, nil)
    push('🏳️‍🌈🏳️‍⚧️🏴‍☠️', vt)
    expect([[putglyph 1f3f3,fe0f,200d,1f308 2 0,0
putglyph 1f3f3,fe0f,200d,26a7,fe0f 2 0,2
putglyph 1f3f4,200d,2620,fe0f 2 0,4]])

    -- emoji, one code point at a time
    reset(state, nil)
    push('🏳', vt)
    expect('putglyph 1f3f3 2 0,0')
    push('\xef\xb8\x8f', vt)
    expect('putglyph 1f3f3,fe0f 2 0,0')
    push('\xe2\x80\x8d', vt)
    expect('putglyph 1f3f3,fe0f,200d 2 0,0')
    push('🌈', vt)
    expect('putglyph 1f3f3,fe0f,200d,1f308 2 0,0')

    -- modifier can change width
    push('❤', vt)
    expect('putglyph 2764 1 0,2')
    push('\xef\xb8\x8f', vt)
    expect('putglyph 2764,fe0f 2 0,2')

    -- also works batched
    push('❤️', vt)
    expect('putglyph 2764,fe0f 2 0,4')

    -- DECSCA protected
    reset(state, nil)
    push('A\x1b[1"qB\x1b[2"qC', vt)
    expect('putglyph 41 1 0,0\nputglyph 42 1 0,1 prot\nputglyph 43 1 0,2')
  end)

  itp('11state_movecursor', function()
    local vt = init()
    local state = wantstate(vt)

    -- Implicit
    push('ABC', vt)
    cursor(0, 3, state)

    -- Backspace
    push('\b', vt)
    cursor(0, 2, state)
    -- Horizontal Tab
    push('\t', vt)
    cursor(0, 8, state)
    -- Carriage Return
    push('\r', vt)
    cursor(0, 0, state)
    -- Linefeed
    push('\n', vt)
    cursor(1, 0, state)

    -- Backspace bounded by lefthand edge
    push('\x1b[4;2H', vt)
    cursor(3, 1, state)
    push('\b', vt)
    cursor(3, 0, state)
    push('\b', vt)
    cursor(3, 0, state)

    -- Backspace cancels phantom
    push('\x1b[4;80H', vt)
    cursor(3, 79, state)
    push('X', vt)
    cursor(3, 79, state)
    push('\b', vt)
    cursor(3, 78, state)

    -- HT bounded by righthand edge
    push('\x1b[1;78H', vt)
    cursor(0, 77, state)
    push('\t', vt)
    cursor(0, 79, state)
    push('\t', vt)
    cursor(0, 79, state)

    reset(state, nil)

    -- Index
    push('ABC\x1bD', vt)
    cursor(1, 3, state)
    -- Reverse Index
    push('\x1bM', vt)
    cursor(0, 3, state)
    -- Newline
    push('\x1bE', vt)
    cursor(1, 0, state)

    reset(state, nil)

    -- Cursor Forward
    push('\x1b[B', vt)
    cursor(1, 0, state)
    push('\x1b[3B', vt)
    cursor(4, 0, state)
    push('\x1b[0B', vt)
    cursor(5, 0, state)

    -- Cursor Down
    push('\x1b[C', vt)
    cursor(5, 1, state)
    push('\x1b[3C', vt)
    cursor(5, 4, state)
    push('\x1b[0C', vt)
    cursor(5, 5, state)

    -- Cursor Up
    push('\x1b[A', vt)
    cursor(4, 5, state)
    push('\x1b[3A', vt)
    cursor(1, 5, state)
    push('\x1b[0A', vt)
    cursor(0, 5, state)

    -- Cursor Backward
    push('\x1b[D', vt)
    cursor(0, 4, state)
    push('\x1b[3D', vt)
    cursor(0, 1, state)
    push('\x1b[0D', vt)
    cursor(0, 0, state)

    -- Cursor Next Line
    push('   ', vt)
    cursor(0, 3, state)
    push('\x1b[E', vt)
    cursor(1, 0, state)
    push('   ', vt)
    cursor(1, 3, state)
    push('\x1b[2E', vt)
    cursor(3, 0, state)
    push('\x1b[0E', vt)
    cursor(4, 0, state)

    -- Cursor Previous Line
    push('   ', vt)
    cursor(4, 3, state)
    push('\x1b[F', vt)
    cursor(3, 0, state)
    push('   ', vt)
    cursor(3, 3, state)
    push('\x1b[2F', vt)
    cursor(1, 0, state)
    push('\x1b[0F', vt)
    cursor(0, 0, state)

    -- Cursor Horizontal Absolute
    push('\n', vt)
    cursor(1, 0, state)
    push('\x1b[20G', vt)
    cursor(1, 19, state)
    push('\x1b[G', vt)
    cursor(1, 0, state)

    -- Cursor Position
    push('\x1b[10;5H', vt)
    cursor(9, 4, state)
    push('\x1b[8H', vt)
    cursor(7, 0, state)
    push('\x1b[H', vt)
    cursor(0, 0, state)

    -- Cursor Position cancels phantom
    push('\x1b[10;78H', vt)
    cursor(9, 77, state)
    push('ABC', vt)
    cursor(9, 79, state)
    push('\x1b[10;80H', vt)
    push('C', vt)
    cursor(9, 79, state)
    push('X', vt)
    cursor(10, 1, state)

    reset(state, nil)

    -- Bounds Checking
    push('\x1b[A', vt)
    cursor(0, 0, state)
    push('\x1b[D', vt)
    cursor(0, 0, state)
    push('\x1b[25;80H', vt)
    cursor(24, 79, state)
    push('\x1b[B', vt)
    cursor(24, 79, state)
    push('\x1b[C', vt)
    cursor(24, 79, state)
    push('\x1b[E', vt)
    cursor(24, 0, state)
    push('\x1b[H', vt)
    cursor(0, 0, state)
    push('\x1b[F', vt)
    cursor(0, 0, state)
    push('\x1b[999G', vt)
    cursor(0, 79, state)
    push('\x1b[99;99H', vt)
    cursor(24, 79, state)

    reset(state, nil)

    -- Horizontal Position Absolute
    push('\x1b[5`', vt)
    cursor(0, 4, state)

    -- Horizontal Position Relative
    push('\x1b[3a', vt)
    cursor(0, 7, state)

    -- Horizontal Position Backward
    push('\x1b[3j', vt)
    cursor(0, 4, state)

    -- Horizontal and Vertical Position
    push('\x1b[3;3f', vt)
    cursor(2, 2, state)

    -- Vertical Position Absolute
    push('\x1b[5d', vt)
    cursor(4, 2, state)

    -- Vertical Position Relative
    push('\x1b[2e', vt)
    cursor(6, 2, state)

    -- Vertical Position Backward
    push('\x1b[2k', vt)
    cursor(4, 2, state)

    reset(state, nil)

    -- Horizontal Tab
    push('\t', vt)
    cursor(0, 8, state)
    push('   ', vt)
    cursor(0, 11, state)
    push('\t', vt)
    cursor(0, 16, state)
    push('       ', vt)
    cursor(0, 23, state)
    push('\t', vt)
    cursor(0, 24, state)
    push('        ', vt)
    cursor(0, 32, state)
    push('\t', vt)
    cursor(0, 40, state)

    -- Cursor Horizontal Tab
    push('\x1b[I', vt)
    cursor(0, 48, state)
    push('\x1b[2I', vt)
    cursor(0, 64, state)

    -- Cursor Backward Tab
    push('\x1b[Z', vt)
    cursor(0, 56, state)
    push('\x1b[2Z', vt)
    cursor(0, 40, state)
  end)

  itp('12state_scroll', function()
    local vt = init()
    local state = wantstate(vt, { s = true })

    -- Linefeed
    push(string.rep('\n', 24), vt)
    cursor(24, 0, state)
    push('\n', vt)
    expect('scrollrect 0..25,0..80 => +1,+0')
    cursor(24, 0, state)

    reset(state, nil)

    -- Index
    push('\x1b[25H', vt)
    push('\x1bD', vt)
    expect('scrollrect 0..25,0..80 => +1,+0')

    reset(state, nil)

    -- Reverse Index
    push('\x1bM', vt)
    expect('scrollrect 0..25,0..80 => -1,+0')

    reset(state, nil)

    -- Linefeed in DECSTBM
    push('\x1b[1;10r', vt)
    cursor(0, 0, state)
    push(string.rep('\n', 9), vt)
    cursor(9, 0, state)
    push('\n', vt)
    expect('scrollrect 0..10,0..80 => +1,+0')
    cursor(9, 0, state)

    -- Linefeed outside DECSTBM
    push('\x1b[20H', vt)
    cursor(19, 0, state)
    push('\n', vt)
    cursor(20, 0, state)

    -- Index in DECSTBM
    push('\x1b[9;10r', vt)
    push('\x1b[10H', vt)
    push('\x1bM', vt)
    cursor(8, 0, state)
    push('\x1bM', vt)
    expect('scrollrect 8..10,0..80 => -1,+0')

    -- Reverse Index in DECSTBM
    push('\x1b[25H', vt)
    cursor(24, 0, state)
    push('\n', vt)
    -- no scrollrect
    cursor(24, 0, state)

    -- Linefeed in DECSTBM+DECSLRM
    push('\x1b[?69h', vt)
    push('\x1b[3;10r\x1b[10;40s', vt)
    push('\x1b[10;10H\n', vt)
    expect('scrollrect 2..10,9..40 => +1,+0')

    -- IND/RI in DECSTBM+DECSLRM
    push('\x1bD', vt)
    expect('scrollrect 2..10,9..40 => +1,+0')
    push('\x1b[3;10H\x1bM', vt)
    expect('scrollrect 2..10,9..40 => -1,+0')

    -- DECRQSS on DECSTBM
    push('\x1bP$qr\x1b\\', vt)
    expect_output('\x1bP1$r3;10r\x1b\\')

    -- DECRQSS on DECSLRM
    push('\x1bP$qs\x1b\\', vt)
    expect_output('\x1bP1$r10;40s\x1b\\')

    -- Setting invalid DECSLRM with !DECVSSM is still rejected
    push('\x1b[?69l\x1b[;0s\x1b[?69h', vt)

    reset(state, nil)

    -- Scroll Down
    push('\x1b[S', vt)
    expect('scrollrect 0..25,0..80 => +1,+0')
    cursor(0, 0, state)
    push('\x1b[2S', vt)
    expect('scrollrect 0..25,0..80 => +2,+0')
    cursor(0, 0, state)
    push('\x1b[100S', vt)
    expect('scrollrect 0..25,0..80 => +25,+0')

    -- Scroll Up
    push('\x1b[T', vt)
    expect('scrollrect 0..25,0..80 => -1,+0')
    cursor(0, 0, state)
    push('\x1b[2T', vt)
    expect('scrollrect 0..25,0..80 => -2,+0')
    cursor(0, 0, state)
    push('\x1b[100T', vt)
    expect('scrollrect 0..25,0..80 => -25,+0')

    -- SD/SU in DECSTBM
    push('\x1b[5;20r', vt)
    push('\x1b[S', vt)
    expect('scrollrect 4..20,0..80 => +1,+0')
    push('\x1b[T', vt)
    expect('scrollrect 4..20,0..80 => -1,+0')

    reset(state, nil)

    -- SD/SU in DECSTBM+DECSLRM
    push('\x1b[?69h', vt)
    push('\x1b[3;10r\x1b[10;40s', vt)
    cursor(0, 0, state)
    push('\x1b[3;10H', vt)
    cursor(2, 9, state)
    push('\x1b[S', vt)
    expect('scrollrect 2..10,9..40 => +1,+0')
    push('\x1b[?69l', vt)
    push('\x1b[S', vt)
    expect('scrollrect 2..10,0..80 => +1,+0')

    -- Invalid boundaries
    reset(state, nil)

    push('\x1b[100;105r\x1bD', vt)
    push('\x1b[5;2r\x1bD', vt)

    reset(state, nil)
    state = wantstate(vt, { m = true, e = true })

    -- Scroll Down move+erase emulation
    push('\x1b[S', vt)
    expect('moverect 1..25,0..80 -> 0..24,0..80\nerase 24..25,0..80')
    cursor(0, 0, state)
    push('\x1b[2S', vt)
    expect('moverect 2..25,0..80 -> 0..23,0..80\nerase 23..25,0..80')
    cursor(0, 0, state)

    -- Scroll Up move+erase emulation
    push('\x1b[T', vt)
    expect('moverect 0..24,0..80 -> 1..25,0..80\nerase 0..1,0..80')
    cursor(0, 0, state)
    push('\x1b[2T', vt)
    expect('moverect 0..23,0..80 -> 2..25,0..80\nerase 0..2,0..80')
    cursor(0, 0, state)

    -- DECSTBM resets cursor position
    push('\x1b[5;5H', vt)
    cursor(4, 4, state)
    push('\x1b[r', vt)
    cursor(0, 0, state)
  end)

  itp('13state_edit', function()
    local vt = init()
    local state = wantstate(vt, { s = true, e = true, b = true })

    -- ICH
    reset(state, nil)
    expect('erase 0..25,0..80')
    cursor(0, 0, state)
    push('ACD', vt)
    push('\x1b[2D', vt)
    cursor(0, 1, state)
    push('\x1b[@', vt)
    expect('scrollrect 0..1,1..80 => +0,-1')
    cursor(0, 1, state)
    push('B', vt)
    cursor(0, 2, state)
    push('\x1b[3@', vt)
    expect('scrollrect 0..1,2..80 => +0,-3')

    -- ICH with DECSLRM
    push('\x1b[?69h', vt)
    push('\x1b[;50s', vt)
    push('\x1b[20G\x1b[@', vt)
    expect('scrollrect 0..1,19..50 => +0,-1')

    -- ICH outside DECSLRM
    push('\x1b[70G\x1b[@', vt)
    -- nothing happens

    -- DCH
    reset(state, nil)
    expect('erase 0..25,0..80')
    cursor(0, 0, state)
    push('ABBC', vt)
    push('\x1b[3D', vt)
    cursor(0, 1, state)
    push('\x1b[P', vt)
    expect('scrollrect 0..1,1..80 => +0,+1')
    cursor(0, 1, state)
    push('\x1b[3P', vt)
    expect('scrollrect 0..1,1..80 => +0,+3')
    cursor(0, 1, state)

    -- DCH with DECSLRM
    push('\x1b[?69h', vt)
    push('\x1b[;50s', vt)
    push('\x1b[20G\x1b[P', vt)
    expect('scrollrect 0..1,19..50 => +0,+1')

    -- DCH outside DECSLRM
    push('\x1b[70G\x1b[P', vt)
    -- nothing happens

    -- ECH
    reset(state, nil)
    expect('erase 0..25,0..80')
    cursor(0, 0, state)
    push('ABC', vt)
    push('\x1b[2D', vt)
    cursor(0, 1, state)
    push('\x1b[X', vt)
    expect('erase 0..1,1..2')
    cursor(0, 1, state)
    push('\x1b[3X', vt)
    expect('erase 0..1,1..4')
    cursor(0, 1, state)
    -- ECH more columns than there are should be bounded
    push('\x1b[100X', vt)
    expect('erase 0..1,1..80')

    -- IL
    reset(state, nil)
    expect('erase 0..25,0..80')
    cursor(0, 0, state)
    push('A\r\nC', vt)
    cursor(1, 1, state)
    push('\x1b[L', vt)
    expect('scrollrect 1..25,0..80 => -1,+0')
    -- TODO(libvterm): ECMA-48 says we should move to line home, but neither xterm nor xfce4-terminal do this
    cursor(1, 1, state)
    push('\rB', vt)
    cursor(1, 1, state)
    push('\x1b[3L', vt)
    expect('scrollrect 1..25,0..80 => -3,+0')

    -- IL with DECSTBM
    push('\x1b[5;15r', vt)
    push('\x1b[5H\x1b[L', vt)
    expect('scrollrect 4..15,0..80 => -1,+0')

    -- IL outside DECSTBM
    push('\x1b[20H\x1b[L', vt)
    -- nothing happens

    -- IL with DECSTBM+DECSLRM
    push('\x1b[?69h', vt)
    push('\x1b[10;50s', vt)
    push('\x1b[5;10H\x1b[L', vt)
    expect('scrollrect 4..15,9..50 => -1,+0')

    -- DL
    reset(state, nil)
    expect('erase 0..25,0..80')
    cursor(0, 0, state)
    push('A\r\nB\r\nB\r\nC', vt)
    cursor(3, 1, state)
    push('\x1b[2H', vt)
    cursor(1, 0, state)
    push('\x1b[M', vt)
    expect('scrollrect 1..25,0..80 => +1,+0')
    cursor(1, 0, state)
    push('\x1b[3M', vt)
    expect('scrollrect 1..25,0..80 => +3,+0')
    cursor(1, 0, state)

    -- DL with DECSTBM
    push('\x1b[5;15r', vt)
    push('\x1b[5H\x1b[M', vt)
    expect('scrollrect 4..15,0..80 => +1,+0')

    -- DL outside DECSTBM
    push('\x1b[20H\x1b[M', vt)
    -- nothing happens

    -- DL with DECSTBM+DECSLRM
    push('\x1b[?69h', vt)
    push('\x1b[10;50s', vt)
    push('\x1b[5;10H\x1b[M', vt)
    expect('scrollrect 4..15,9..50 => +1,+0')

    -- DECIC
    reset(state, nil)
    expect('erase 0..25,0..80')
    push("\x1b[20G\x1b[5'}", vt)
    expect('scrollrect 0..25,19..80 => +0,-5')

    -- DECIC with DECSTBM+DECSLRM
    push('\x1b[?69h', vt)
    push('\x1b[4;20r\x1b[20;60s', vt)
    push("\x1b[4;20H\x1b[3'}", vt)
    expect('scrollrect 3..20,19..60 => +0,-3')

    -- DECIC outside DECSLRM
    push("\x1b[70G\x1b['}", vt)
    -- nothing happens

    -- DECDC
    reset(state, nil)
    expect('erase 0..25,0..80')
    push("\x1b[20G\x1b[5'~", vt)
    expect('scrollrect 0..25,19..80 => +0,+5')

    -- DECDC with DECSTBM+DECSLRM
    push('\x1b[?69h', vt)
    push('\x1b[4;20r\x1b[20;60s', vt)
    push("\x1b[4;20H\x1b[3'~", vt)
    expect('scrollrect 3..20,19..60 => +0,+3')

    -- DECDC outside DECSLRM
    push("\x1b[70G\x1b['~", vt)
    -- nothing happens

    -- EL 0
    reset(state, nil)
    expect('erase 0..25,0..80')
    cursor(0, 0, state)
    push('ABCDE', vt)
    push('\x1b[3D', vt)
    cursor(0, 2, state)
    push('\x1b[0K', vt)
    expect('erase 0..1,2..80')
    cursor(0, 2, state)

    -- EL 1
    reset(state, nil)
    expect('erase 0..25,0..80')
    cursor(0, 0, state)
    push('ABCDE', vt)
    push('\x1b[3D', vt)
    cursor(0, 2, state)
    push('\x1b[1K', vt)
    expect('erase 0..1,0..3')
    cursor(0, 2, state)

    -- EL 2
    reset(state, nil)
    expect('erase 0..25,0..80')
    cursor(0, 0, state)
    push('ABCDE', vt)
    push('\x1b[3D', vt)
    cursor(0, 2, state)
    push('\x1b[2K', vt)
    expect('erase 0..1,0..80')
    cursor(0, 2, state)

    -- SEL
    reset(state, nil)
    expect('erase 0..25,0..80')
    cursor(0, 0, state)
    push('\x1b[11G', vt)
    cursor(0, 10, state)
    push('\x1b[?0K', vt)
    expect('erase 0..1,10..80 selective')
    cursor(0, 10, state)
    push('\x1b[?1K', vt)
    expect('erase 0..1,0..11 selective')
    cursor(0, 10, state)
    push('\x1b[?2K', vt)
    expect('erase 0..1,0..80 selective')
    cursor(0, 10, state)

    -- ED 0
    reset(state, nil)
    expect('erase 0..25,0..80')
    cursor(0, 0, state)
    push('\x1b[2;2H', vt)
    cursor(1, 1, state)
    push('\x1b[0J', vt)
    expect('erase 1..2,1..80\nerase 2..25,0..80')
    cursor(1, 1, state)

    -- ED 1
    reset(state, nil)
    expect('erase 0..25,0..80')
    cursor(0, 0, state)
    push('\x1b[2;2H', vt)
    cursor(1, 1, state)
    push('\x1b[1J', vt)
    expect('erase 0..1,0..80\nerase 1..2,0..2')
    cursor(1, 1, state)

    -- ED 2
    reset(state, nil)
    expect('erase 0..25,0..80')
    cursor(0, 0, state)
    push('\x1b[2;2H', vt)
    cursor(1, 1, state)
    push('\x1b[2J', vt)
    expect('erase 0..25,0..80')
    cursor(1, 1, state)

    -- ED 3
    push('\x1b[3J', vt)
    expect('sb_clear')

    -- SED
    reset(state, nil)
    expect('erase 0..25,0..80')
    push('\x1b[5;5H', vt)
    cursor(4, 4, state)
    push('\x1b[?0J', vt)
    expect('erase 4..5,4..80 selective\nerase 5..25,0..80 selective')
    cursor(4, 4, state)
    push('\x1b[?1J', vt)
    expect('erase 0..4,0..80 selective\nerase 4..5,0..5 selective')
    cursor(4, 4, state)
    push('\x1b[?2J', vt)
    expect('erase 0..25,0..80 selective')
    cursor(4, 4, state)

    -- DECRQSS on DECSCA
    push('\x1b[2"q', vt)
    push('\x1bP$q"q\x1b\\', vt)
    expect_output('\x1bP1$r2"q\x1b\\')

    state = wantstate(vt, { m = true, e = true, b = true })
    expect('erase 0..25,0..80') -- TODO(dundargoc): strange, this should not be needed according to the original code

    -- ICH move+erase emuation
    reset(state, nil)
    expect('erase 0..25,0..80')
    cursor(0, 0, state)
    push('ACD', vt)
    push('\x1b[2D', vt)
    cursor(0, 1, state)
    push('\x1b[@', vt)
    expect('moverect 0..1,1..79 -> 0..1,2..80\nerase 0..1,1..2')
    cursor(0, 1, state)
    push('B', vt)
    cursor(0, 2, state)
    push('\x1b[3@', vt)
    expect('moverect 0..1,2..77 -> 0..1,5..80\nerase 0..1,2..5')

    -- DCH move+erase emulation
    reset(state, nil)
    expect('erase 0..25,0..80')
    cursor(0, 0, state)
    push('ABBC', vt)
    push('\x1b[3D', vt)
    cursor(0, 1, state)
    push('\x1b[P', vt)
    expect('moverect 0..1,2..80 -> 0..1,1..79\nerase 0..1,79..80')
    cursor(0, 1, state)
    push('\x1b[3P', vt)
    expect('moverect 0..1,4..80 -> 0..1,1..77\nerase 0..1,77..80')
    cursor(0, 1, state)
  end)

  itp('14state_encoding', function()
    local vt = init()
    vterm.vterm_set_utf8(vt, false)
    local state = wantstate(vt, { g = true })

    -- Default
    reset(state, nil)
    push('#', vt)
    expect('putglyph 23 1 0,0')

    -- Designate G0=DEC drawing
    reset(state, nil)
    push('\x1b(0', vt)
    push('a', vt)
    expect('putglyph 2592 1 0,0')

    -- Designate G1 + LS1
    reset(state, nil)
    push('\x1b)0', vt)
    push('a', vt)
    expect('putglyph 61 1 0,0')
    push('\x0e', vt)
    push('a', vt)
    expect('putglyph 2592 1 0,1')
    -- LS0
    push('\x0f', vt)
    push('a', vt)
    expect('putglyph 61 1 0,2')

    -- Designate G2 + LS2
    push('\x1b*0', vt)
    push('a', vt)
    expect('putglyph 61 1 0,3')
    push('\x1bn', vt)
    push('a', vt)
    expect('putglyph 2592 1 0,4')
    push('\x0f', vt)
    push('a', vt)
    expect('putglyph 61 1 0,5')

    -- Designate G3 + LS3
    push('\x1b+0', vt)
    push('a', vt)
    expect('putglyph 61 1 0,6')
    push('\x1bo', vt)
    push('a', vt)
    expect('putglyph 2592 1 0,7')
    push('\x0f', vt)
    push('a', vt)
    expect('putglyph 61 1 0,8')

    -- SS2
    push('a\x8eaa', vt)
    expect('putglyph 61 1 0,9\nputglyph 2592 1 0,10\nputglyph 61 1 0,11')

    -- SS3
    push('a\x8faa', vt)
    expect('putglyph 61 1 0,12\nputglyph 2592 1 0,13\nputglyph 61 1 0,14')

    -- LS1R
    reset(state, nil)
    push('\x1b~', vt)
    push('\xe1', vt)
    expect('putglyph 61 1 0,0')
    push('\x1b)0', vt)
    push('\xe1', vt)
    expect('putglyph 2592 1 0,1')

    -- LS2R
    reset(state, nil)
    push('\x1b}', vt)
    push('\xe1', vt)
    expect('putglyph 61 1 0,0')
    push('\x1b*0', vt)
    push('\xe1', vt)
    expect('putglyph 2592 1 0,1')

    -- LS3R
    reset(state, nil)
    push('\x1b|', vt)
    push('\xe1', vt)
    expect('putglyph 61 1 0,0')
    push('\x1b+0', vt)
    push('\xe1', vt)
    expect('putglyph 2592 1 0,1')

    vterm.vterm_set_utf8(vt, true)

    -- Mixed US-ASCII and UTF-8
    -- U+0108 == c4 88
    reset(state, nil)
    push('\x1b(B', vt)
    push('AB\xc4\x88D', vt)
    expect('putglyph 41 1 0,0\nputglyph 42 1 0,1\nputglyph 108 1 0,2\nputglyph 44 1 0,3')

    -- Split UTF-8 after US-ASCII
    reset(state, nil)
    push('AB\xc4', vt)
    expect('putglyph 41 1 0,0\nputglyph 42 1 0,1')
    push('\x88D', vt)
    expect('putglyph 108 1 0,2\nputglyph 44 1 0,3')
  end)

  itp('15state_mode', function()
    local vt = init()
    local state = wantstate(vt, { g = true, m = true, e = true })

    -- Insert/Replace Mode
    reset(state, nil)
    expect('erase 0..25,0..80')
    cursor(0, 0, state)
    push('AC\x1b[DB', vt)
    expect('putglyph 41 1 0,0\nputglyph 43 1 0,1\nputglyph 42 1 0,1')
    push('\x1b[4h', vt)
    push('\x1b[G', vt)
    push('AC\x1b[DB', vt)
    expect(
      'moverect 0..1,0..79 -> 0..1,1..80\nerase 0..1,0..1\nputglyph 41 1 0,0\nmoverect 0..1,1..79 -> 0..1,2..80\nerase 0..1,1..2\nputglyph 43 1 0,1\nmoverect 0..1,1..79 -> 0..1,2..80\nerase 0..1,1..2\nputglyph 42 1 0,1'
    )

    -- Insert mode only happens once for UTF-8 combining
    push('e', vt)
    expect('moverect 0..1,2..79 -> 0..1,3..80\nerase 0..1,2..3\nputglyph 65 1 0,2')
    push('\xCC\x81', vt)
    expect('putglyph 65,301 1 0,2')

    -- Newline/Linefeed mode
    reset(state, nil)
    expect('erase 0..25,0..80')
    cursor(0, 0, state)
    push('\x1b[5G\n', vt)
    cursor(1, 4, state)
    push('\x1b[20h', vt)
    push('\x1b[5G\n', vt)
    cursor(2, 0, state)

    -- DEC origin mode
    reset(state, nil)
    expect('erase 0..25,0..80')
    cursor(0, 0, state)
    push('\x1b[5;15r', vt)
    push('\x1b[H', vt)
    cursor(0, 0, state)
    push('\x1b[3;3H', vt)
    cursor(2, 2, state)
    push('\x1b[?6h', vt)
    push('\x1b[H', vt)
    cursor(4, 0, state)
    push('\x1b[3;3H', vt)
    cursor(6, 2, state)

    -- DECRQM on DECOM
    push('\x1b[?6h', vt)
    push('\x1b[?6$p', vt)
    expect_output('\x1b[?6;1$y')
    push('\x1b[?6l', vt)
    push('\x1b[?6$p', vt)
    expect_output('\x1b[?6;2$y')

    -- Origin mode with DECSLRM
    push('\x1b[?6h', vt)
    push('\x1b[?69h', vt)
    push('\x1b[20;60s', vt)
    push('\x1b[H', vt)
    cursor(4, 19, state)

    push('\x1b[?69l', vt)

    -- Origin mode bounds cursor to scrolling region
    push('\x1b[H', vt)
    push('\x1b[10A', vt)
    cursor(4, 0, state)
    push('\x1b[20B', vt)
    cursor(14, 0, state)

    -- Origin mode without scroll region
    push('\x1b[?6l', vt)
    push('\x1b[r\x1b[?6h', vt)
    cursor(0, 0, state)
  end)

  itp('16state_resize', function()
    local vt = init()
    local state = wantstate(vt, { g = true })

    -- Placement
    reset(state, nil)
    push('AB\x1b[79GCDE', vt)
    expect(
      'putglyph 41 1 0,0\nputglyph 42 1 0,1\nputglyph 43 1 0,78\nputglyph 44 1 0,79\nputglyph 45 1 1,0'
    )

    -- Resize
    reset(state, nil)
    resize(27, 85, vt)
    push('AB\x1b[79GCDE', vt)
    expect(
      'putglyph 41 1 0,0\nputglyph 42 1 0,1\nputglyph 43 1 0,78\nputglyph 44 1 0,79\nputglyph 45 1 0,80'
    )
    cursor(0, 81, state)

    -- Resize without reset
    resize(28, 90, vt)
    cursor(0, 81, state)
    push('FGHI', vt)
    expect('putglyph 46 1 0,81\nputglyph 47 1 0,82\nputglyph 48 1 0,83\nputglyph 49 1 0,84')
    cursor(0, 85, state)

    -- Resize shrink moves cursor
    resize(25, 80, vt)
    cursor(0, 79, state)

    -- Resize grow doesn't cancel phantom
    reset(state, nil)
    push('\x1b[79GAB', vt)
    expect('putglyph 41 1 0,78\nputglyph 42 1 0,79')
    cursor(0, 79, state)
    resize(30, 100, vt)
    cursor(0, 80, state)
    push('C', vt)
    expect('putglyph 43 1 0,80')
    cursor(0, 81, state)
  end)

  itp('17state_mouse', function()
    local vt = init()
    local state = wantstate(vt, { p = true })

    -- DECRQM on with mouse off
    push('\x1b[?1000$p', vt)
    expect_output('\x1b[?1000;2$y')
    push('\x1b[?1002$p', vt)
    expect_output('\x1b[?1002;2$y')
    push('\x1b[?1003$p', vt)
    expect_output('\x1b[?1003;2$y')

    -- Mouse in simple button report mode
    reset(state, nil)
    expect('settermprop 1 true\nsettermprop 2 true\nsettermprop 7 1')
    push('\x1b[?1000h', vt)
    expect('settermprop 8 1')

    -- Press 1
    mousemove(0, 0, vt)
    mousebtn('d', 1, vt)
    expect_output('\x1b[M\x20\x21\x21')

    -- Release 1
    mousebtn('u', 1, vt)
    expect_output('\x1b[M\x23\x21\x21')

    -- Ctrl-Press 1
    mousebtn('d', 1, vt, { C = true })
    expect_output('\x1b[M\x30\x21\x21')
    mousebtn('u', 1, vt, { C = true })
    expect_output('\x1b[M\x33\x21\x21')

    -- Button 2
    mousebtn('d', 2, vt)
    expect_output('\x1b[M\x21\x21\x21')
    mousebtn('u', 2, vt)
    expect_output('\x1b[M\x23\x21\x21')

    -- Position
    mousemove(10, 20, vt)
    mousebtn('d', 1, vt)
    expect_output('\x1b[M\x20\x35\x2b')

    mousebtn('u', 1, vt)
    expect_output('\x1b[M\x23\x35\x2b')
    mousemove(10, 21, vt)
    -- no output

    -- Wheel events
    mousebtn('d', 4, vt)
    expect_output('\x1b[M\x60\x36\x2b')
    mousebtn('d', 4, vt)
    expect_output('\x1b[M\x60\x36\x2b')
    mousebtn('d', 5, vt)
    expect_output('\x1b[M\x61\x36\x2b')
    mousebtn('d', 6, vt)
    expect_output('\x1b[M\x62\x36\x2b')
    mousebtn('d', 7, vt)
    expect_output('\x1b[M\x63\x36\x2b')

    -- DECRQM on mouse button mode
    push('\x1b[?1000$p', vt)
    expect_output('\x1b[?1000;1$y')
    push('\x1b[?1002$p', vt)
    expect_output('\x1b[?1002;2$y')
    push('\x1b[?1003$p', vt)
    expect_output('\x1b[?1003;2$y')

    -- Drag events
    reset(state, nil)
    expect('settermprop 1 true\nsettermprop 2 true\nsettermprop 7 1')
    push('\x1b[?1002h', vt)
    expect('settermprop 8 2')

    mousemove(5, 5, vt)
    mousebtn('d', 1, vt)
    expect_output('\x1b[M\x20\x26\x26')
    mousemove(5, 6, vt)
    expect_output('\x1b[M\x40\x27\x26')
    mousemove(6, 6, vt)
    expect_output('\x1b[M\x40\x27\x27')
    mousemove(6, 6, vt)
    -- no output
    mousebtn('u', 1, vt)
    expect_output('\x1b[M\x23\x27\x27')
    mousemove(6, 7, vt)
    -- no output

    -- DECRQM on mouse drag mode
    push('\x1b[?1000$p', vt)
    expect_output('\x1b[?1000;2$y')
    push('\x1b[?1002$p', vt)
    expect_output('\x1b[?1002;1$y')
    push('\x1b[?1003$p', vt)
    expect_output('\x1b[?1003;2$y')

    -- Non-drag motion events
    push('\x1b[?1003h', vt)
    expect('settermprop 8 3')

    mousemove(6, 8, vt)
    expect_output('\x1b[M\x43\x29\x27')

    -- DECRQM on mouse motion mode
    push('\x1b[?1000$p', vt)
    expect_output('\x1b[?1000;2$y')
    push('\x1b[?1002$p', vt)
    expect_output('\x1b[?1002;2$y')
    push('\x1b[?1003$p', vt)
    expect_output('\x1b[?1003;1$y')

    -- Bounds checking
    mousemove(300, 300, vt)
    expect_output('\x1b[M\x43\xff\xff')
    mousebtn('d', 1, vt)
    expect_output('\x1b[M\x20\xff\xff')
    mousebtn('u', 1, vt)
    expect_output('\x1b[M\x23\xff\xff')

    -- DECRQM on standard encoding mode
    push('\x1b[?1005$p', vt)
    expect_output('\x1b[?1005;2$y')
    push('\x1b[?1006$p', vt)
    expect_output('\x1b[?1006;2$y')
    push('\x1b[?1015$p', vt)
    expect_output('\x1b[?1015;2$y')

    -- UTF-8 extended encoding mode
    -- 300 + 32 + 1 = 333 = U+014d = \xc5\x8d
    push('\x1b[?1005h', vt)
    mousebtn('d', 1, vt)
    expect_output('\x1b[M\x20\xc5\x8d\xc5\x8d')
    mousebtn('u', 1, vt)
    expect_output('\x1b[M\x23\xc5\x8d\xc5\x8d')

    -- DECRQM on UTF-8 extended encoding mode
    push('\x1b[?1005$p', vt)
    expect_output('\x1b[?1005;1$y')
    push('\x1b[?1006$p', vt)
    expect_output('\x1b[?1006;2$y')
    push('\x1b[?1015$p', vt)
    expect_output('\x1b[?1015;2$y')

    -- SGR extended encoding mode
    push('\x1b[?1006h', vt)
    mousebtn('d', 1, vt)
    expect_output('\x1b[<0;301;301M')
    mousebtn('u', 1, vt)
    expect_output('\x1b[<0;301;301m')

    -- Button 8 on SGR extended encoding mode
    mousebtn('d', 8, vt)
    expect_output('\x1b[<128;301;301M')
    mousebtn('u', 8, vt)
    expect_output('\x1b[<128;301;301m')

    -- Button 9 on SGR extended encoding mode
    mousebtn('d', 9, vt)
    expect_output('\x1b[<129;301;301M')
    mousebtn('u', 9, vt)
    expect_output('\x1b[<129;301;301m')

    -- DECRQM on SGR extended encoding mode
    push('\x1b[?1005$p', vt)
    expect_output('\x1b[?1005;2$y')
    push('\x1b[?1006$p', vt)
    expect_output('\x1b[?1006;1$y')
    push('\x1b[?1015$p', vt)
    expect_output('\x1b[?1015;2$y')

    -- rxvt extended encoding mode
    push('\x1b[?1015h', vt)
    mousebtn('d', 1, vt)
    expect_output('\x1b[0;301;301M')
    mousebtn('u', 1, vt)
    expect_output('\x1b[3;301;301M')

    -- Button 8 on rxvt extended encoding mode
    mousebtn('d', 8, vt)
    expect_output('\x1b[128;301;301M')
    mousebtn('u', 8, vt)
    expect_output('\x1b[3;301;301M')

    -- Button 9 on rxvt extended encoding mode
    mousebtn('d', 9, vt)
    expect_output('\x1b[129;301;301M')
    mousebtn('u', 9, vt)
    expect_output('\x1b[3;301;301M')

    -- DECRQM on rxvt extended encoding mode
    push('\x1b[?1005$p', vt)
    expect_output('\x1b[?1005;2$y')
    push('\x1b[?1006$p', vt)
    expect_output('\x1b[?1006;2$y')
    push('\x1b[?1015$p', vt)
    expect_output('\x1b[?1015;1$y')

    -- Mouse disabled reports nothing
    reset(state, nil)
    expect('settermprop 1 true\nsettermprop 2 true\nsettermprop 7 1')
    mousemove(0, 0, vt)
    mousebtn('d', 1, vt)
    mousebtn('u', 1, vt)

    -- DECSM can set multiple modes at once
    push('\x1b[?1002;1006h', vt)
    expect('settermprop 8 2')
    mousebtn('d', 1, vt)
    expect_output('\x1b[<0;1;1M')
  end)

  itp('18state_termprops', function()
    local vt = init()
    local state = wantstate(vt, { p = true })

    reset(state, nil)
    expect('settermprop 1 true\nsettermprop 2 true\nsettermprop 7 1')

    -- Cursor visibility
    push('\x1b[?25h', vt)
    expect('settermprop 1 true')
    push('\x1b[?25$p', vt)
    expect_output('\x1b[?25;1$y')
    push('\x1b[?25l', vt)
    expect('settermprop 1 false')
    push('\x1b[?25$p', vt)
    expect_output('\x1b[?25;2$y')

    -- Cursor blink
    push('\x1b[?12h', vt)
    expect('settermprop 2 true')
    push('\x1b[?12$p', vt)
    expect_output('\x1b[?12;1$y')
    push('\x1b[?12l', vt)
    expect('settermprop 2 false')
    push('\x1b[?12$p', vt)
    expect_output('\x1b[?12;2$y')

    -- Cursor shape
    push('\x1b[3 q', vt)
    expect('settermprop 2 true\nsettermprop 7 2')

    -- Title
    push('\x1b]2;Here is my title\a', vt)
    expect('settermprop 4 ["Here is my title"]')

    -- Title split write
    push('\x1b]2;Here is', vt)
    expect('settermprop 4 ["Here is"')
    push(' another title\a', vt)
    expect('settermprop 4 " another title"]')
  end)

  itp('20state_wrapping', function()
    local vt = init()
    local state = wantstate(vt, { g = true, m = true })

    -- 79th Column
    push('\x1b[75G', vt)
    push(string.rep('A', 5), vt)
    expect(
      'putglyph 41 1 0,74\nputglyph 41 1 0,75\nputglyph 41 1 0,76\nputglyph 41 1 0,77\nputglyph 41 1 0,78'
    )
    cursor(0, 79, state)

    -- 80th Column Phantom
    push('A', vt)
    expect('putglyph 41 1 0,79')
    cursor(0, 79, state)

    -- Line Wraparound
    push('B', vt)
    expect('putglyph 42 1 1,0')
    cursor(1, 1, state)

    -- Line Wraparound during combined write
    push('\x1b[78G', vt)
    push('BBBCC', vt)
    expect(
      'putglyph 42 1 1,77\nputglyph 42 1 1,78\nputglyph 42 1 1,79\nputglyph 43 1 2,0\nputglyph 43 1 2,1'
    )
    cursor(2, 2, state)

    -- DEC Auto Wrap Mode
    reset(state, nil)
    push('\x1b[?7l', vt)
    push('\x1b[75G', vt)
    push(string.rep('D', 6), vt)
    expect(
      'putglyph 44 1 0,74\nputglyph 44 1 0,75\nputglyph 44 1 0,76\nputglyph 44 1 0,77\nputglyph 44 1 0,78\nputglyph 44 1 0,79'
    )
    cursor(0, 79, state)
    push('D', vt)
    expect('putglyph 44 1 0,79')
    cursor(0, 79, state)
    push('\x1b[?7h', vt)

    -- 80th column causes linefeed on wraparound
    push('\x1b[25;78HABC', vt)
    expect('putglyph 41 1 24,77\nputglyph 42 1 24,78\nputglyph 43 1 24,79')
    cursor(24, 79, state)
    push('D', vt)
    expect('moverect 1..25,0..80 -> 0..24,0..80\nputglyph 44 1 24,0')

    -- 80th column phantom linefeed phantom cancelled by explicit cursor move
    push('\x1b[25;78HABC', vt)
    expect('putglyph 41 1 24,77\nputglyph 42 1 24,78\nputglyph 43 1 24,79')
    cursor(24, 79, state)
    push('\x1b[25;1HD', vt)
    expect('putglyph 44 1 24,0')
  end)

  itp('21state_tabstops', function()
    local vt = init()
    local state = wantstate(vt, { g = true })

    -- Initial
    reset(state, nil)
    push('\tX', vt)
    expect('putglyph 58 1 0,8')
    push('\tX', vt)
    expect('putglyph 58 1 0,16')
    cursor(0, 17, state)

    -- HTS
    push('\x1b[5G\x1bH', vt)
    push('\x1b[G\tX', vt)
    expect('putglyph 58 1 0,4')
    cursor(0, 5, state)

    -- TBC 0
    push('\x1b[9G\x1b[g', vt)
    push('\x1b[G\tX\tX', vt)
    expect('putglyph 58 1 0,4\nputglyph 58 1 0,16')
    cursor(0, 17, state)

    -- TBC 3
    push('\x1b[3g\x1b[50G\x1bH\x1b[G', vt)
    cursor(0, 0, state)
    push('\tX', vt)
    expect('putglyph 58 1 0,49')
    cursor(0, 50, state)

    -- Tabstops after resize
    reset(state, nil)
    resize(30, 100, vt)
    -- Should be 100/8 = 12 tabstops
    push('\tX', vt)
    expect('putglyph 58 1 0,8')
    push('\tX', vt)
    expect('putglyph 58 1 0,16')
    push('\tX', vt)
    expect('putglyph 58 1 0,24')
    push('\tX', vt)
    expect('putglyph 58 1 0,32')
    push('\tX', vt)
    expect('putglyph 58 1 0,40')
    push('\tX', vt)
    expect('putglyph 58 1 0,48')
    push('\tX', vt)
    expect('putglyph 58 1 0,56')
    push('\tX', vt)
    expect('putglyph 58 1 0,64')
    push('\tX', vt)
    expect('putglyph 58 1 0,72')
    push('\tX', vt)
    expect('putglyph 58 1 0,80')
    push('\tX', vt)
    expect('putglyph 58 1 0,88')
    push('\tX', vt)
    expect('putglyph 58 1 0,96')
    cursor(0, 97, state)
  end)

  itp('22state_save', function()
    local vt = init()
    local state = wantstate(vt, { p = true })

    reset(state, nil)
    expect('settermprop 1 true\nsettermprop 2 true\nsettermprop 7 1')

    -- Set up state
    push('\x1b[2;2H', vt)
    cursor(1, 1, state)
    push('\x1b[1m', vt)
    pen('bold', true, state)

    -- Save
    push('\x1b[?1048h', vt)

    -- Change state
    push('\x1b[5;5H', vt)
    cursor(4, 4, state)
    push('\x1b[4 q', vt)
    expect('settermprop 2 false\nsettermprop 7 2')
    push('\x1b[22;4m', vt)
    pen('bold', false, state)
    pen('underline', 1, state)

    -- Restore
    push('\x1b[?1048l', vt)
    expect('settermprop 1 true\nsettermprop 2 true\nsettermprop 7 1')
    cursor(1, 1, state)
    pen('bold', true, state)
    pen('underline', 0, state)

    -- Save/restore using DECSC/DECRC
    push('\x1b[2;2H\x1b7', vt)
    cursor(1, 1, state)

    push('\x1b[5;5H', vt)
    cursor(4, 4, state)
    push('\x1b8', vt)
    expect('settermprop 1 true\nsettermprop 2 true\nsettermprop 7 1')
    cursor(1, 1, state)

    -- Save twice, restore twice happens on both edge transitions
    push('\x1b[2;10H\x1b[?1048h\x1b[6;10H\x1b[?1048h', vt)
    push('\x1b[H', vt)
    cursor(0, 0, state)
    push('\x1b[?1048l', vt)
    expect('settermprop 1 true\nsettermprop 2 true\nsettermprop 7 1')
    cursor(5, 9, state)
    push('\x1b[H', vt)
    cursor(0, 0, state)
    push('\x1b[?1048l', vt)
    expect('settermprop 1 true\nsettermprop 2 true\nsettermprop 7 1')
    cursor(5, 9, state)
  end)

  itp('25state_input', function()
    local vt = init()
    local state = wantstate(vt)

    -- Disambiguate escape codes enabled
    push('\x1b[>1u', vt)

    -- Unmodified ASCII
    inchar(0x41, vt)
    expect_output('A')
    inchar(0x61, vt)
    expect_output('a')

    -- Ctrl modifier on ASCII letters
    inchar(0x41, vt, { C = true })
    expect_output('\x1b[97;6u')
    inchar(0x61, vt, { C = true })
    expect_output('\x1b[97;5u')

    -- Alt modifier on ASCII letters
    inchar(0x41, vt, { A = true })
    expect_output('\x1b[97;4u')
    inchar(0x61, vt, { A = true })
    expect_output('\x1b[97;3u')

    -- Ctrl-Alt modifier on ASCII letters
    inchar(0x41, vt, { C = true, A = true })
    expect_output('\x1b[97;8u')
    inchar(0x61, vt, { C = true, A = true })
    expect_output('\x1b[97;7u')

    -- Ctrl-I is disambiguated
    inchar(0x49, vt)
    expect_output('I')
    inchar(0x69, vt)
    expect_output('i')
    inchar(0x49, vt, { C = true })
    expect_output('\x1b[105;6u')
    inchar(0x69, vt, { C = true })
    expect_output('\x1b[105;5u')
    inchar(0x49, vt, { A = true })
    expect_output('\x1b[105;4u')
    inchar(0x69, vt, { A = true })
    expect_output('\x1b[105;3u')
    inchar(0x49, vt, { A = true, C = true })
    expect_output('\x1b[105;8u')
    inchar(0x69, vt, { A = true, C = true })
    expect_output('\x1b[105;7u')

    -- Ctrl+Digits
    for i = 0, 9 do
      local c = 0x30 + i
      inchar(c, vt)
      expect_output(tostring(i))
      inchar(c, vt, { C = true })
      expect_output(string.format('\x1b[%d;5u', c))
      inchar(c, vt, { C = true, S = true })
      expect_output(string.format('\x1b[%d;6u', c))
      inchar(c, vt, { C = true, A = true })
      expect_output(string.format('\x1b[%d;7u', c))
      inchar(c, vt, { C = true, A = true, S = true })
      expect_output(string.format('\x1b[%d;8u', c))
    end

    -- Special handling of Space
    inchar(0x20, vt)
    expect_output(' ')
    inchar(0x20, vt, { S = true })
    expect_output('\x1b[32;2u')
    inchar(0x20, vt, { C = true })
    expect_output('\x1b[32;5u')
    inchar(0x20, vt, { C = true, S = true })
    expect_output('\x1b[32;6u')
    inchar(0x20, vt, { A = true })
    expect_output('\x1b[32;3u')
    inchar(0x20, vt, { S = true, A = true })
    expect_output('\x1b[32;4u')
    inchar(0x20, vt, { C = true, A = true })
    expect_output('\x1b[32;7u')
    inchar(0x20, vt, { S = true, C = true, A = true })
    expect_output('\x1b[32;8u')

    -- Cursor keys in reset (cursor) mode
    inkey('up', vt)
    expect_output('\x1b[A')
    inkey('up', vt, { S = true })
    expect_output('\x1b[1;2A')
    inkey('up', vt, { C = true })
    expect_output('\x1b[1;5A')
    inkey('up', vt, { S = true, C = true })
    expect_output('\x1b[1;6A')
    inkey('up', vt, { A = true })
    expect_output('\x1b[1;3A')
    inkey('up', vt, { S = true, A = true })
    expect_output('\x1b[1;4A')
    inkey('up', vt, { C = true, A = true })
    expect_output('\x1b[1;7A')
    inkey('up', vt, { S = true, C = true, A = true })
    expect_output('\x1b[1;8A')

    -- Cursor keys in application mode
    push('\x1b[?1h', vt)
    -- Plain "Up" should be SS3 A now
    inkey('up', vt)
    expect_output('\x1bOA')
    -- Modified keys should still use CSI
    inkey('up', vt, { S = true })
    expect_output('\x1b[1;2A')
    inkey('up', vt, { C = true })
    expect_output('\x1b[1;5A')

    -- Tab
    inkey('tab', vt)
    expect_output('\x09')
    inkey('tab', vt, { S = true })
    expect_output('\x1b[9;2u')
    inkey('tab', vt, { C = true })
    expect_output('\x1b[9;5u')
    inkey('tab', vt, { A = true })
    expect_output('\x1b[9;3u')
    inkey('tab', vt, { C = true, A = true })
    expect_output('\x1b[9;7u')

    -- Backspace
    inkey('bs', vt)
    expect_output('\x7f')
    inkey('bs', vt, { S = true })
    expect_output('\x1b[127;2u')
    inkey('bs', vt, { C = true })
    expect_output('\x1b[127;5u')
    inkey('bs', vt, { A = true })
    expect_output('\x1b[127;3u')
    inkey('bs', vt, { C = true, A = true })
    expect_output('\x1b[127;7u')

    -- DEL
    inkey('del', vt)
    expect_output('\x1b[3~')
    inkey('del', vt, { S = true })
    expect_output('\x1b[3;2~')
    inkey('del', vt, { C = true })
    expect_output('\x1b[3;5~')
    inkey('del', vt, { A = true })
    expect_output('\x1b[3;3~')
    inkey('del', vt, { C = true, A = true })
    expect_output('\x1b[3;7~')

    -- ESC
    inkey('esc', vt)
    expect_output('\x1b[27;1u')
    inkey('esc', vt, { S = true })
    expect_output('\x1b[27;2u')
    inkey('esc', vt, { C = true })
    expect_output('\x1b[27;5u')
    inkey('esc', vt, { A = true })
    expect_output('\x1b[27;3u')
    inkey('esc', vt, { C = true, A = true })
    expect_output('\x1b[27;7u')

    -- Enter in linefeed mode
    inkey('enter', vt)
    expect_output('\x0d')
    inkey('enter', vt, { S = true })
    expect_output('\x1b[13;2u')
    inkey('enter', vt, { C = true })
    expect_output('\x1b[13;5u')
    inkey('enter', vt, { A = true })
    expect_output('\x1b[13;3u')
    inkey('enter', vt, { C = true, A = true })
    expect_output('\x1b[13;7u')

    -- Enter in newline mode
    push('\x1b[20h', vt)
    inkey('enter', vt)
    expect_output('\x0d\x0a')

    -- Unmodified F1 is SS3 P
    inkey('f1', vt)
    expect_output('\x1bOP')

    -- Modified F1 is CSI P
    inkey('f1', vt, { S = true })
    expect_output('\x1b[1;2P')
    inkey('f1', vt, { A = true })
    expect_output('\x1b[1;3P')
    inkey('f1', vt, { C = true })
    expect_output('\x1b[1;5P')

    -- Keypad in DECKPNM
    inkey('kp0', vt)
    expect_output('\x1b[57399;1u')

    -- Keypad in DECKPAM
    push('\x1b=', vt)
    inkey('kp0', vt)
    expect_output('\x1bOp')

    -- Bracketed paste mode off
    vterm.vterm_keyboard_start_paste(vt)
    vterm.vterm_keyboard_end_paste(vt)

    -- Bracketed paste mode on
    push('\x1b[?2004h', vt)
    vterm.vterm_keyboard_start_paste(vt)
    expect_output('\x1b[200~')
    vterm.vterm_keyboard_end_paste(vt)
    expect_output('\x1b[201~')

    -- Focus reporting disabled
    vterm.vterm_state_focus_in(state)
    vterm.vterm_state_focus_out(state)

    -- Focus reporting enabled
    state = wantstate(vt, { p = true })
    push('\x1b[?1004h', vt)
    expect('settermprop 9 true')
    vterm.vterm_state_focus_in(state)
    expect_output('\x1b[I')
    vterm.vterm_state_focus_out(state)
    expect_output('\x1b[O')

    -- Synchronized output mode 2026
    push('\x1b[?2026h', vt)
    expect('settermprop 11 true')
    push('\x1b[?2026l', vt)
    expect('settermprop 11 false')

    -- DECRQM on synchronized output
    push('\x1b[?2026h', vt)
    expect('settermprop 11 true')
    push('\x1b[?2026$p', vt)
    expect_output('\x1b[?2026;1$y')
    push('\x1b[?2026l', vt)
    expect('settermprop 11 false')
    push('\x1b[?2026$p', vt)
    expect_output('\x1b[?2026;2$y')

    -- Disambiguate escape codes disabled
    push('\x1b[<u', vt)

    -- Unmodified ASCII
    inchar(0x41, vt)
    expect_output('A')
    inchar(0x61, vt)
    expect_output('a')

    -- Ctrl modifier on ASCII letters
    inchar(0x41, vt, { C = true })
    expect_output('\x01')
    inchar(0x61, vt, { C = true })
    expect_output('\x01')

    -- Alt modifier on ASCII letters
    inchar(0x41, vt, { A = true })
    expect_output('\x1bA')
    inchar(0x61, vt, { A = true })
    expect_output('\x1ba')

    -- Ctrl-Alt modifier on ASCII letters
    inchar(0x41, vt, { C = true, A = true })
    expect_output('\x1b\x01')
    inchar(0x61, vt, { C = true, A = true })
    expect_output('\x1b\x01')

    -- Ctrl-I is ambiguous
    inchar(0x49, vt)
    expect_output('I')
    inchar(0x69, vt)
    expect_output('i')
    inchar(0x49, vt, { C = true })
    expect_output('\x09')
    inchar(0x69, vt, { C = true })
    expect_output('\x09')
    inchar(0x49, vt, { A = true })
    expect_output('\x1bI')
    inchar(0x69, vt, { A = true })
    expect_output('\x1bi')
    inchar(0x49, vt, { A = true, C = true })
    expect_output('\x1b\x09')
    inchar(0x69, vt, { A = true, C = true })
    expect_output('\x1b\x09')

    -- Ctrl+Digits
    inchar(0x30, vt, { C = true })
    expect_output('0')
    inchar(0x31, vt, { C = true })
    expect_output('1')
    inchar(0x32, vt, { C = true })
    expect_output('\x00')
    inchar(0x33, vt, { C = true })
    expect_output('\x1b')
    inchar(0x34, vt, { C = true })
    expect_output('\x1c')
    inchar(0x35, vt, { C = true })
    expect_output('\x1d')
    inchar(0x36, vt, { C = true })
    expect_output('\x1e')
    inchar(0x37, vt, { C = true })
    expect_output('\x1f')
    inchar(0x38, vt, { C = true })
    expect_output('\x7f')
    inchar(0x39, vt, { C = true })
    expect_output('9')

    -- Ctrl+/
    inchar(0x2F, vt, { C = true })
    expect_output('\x1f')
  end)

  itp('26state_query', function()
    local vt = init()
    local state = wantstate(vt)

    -- DA
    reset(state, nil)
    push('\x1b[c', vt)
    expect_output('\x1b[?61;22;52c')

    -- XTVERSION
    reset(state, nil)
    push('\x1b[>q', vt)
    expect_output('\x1bP>|libvterm(0.3)\x1b\\')

    -- DSR
    reset(state, nil)
    push('\x1b[5n', vt)
    expect_output('\x1b[0n')

    -- CPR
    push('\x1b[6n', vt)
    expect_output('\x1b[1;1R')
    push('\x1b[10;10H\x1b[6n', vt)
    expect_output('\x1b[10;10R')

    -- DECCPR
    push('\x1b[?6n', vt)
    expect_output('\x1b[?10;10R')

    -- DECRQSS on DECSCUSR
    push('\x1b[3 q', vt)
    push('\x1bP$q q\x1b\\', vt)
    expect_output('\x1bP1$r3 q\x1b\\')

    -- DECRQSS on SGR
    push('\x1b[1;5;7m', vt)
    push('\x1bP$qm\x1b\\', vt)
    expect_output('\x1bP1$r1;5;7m\x1b\\')

    -- DECRQSS on SGR ANSI colours
    push('\x1b[0;31;42m', vt)
    push('\x1bP$qm\x1b\\', vt)
    expect_output('\x1bP1$r31;42m\x1b\\')

    -- DECRQSS on SGR ANSI hi-bright colours
    push('\x1b[0;93;104m', vt)
    push('\x1bP$qm\x1b\\', vt)
    expect_output('\x1bP1$r93;104m\x1b\\')

    -- DECRQSS on SGR 256-palette colours
    push('\x1b[0;38:5:56;48:5:78m', vt)
    push('\x1bP$qm\x1b\\', vt)
    expect_output('\x1bP1$r38:5:56;48:5:78m\x1b\\')

    -- DECRQSS on SGR RGB8 colours
    push('\x1b[0;38:2:24:68:112;48:2:13:57:101m', vt)
    push('\x1bP$qm\x1b\\', vt)
    expect_output('\x1bP1$r38:2:24:68:112;48:2:13:57:101m\x1b\\')

    -- S8C1T on DSR
    push('\x1b G', vt)
    push('\x1b[5n', vt)
    expect_output('\x9b0n')
    push('\x1b F', vt)
  end)

  itp('27state_reset', function()
    local vt = init()
    local state = wantstate(vt)

    reset(state, nil)

    -- RIS homes cursor
    push('\x1b[5;5H', vt)
    cursor(4, 4, state)
    state = wantstate(vt, { m = true })
    push('\x1bc', vt)
    cursor(0, 0, state)
    wantstate(vt)

    -- RIS cancels scrolling region
    push('\x1b[5;10r', vt)
    wantstate(vt, { s = true })
    push('\x1bc\x1b[25H\n', vt)
    expect('scrollrect 0..25,0..80 => +1,+0')
    wantstate(vt)

    -- RIS erases screen
    push('ABCDE', vt)
    state = wantstate(vt, { e = true })
    push('\x1bc', vt)
    expect('erase 0..25,0..80')
    wantstate(vt)

    -- RIS clears tabstops
    push('\x1b[5G\x1bH\x1b[G\t', vt)
    cursor(0, 4, state)
    push('\x1bc\t', vt)
    cursor(0, 8, state)
  end)

  itp('28state_dbl_wh', function()
    local vt = init()
    local state = wantstate(vt, { g = true })

    -- Single Width, Single Height
    reset(state, nil)
    push('\x1b#5', vt)
    push('Hello', vt)
    expect(
      'putglyph 48 1 0,0\nputglyph 65 1 0,1\nputglyph 6c 1 0,2\nputglyph 6c 1 0,3\nputglyph 6f 1 0,4'
    )

    -- Double Width, Single Height
    reset(state, nil)
    push('\x1b#6', vt)
    push('Hello', vt)
    expect(
      'putglyph 48 1 0,0 dwl\nputglyph 65 1 0,1 dwl\nputglyph 6c 1 0,2 dwl\nputglyph 6c 1 0,3 dwl\nputglyph 6f 1 0,4 dwl'
    )
    cursor(0, 5, state)
    push('\x1b[40GAB', vt)
    expect('putglyph 41 1 0,39 dwl\nputglyph 42 1 1,0')
    cursor(1, 1, state)

    -- Double Height
    reset(state, nil)
    push('\x1b#3', vt)
    push('Hello', vt)
    expect(
      'putglyph 48 1 0,0 dwl dhl-top\nputglyph 65 1 0,1 dwl dhl-top\nputglyph 6c 1 0,2 dwl dhl-top\nputglyph 6c 1 0,3 dwl dhl-top\nputglyph 6f 1 0,4 dwl dhl-top'
    )
    cursor(0, 5, state)
    push('\r\n\x1b#4', vt)
    push('Hello', vt)
    expect(
      'putglyph 48 1 1,0 dwl dhl-bottom\nputglyph 65 1 1,1 dwl dhl-bottom\nputglyph 6c 1 1,2 dwl dhl-bottom\nputglyph 6c 1 1,3 dwl dhl-bottom\nputglyph 6f 1 1,4 dwl dhl-bottom'
    )
    cursor(1, 5, state)

    -- Double Width scrolling
    reset(state, nil)
    push('\x1b[20H\x1b#6ABC', vt)
    expect('putglyph 41 1 19,0 dwl\nputglyph 42 1 19,1 dwl\nputglyph 43 1 19,2 dwl')
    push('\x1b[25H\n', vt)
    push('\x1b[19;4HDE', vt)
    expect('putglyph 44 1 18,3 dwl\nputglyph 45 1 18,4 dwl')
    push('\x1b[H\x1bM', vt)
    push('\x1b[20;6HFG', vt)
    expect('putglyph 46 1 19,5 dwl\nputglyph 47 1 19,6 dwl')
  end)

  itp('29state_fallback', function()
    local vt = init()
    local state = wantstate(vt, { f = true })
    reset(state, nil)

    -- Unrecognised control
    push('\x03', vt)
    expect('control 03')

    -- Unrecognised CSI
    push('\x1b[?15;2z', vt)
    expect('csi 7a L=3f 15,2')

    -- Unrecognised OSC
    push('\x1b]27;Something\x1b\\', vt)
    expect('osc [27;Something]')

    -- Unrecognised DCS
    push('\x1bPz123\x1b\\', vt)
    expect('dcs [z123]')

    -- Unrecognised APC
    push('\x1b_z123\x1b\\', vt)
    expect('apc [z123]')

    -- Unrecognised PM
    push('\x1b^z123\x1b\\', vt)
    expect('pm [z123]')

    -- Unrecognised SOS
    push('\x1bXz123\x1b\\', vt)
    expect('sos [z123]')
  end)

  itp('30state_pen', function()
    local vt = init()
    local state = wantstate(vt)

    -- Reset
    push('\x1b[m', vt)
    pen('bold', false, state)
    pen('underline', 0, state)
    pen('italic', false, state)
    pen('blink', false, state)
    pen('reverse', false, state)
    pen('font', 0, state)
    -- TODO(dundargoc): fix
    -- ?pen foreground = rgb(240,240,240,is_default_fg)
    -- ?pen background = rgb(0,0,0,is_default_bg)

    -- Bold
    push('\x1b[1m', vt)
    pen('bold', true, state)
    push('\x1b[22m', vt)
    pen('bold', false, state)
    push('\x1b[1m\x1b[m', vt)
    pen('bold', false, state)

    -- Underline
    push('\x1b[4m', vt)
    pen('underline', 1, state)
    push('\x1b[21m', vt)
    pen('underline', 2, state)
    push('\x1b[24m', vt)
    pen('underline', 0, state)
    push('\x1b[4m\x1b[4:0m', vt)
    pen('underline', 0, state)
    push('\x1b[4:1m', vt)
    pen('underline', 1, state)
    push('\x1b[4:2m', vt)
    pen('underline', 2, state)
    push('\x1b[4:3m', vt)
    pen('underline', 3, state)
    push('\x1b[4m\x1b[m', vt)
    pen('underline', 0, state)

    -- Italic
    push('\x1b[3m', vt)
    pen('italic', true, state)
    push('\x1b[23m', vt)
    pen('italic', false, state)
    push('\x1b[3m\x1b[m', vt)
    pen('italic', false, state)

    -- Blink
    push('\x1b[5m', vt)
    pen('blink', true, state)
    push('\x1b[25m', vt)
    pen('blink', false, state)
    push('\x1b[5m\x1b[m', vt)
    pen('blink', false, state)

    -- Reverse
    push('\x1b[7m', vt)
    pen('reverse', true, state)
    push('\x1b[27m', vt)
    pen('reverse', false, state)
    push('\x1b[7m\x1b[m', vt)
    pen('reverse', false, state)

    -- Font Selection
    push('\x1b[11m', vt)
    pen('font', 1, state)
    push('\x1b[19m', vt)
    pen('font', 9, state)
    push('\x1b[10m', vt)
    pen('font', 0, state)
    push('\x1b[11m\x1b[m', vt)
    pen('font', 0, state)

    -- Dim
    push('\x1b[2m', vt)
    pen('dim', true, state)
    push('\x1b[22m', vt)
    pen('dim', false, state)
    push('\x1b[2m\x1b[m', vt)
    pen('dim', false, state)

    -- Bold and Dim interaction (SGR 22 turns off both)
    push('\x1b[1;2m', vt)
    pen('bold', true, state)
    pen('dim', true, state)
    push('\x1b[22m', vt)
    pen('bold', false, state)
    pen('dim', false, state)

    -- Overline
    push('\x1b[53m', vt)
    pen('overline', true, state)
    push('\x1b[55m', vt)
    pen('overline', false, state)
    push('\x1b[53m\x1b[m', vt)
    pen('overline', false, state)

    -- TODO(dundargoc): fix
    -- Foreground
    -- push "\x1b[31m"
    --   ?pen foreground = idx(1)
    -- push "\x1b[32m"
    --   ?pen foreground = idx(2)
    -- push "\x1b[34m"
    --   ?pen foreground = idx(4)
    -- push "\x1b[91m"
    --   ?pen foreground = idx(9)
    -- push "\x1b[38:2:10:20:30m"
    --   ?pen foreground = rgb(10,20,30)
    -- push "\x1b[38:5:1m"
    --   ?pen foreground = idx(1)
    -- push "\x1b[39m"
    --   ?pen foreground = rgb(240,240,240,is_default_fg)
    --
    -- Background
    -- push "\x1b[41m"
    --   ?pen background = idx(1)
    -- push "\x1b[42m"
    --   ?pen background = idx(2)
    -- push "\x1b[44m"
    --   ?pen background = idx(4)
    -- push "\x1b[101m"
    --   ?pen background = idx(9)
    -- push "\x1b[48:2:10:20:30m"
    --   ?pen background = rgb(10,20,30)
    -- push "\x1b[48:5:1m"
    --   ?pen background = idx(1)
    -- push "\x1b[49m"
    --   ?pen background = rgb(0,0,0,is_default_bg)
    --
    -- Bold+ANSI colour == highbright
    -- push "\x1b[m\x1b[1;37m"
    --   ?pen bold = on
    --   ?pen foreground = idx(15)
    -- push "\x1b[m\x1b[37;1m"
    --   ?pen bold = on
    --   ?pen foreground = idx(15)
    --
    -- Super/Subscript
    -- push "\x1b[73m"
    --   ?pen small = on
    --   ?pen baseline = raise
    -- push "\x1b[74m"
    --   ?pen small = on
    --   ?pen baseline = lower
    -- push "\x1b[75m"
    --   ?pen small = off
    --   ?pen baseline = normal
    --
    -- DECSTR resets pen attributes
    -- push "\x1b[1;4m"
    --   ?pen bold = on
    --   ?pen underline = 1
    -- push "\x1b[!p"
    --   ?pen bold = off
    --   ?pen underline = 0
  end)

  itp('31state_rep', function()
    local vt = init()
    local state = wantstate(vt, { g = true })

    -- REP no argument
    reset(state, nil)
    push('a\x1b[b', vt)
    expect('putglyph 61 1 0,0\nputglyph 61 1 0,1')

    -- REP zero (zero should be interpreted as one)
    reset(state, nil)
    push('a\x1b[0b', vt)
    expect('putglyph 61 1 0,0\nputglyph 61 1 0,1')

    -- REP lowercase a times two
    reset(state, nil)
    push('a\x1b[2b', vt)
    expect('putglyph 61 1 0,0\nputglyph 61 1 0,1\nputglyph 61 1 0,2')

    -- REP with UTF-8 1 char
    -- U+00E9 = C3 A9  name: LATIN SMALL LETTER E WITH ACUTE
    reset(state, nil)
    push('\xC3\xA9\x1b[b', vt)
    expect('putglyph e9 1 0,0\nputglyph e9 1 0,1')

    -- REP with UTF-8 wide char
    -- U+00E9 = C3 A9  name: LATIN SMALL LETTER E WITH ACUTE
    reset(state, nil)
    push('\xEF\xBC\x90\x1b[b', vt)
    expect('putglyph ff10 2 0,0\nputglyph ff10 2 0,2')

    -- REP with UTF-8 combining character
    reset(state, nil)
    push('e\xCC\x81\x1b[b', vt)
    expect('putglyph 65,301 1 0,0\nputglyph 65,301 1 0,1')

    -- REP till end of line
    reset(state, nil)
    push('a\x1b[1000bb', vt)
    expect(
      'putglyph 61 1 0,0\nputglyph 61 1 0,1\nputglyph 61 1 0,2\nputglyph 61 1 0,3\nputglyph 61 1 0,4\nputglyph 61 1 0,5\nputglyph 61 1 0,6\nputglyph 61 1 0,7\nputglyph 61 1 0,8\nputglyph 61 1 0,9\nputglyph 61 1 0,10\nputglyph 61 1 0,11\nputglyph 61 1 0,12\nputglyph 61 1 0,13\nputglyph 61 1 0,14\nputglyph 61 1 0,15\nputglyph 61 1 0,16\nputglyph 61 1 0,17\nputglyph 61 1 0,18\nputglyph 61 1 0,19\nputglyph 61 1 0,20\nputglyph 61 1 0,21\nputglyph 61 1 0,22\nputglyph 61 1 0,23\nputglyph 61 1 0,24\nputglyph 61 1 0,25\nputglyph 61 1 0,26\nputglyph 61 1 0,27\nputglyph 61 1 0,28\nputglyph 61 1 0,29\nputglyph 61 1 0,30\nputglyph 61 1 0,31\nputglyph 61 1 0,32\nputglyph 61 1 0,33\nputglyph 61 1 0,34\nputglyph 61 1 0,35\nputglyph 61 1 0,36\nputglyph 61 1 0,37\nputglyph 61 1 0,38\nputglyph 61 1 0,39\nputglyph 61 1 0,40\nputglyph 61 1 0,41\nputglyph 61 1 0,42\nputglyph 61 1 0,43\nputglyph 61 1 0,44\nputglyph 61 1 0,45\nputglyph 61 1 0,46\nputglyph 61 1 0,47\nputglyph 61 1 0,48\nputglyph 61 1 0,49\nputglyph 61 1 0,50\nputglyph 61 1 0,51\nputglyph 61 1 0,52\nputglyph 61 1 0,53\nputglyph 61 1 0,54\nputglyph 61 1 0,55\nputglyph 61 1 0,56\nputglyph 61 1 0,57\nputglyph 61 1 0,58\nputglyph 61 1 0,59\nputglyph 61 1 0,60\nputglyph 61 1 0,61\nputglyph 61 1 0,62\nputglyph 61 1 0,63\nputglyph 61 1 0,64\nputglyph 61 1 0,65\nputglyph 61 1 0,66\nputglyph 61 1 0,67\nputglyph 61 1 0,68\nputglyph 61 1 0,69\nputglyph 61 1 0,70\nputglyph 61 1 0,71\nputglyph 61 1 0,72\nputglyph 61 1 0,73\nputglyph 61 1 0,74\nputglyph 61 1 0,75\nputglyph 61 1 0,76\nputglyph 61 1 0,77\nputglyph 61 1 0,78\nputglyph 61 1 0,79\nputglyph 62 1 1,0'
    )
  end)

  itp('32state_flow', function()
    local vt = init()
    local state = wantstate(vt)

    -- Many of these test cases inspired by
    -- https://blueprints.launchpad.net/libvterm/+spec/reflow-cases

    -- Spillover text marks continuation on second line
    reset(state, nil)
    push(string.rep('A', 100), vt)
    push('\r\n', vt)
    lineinfo(0, {}, state)
    lineinfo(1, { cont = true }, state)

    -- CRLF in column 80 does not mark continuation
    reset(state, nil)
    push(string.rep('B', 80), vt)
    push('\r\n', vt)
    push(string.rep('B', 20), vt)
    push('\r\n', vt)
    lineinfo(0, {}, state)
    lineinfo(1, {}, state)

    -- EL cancels continuation of following line
    reset(state, nil)
    push(string.rep('D', 100), vt)
    lineinfo(1, { cont = true }, state)
    push('\x1bM\x1b[79G\x1b[K', vt)
    lineinfo(1, {}, state)
  end)

  itp('40state_selection', function()
    local vt = init()
    wantstate(vt)

    -- Set clipboard; final chunk len 4
    push('\x1b]52;c;SGVsbG8s\x1b\\', vt)
    expect('selection-set mask=0001 [Hello,]')

    -- Set clipboard; final chunk len 3
    push('\x1b]52;c;SGVsbG8sIHc=\x1b\\', vt)
    expect('selection-set mask=0001 [Hello, w]')

    -- Set clipboard; final chunk len 2
    push('\x1b]52;c;SGVsbG8sIHdvcmxkCg==\x1b\\', vt)
    expect('selection-set mask=0001 [Hello, world\n]')

    -- Set clipboard; split between chunks
    push('\x1b]52;c;SGVs', vt)
    expect('selection-set mask=0001 [Hel')
    push('bG8s\x1b\\', vt)
    expect('selection-set mask=0001 lo,]')

    -- Set clipboard; split within chunk
    push('\x1b]52;c;SGVsbG', vt)
    expect('selection-set mask=0001 [Hel')
    push('8s\x1b\\', vt)
    expect('selection-set mask=0001 lo,]')

    -- Set clipboard; empty first chunk
    push('\x1b]52;c;', vt)
    push('SGVsbG8s\x1b\\', vt)
    expect('selection-set mask=0001 [Hello,]')

    -- Set clipboard; empty final chunk
    push('\x1b]52;c;SGVsbG8s', vt)
    expect('selection-set mask=0001 [Hello,')
    push('\x1b\\', vt)
    expect('selection-set mask=0001 ]')

    -- Set clipboard; longer than buffer
    push('\x1b]52;c;' .. string.rep('LS0t', 10) .. '\x1b\\', vt)
    expect('selection-set mask=0001 [---------------\nselection-set mask=0001 ---------------]')

    -- Clear clipboard
    push('\x1b]52;c;\x1b\\', vt)
    expect('selection-set mask=0001 []')

    -- Set invalid data clears and ignores
    push('\x1b]52;c;SGVs*SGVsbG8s\x1b\\', vt)
    expect('selection-set mask=0001 []')

    -- Query clipboard
    push('\x1b]52;c;?\x1b\\', vt)
    expect('selection-query mask=0001')

    -- TODO(dundargoc): fix
    -- Send clipboard; final chunk len 4
    -- SELECTION 1 ["Hello,"]
    --   output "\x1b]52;c;"
    --   output "SGVsbG8s"
    --   output "\x1b\\"
    --
    -- Send clipboard; final chunk len 3
    -- SELECTION 1 ["Hello, w"]
    --   output "\x1b]52;c;"
    --   output "SGVsbG8s"
    --   output "IHc=\x1b\\"
    --
    -- Send clipboard; final chunk len 2
    -- SELECTION 1 ["Hello, world\n"]
    --   output "\x1b]52;c;"
    --   output "SGVsbG8sIHdvcmxk"
    --   output "Cg==\x1b\\"
    --
    -- Send clipboard; split between chunks
    -- SELECTION 1 ["Hel"
    --   output "\x1b]52;c;"
    --   output "SGVs"
    -- SELECTION 1  "lo,"]
    --   output "bG8s"
    --   output "\x1b\\"
    --
    -- Send clipboard; split within chunk
    -- SELECTION 1 ["Hello"
    --   output "\x1b]52;c;"
    --   output "SGVs"
    -- SELECTION 1 ","]
    --   output "bG8s"
    --   output "\x1b\\"
  end)

  itp('60screen_ascii', function()
    local vt = init()
    local screen = wantscreen(vt, { a = true, c = true })

    -- Get
    reset(nil, screen)
    push('ABC', vt)
    expect('movecursor 0,3')
    screen_chars(0, 0, 1, 3, 'ABC', screen)
    screen_chars(0, 0, 1, 80, 'ABC', screen)
    screen_text(0, 0, 1, 3, '41,42,43', screen)
    screen_text(0, 0, 1, 80, '41,42,43', screen)
    screen_cell(0, 0, '{41} width=1 attrs={} fg=rgb(240,240,240) bg=rgb(0,0,0)', screen)
    screen_cell(0, 1, '{42} width=1 attrs={} fg=rgb(240,240,240) bg=rgb(0,0,0)', screen)
    screen_cell(0, 2, '{43} width=1 attrs={} fg=rgb(240,240,240) bg=rgb(0,0,0)', screen)
    screen_row(0, 'ABC', screen)
    screen_eol(0, 0, 0, screen)
    screen_eol(0, 2, 0, screen)
    screen_eol(0, 3, 1, screen)
    push('\x1b[H', vt)
    expect('movecursor 0,0')
    screen_row(0, 'ABC', screen)
    screen_text(0, 0, 1, 80, '41,42,43', screen)
    push('E', vt)
    expect('movecursor 0,1')
    screen_row(0, 'EBC', screen)
    screen_text(0, 0, 1, 80, '45,42,43', screen)

    screen = wantscreen(vt, { a = true })

    -- Erase
    reset(nil, screen)
    push('ABCDE\x1b[H\x1b[K', vt)
    -- TODO(dundargoc): fix
    -- screen_row(0, '', screen)
    screen_text(0, 0, 1, 80, '', screen)

    -- Copycell
    reset(nil, screen)
    push('ABC\x1b[H\x1b[@', vt)
    push('1', vt)
    screen_row(0, '1ABC', screen)

    reset(nil, screen)
    push('ABC\x1b[H\x1b[P', vt)
    screen_chars(0, 0, 1, 1, 'B', screen)
    screen_chars(0, 1, 1, 2, 'C', screen)
    screen_chars(0, 0, 1, 80, 'BC', screen)

    -- Space padding
    reset(nil, screen)
    push('Hello\x1b[CWorld', vt)
    screen_row(0, 'Hello World', screen)
    screen_text(0, 0, 1, 80, '48,65,6c,6c,6f,20,57,6f,72,6c,64', screen)

    -- Linefeed padding
    reset(nil, screen)
    push('Hello\r\nWorld', vt)
    screen_chars(0, 0, 2, 80, 'Hello\nWorld', screen)
    screen_text(0, 0, 2, 80, '48,65,6c,6c,6f,0a,57,6f,72,6c,64', screen)

    -- Altscreen
    reset(nil, screen)
    push('P', vt)
    screen_row(0, 'P', screen)
    -- TODO(dundargoc): fix
    -- push('\x1b[?1049h', vt)
    -- screen_row(0, '', screen)
    -- push('\x1b[2K\x1b[HA', vt)
    -- screen_row(0, 'A', screen)
    -- push('\x1b[?1049l', vt)
    -- screen_row(0, 'P', screen)
  end)

  itp('61screen_unicode', function()
    local vt = init()
    local screen = wantscreen(vt)

    -- Single width UTF-8
    -- U+00C1 = C3 81  name: LATIN CAPITAL LETTER A WITH ACUTE
    -- U+00E9 = C3 A9  name: LATIN SMALL LETTER E WITH ACUTE
    reset(nil, screen)
    push('\xC3\x81\xC3\xA9', vt)
    screen_row(0, 'Áé', screen)
    screen_text(0, 0, 1, 80, 'c3,81,c3,a9', screen)
    screen_cell(0, 0, '{c1} width=1 attrs={} fg=rgb(240,240,240) bg=rgb(0,0,0)', screen)

    -- Wide char
    -- U+FF10 = EF BC 90  name: FULLWIDTH DIGIT ZERO
    reset(nil, screen)
    push('0123\x1b[H', vt)
    push('\xEF\xBC\x90', vt)
    screen_row(0, '０23', screen)
    screen_text(0, 0, 1, 80, 'ef,bc,90,32,33', screen)
    screen_cell(0, 0, '{ff10} width=2 attrs={} fg=rgb(240,240,240) bg=rgb(0,0,0)', screen)

    -- Combining char
    -- U+0301 = CC 81  name: COMBINING ACUTE
    reset(nil, screen)
    push('0123\x1b[H', vt)
    push('e\xCC\x81', vt)
    screen_row(0, 'é123', screen)
    screen_text(0, 0, 1, 80, '65,cc,81,31,32,33', screen)
    screen_cell(0, 0, '{65,301} width=1 attrs={} fg=rgb(240,240,240) bg=rgb(0,0,0)', screen)

    -- 10 combining accents should not crash
    reset(nil, screen)
    push('e\xCC\x81\xCC\x82\xCC\x83\xCC\x84\xCC\x85\xCC\x86\xCC\x87\xCC\x88\xCC\x89\xCC\x8A', vt)
    screen_cell(
      0,
      0,
      '{65,301,302,303,304,305,306,307,308,309,30a} width=1 attrs={} fg=rgb(240,240,240) bg=rgb(0,0,0)',
      screen
    )

    -- 40 combining accents in two split writes of 20 should not crash
    reset(nil, screen)
    push(
      'e\xCC\x81\xCC\x81\xCC\x81\xCC\x81\xCC\x81\xCC\x81\xCC\x81\xCC\x81\xCC\x81\xCC\x81\xCC\x81\xCC\x81\xCC\x81\xCC\x81\xCC\x81\xCC\x81\xCC\x81\xCC\x81\xCC\x81\xCC\x81',
      vt
    )
    push(
      '\xCC\x81\xCC\x81\xCC\x81\xCC\x81\xCC\x81\xCC\x81\xCC\x81\xCC\x81\xCC\x81\xCC\x81\xCC\x81\xCC\x81\xCC\x81\xCC\x81\xCC\x81\xCC\x81\xCC\x81\xCC\x81\xCC\x81\xCC\x81',
      vt
    )
    screen_cell(
      0,
      0,
      '{65,301,301,301,301,301,301,301,301,301,301,301,301,301,301} width=1 attrs={} fg=rgb(240,240,240) bg=rgb(0,0,0)',
      screen
    )

    -- Outputting CJK doublewidth in 80th column should wraparound to next line and not crash"
    reset(nil, screen)
    push('\x1b[80G\xEF\xBC\x90', vt)
    screen_cell(0, 79, '{} width=1 attrs={} fg=rgb(240,240,240) bg=rgb(0,0,0)', screen)
    screen_cell(1, 0, '{ff10} width=2 attrs={} fg=rgb(240,240,240) bg=rgb(0,0,0)', screen)

    -- Outputting emoji with ZWJ and variant selectors
    reset(nil, screen)
    push('🏳️‍🌈🏳️‍⚧️🏴‍☠️', vt)

    -- stylua: ignore start
    screen_cell(0, 0, '{1f3f3,fe0f,200d,1f308} width=2 attrs={} fg=rgb(240,240,240) bg=rgb(0,0,0)', screen)
    screen_cell(0, 2, '{1f3f3,fe0f,200d,26a7,fe0f} width=2 attrs={} fg=rgb(240,240,240) bg=rgb(0,0,0)', screen)
    screen_cell(0, 4, '{1f3f4,200d,2620,fe0f} width=2 attrs={} fg=rgb(240,240,240) bg=rgb(0,0,0)', screen)
    -- stylua: ignore end
  end)

  pending('62screen_damage', function() end)

  itp('63screen_resize', function()
    local vt = init()
    local state = wantstate(vt)
    local screen = wantscreen(vt)

    -- Resize wider preserves cells
    reset(state, screen)
    resize(25, 80, vt)
    push('AB\r\nCD', vt)
    screen_chars(0, 0, 1, 80, 'AB', screen)
    screen_chars(1, 0, 2, 80, 'CD', screen)
    resize(25, 100, vt)
    screen_chars(0, 0, 1, 100, 'AB', screen)
    screen_chars(1, 0, 2, 100, 'CD', screen)

    -- Resize wider allows print in new area
    reset(state, screen)
    resize(25, 80, vt)
    push('AB\x1b[79GCD', vt)
    screen_chars(0, 0, 1, 2, 'AB', screen)
    screen_chars(0, 78, 1, 80, 'CD', screen)
    resize(25, 100, vt)
    screen_chars(0, 0, 1, 2, 'AB', screen)
    screen_chars(0, 78, 1, 80, 'CD', screen)
    push('E', vt)
    screen_chars(0, 78, 1, 81, 'CDE', screen)

    -- Resize shorter with blanks just truncates
    reset(state, screen)
    resize(25, 80, vt)
    push('Top\x1b[10HLine 10', vt)
    screen_row(0, 'Top', screen)
    screen_row(9, 'Line 10', screen)
    cursor(9, 7, state)
    resize(20, 80, vt)
    screen_row(0, 'Top', screen)
    screen_row(9, 'Line 10', screen)
    cursor(9, 7, state)

    -- Resize shorter with content must scroll
    reset(state, screen)
    resize(25, 80, vt)
    push('Top\x1b[25HLine 25\x1b[15H', vt)
    screen_row(0, 'Top', screen)
    screen_row(24, 'Line 25', screen)
    cursor(14, 0, state)
    screen = wantscreen(vt, { b = true })
    resize(20, 80, vt)
    expect(
      'sb_pushline 80 = 54 6f 70\nsb_pushline 80 =\nsb_pushline 80 =\nsb_pushline 80 =\nsb_pushline 80 ='
    )
    -- TODO(dundargoc): fix or remove
    -- screen_row( 0  , "",screen)
    screen_row(19, 'Line 25', screen)
    cursor(9, 0, state)

    -- Resize shorter does not lose line with cursor
    -- See also https://github.com/neovim/libvterm/commit/1b745d29d45623aa8d22a7b9288c7b0e331c7088
    reset(state, screen)
    wantscreen(vt)
    resize(25, 80, vt)
    screen = wantscreen(vt, { b = true })
    push('\x1b[24HLine 24\r\nLine 25\r\n', vt)
    expect('sb_pushline 80 =')
    screen_row(23, 'Line 25', screen)
    cursor(24, 0, state)
    resize(24, 80, vt)
    expect('sb_pushline 80 =')
    screen_row(22, 'Line 25', screen)
    cursor(23, 0, state)

    -- Resize shorter does not send the cursor to a negative row
    -- See also https://github.com/vim/vim/pull/6141
    reset(state, screen)
    wantscreen(vt)
    resize(25, 80, vt)
    screen = wantscreen(vt, { b = true })
    push('\x1b[24HLine 24\r\nLine 25\x1b[H', vt)
    cursor(0, 0, state)
    resize(20, 80, vt)
    expect(
      'sb_pushline 80 =\nsb_pushline 80 =\nsb_pushline 80 =\nsb_pushline 80 =\nsb_pushline 80 ='
    )
    cursor(0, 0, state)

    -- Resize taller attempts to pop scrollback
    reset(state, screen)
    screen = wantscreen(vt)
    resize(25, 80, vt)
    push('Line 1\x1b[25HBottom\x1b[15H', vt)
    screen_row(0, 'Line 1', screen)
    screen_row(24, 'Bottom', screen)
    cursor(14, 0, state)
    screen = wantscreen(vt, { b = true })
    resize(30, 80, vt)
    expect('sb_popline 80\nsb_popline 80\nsb_popline 80\nsb_popline 80\nsb_popline 80')
    screen_row(0, 'ABCDE', screen)
    screen_row(5, 'Line 1', screen)
    screen_row(29, 'Bottom', screen)
    cursor(19, 0, state)
    screen = wantscreen(vt)

    -- Resize can operate on altscreen
    reset(state, screen)
    screen = wantscreen(vt, { a = true })
    resize(25, 80, vt)
    push('Main screen\x1b[?1049h\x1b[HAlt screen', vt)
    resize(30, 80, vt)
    screen_row(0, 'Alt screen', screen)
    push('\x1b[?1049l', vt)
    screen_row(0, 'Main screen', screen)
  end)

  itp('64screen_pen', function()
    local vt = init()
    local screen = wantscreen(vt)

    reset(nil, screen)

    -- Plain
    push('A', vt)
    screen_cell(0, 0, '{41} width=1 attrs={} fg=rgb(240,240,240) bg=rgb(0,0,0)', screen)

    -- Bold
    push('\x1b[1mB', vt)
    screen_cell(0, 1, '{42} width=1 attrs={B} fg=rgb(240,240,240) bg=rgb(0,0,0)', screen)

    -- Italic
    push('\x1b[3mC', vt)
    screen_cell(0, 2, '{43} width=1 attrs={BI} fg=rgb(240,240,240) bg=rgb(0,0,0)', screen)

    -- Underline
    push('\x1b[4mD', vt)
    screen_cell(0, 3, '{44} width=1 attrs={BU1I} fg=rgb(240,240,240) bg=rgb(0,0,0)', screen)

    -- Reset
    push('\x1b[mE', vt)
    screen_cell(0, 4, '{45} width=1 attrs={} fg=rgb(240,240,240) bg=rgb(0,0,0)', screen)

    -- Font
    push('\x1b[11mF\x1b[m', vt)
    screen_cell(0, 5, '{46} width=1 attrs={F1} fg=rgb(240,240,240) bg=rgb(0,0,0)', screen)

    -- Foreground
    push('\x1b[31mG\x1b[m', vt)
    screen_cell(0, 6, '{47} width=1 attrs={} fg=rgb(224,0,0) bg=rgb(0,0,0)', screen)

    -- Background
    push('\x1b[42mH\x1b[m', vt)
    screen_cell(0, 7, '{48} width=1 attrs={} fg=rgb(240,240,240) bg=rgb(0,224,0)', screen)

    -- Super/subscript
    push('x\x1b[74m0\x1b[73m2\x1b[m', vt)
    screen_cell(0, 8, '{78} width=1 attrs={} fg=rgb(240,240,240) bg=rgb(0,0,0)', screen)
    screen_cell(0, 9, '{30} width=1 attrs={S_} fg=rgb(240,240,240) bg=rgb(0,0,0)', screen)
    screen_cell(0, 10, '{32} width=1 attrs={S^} fg=rgb(240,240,240) bg=rgb(0,0,0)', screen)

    -- Dim
    push('\x1b[2mI\x1b[m', vt)
    screen_cell(0, 11, '{49} width=1 attrs={D} fg=rgb(240,240,240) bg=rgb(0,0,0)', screen)

    -- Overline
    push('\x1b[53mJ\x1b[m', vt)
    screen_cell(0, 12, '{4a} width=1 attrs={O} fg=rgb(240,240,240) bg=rgb(0,0,0)', screen)

    -- EL sets only colours to end of line, not other attrs
    push('\x1b[H\x1b[7;33;44m\x1b[K', vt)
    screen_cell(0, 0, '{} width=1 attrs={} fg=rgb(224,224,0) bg=rgb(0,0,224)', screen)
    screen_cell(0, 79, '{} width=1 attrs={} fg=rgb(224,224,0) bg=rgb(0,0,224)', screen)

    -- DECSCNM xors reverse for entire screen
    push('R\x1b[?5h', vt)
    screen_cell(0, 0, '{52} width=1 attrs={} fg=rgb(224,224,0) bg=rgb(0,0,224)', screen)
    screen_cell(1, 0, '{} width=1 attrs={R} fg=rgb(240,240,240) bg=rgb(0,0,0)', screen)
    push('\x1b[?5$p', vt)
    expect_output('\x1b[?5;1$y')
    push('\x1b[?5l', vt)
    screen_cell(0, 0, '{52} width=1 attrs={R} fg=rgb(224,224,0) bg=rgb(0,0,224)', screen)
    screen_cell(1, 0, '{} width=1 attrs={} fg=rgb(240,240,240) bg=rgb(0,0,0)', screen)
    -- TODO(dundargoc): fix
    -- push('\x1b[?5$p')
    -- expect_output('\x1b[?5;2$y')

    -- Set default colours
    reset(nil, screen)
    push('ABC\x1b[31mDEF\x1b[m', vt)
    screen_cell(0, 0, '{41} width=1 attrs={} fg=rgb(240,240,240) bg=rgb(0,0,0)', screen)
    screen_cell(0, 3, '{44} width=1 attrs={} fg=rgb(224,0,0) bg=rgb(0,0,0)', screen)
    -- TODO(dundargoc): fix
    -- SETDEFAULTCOL rgb(252,253,254)
    --   ?screen_cell 0,0  = {41} width=1 attrs={} fg=rgb(252,253,254) bg=rgb(0,0,0)
    --   ?screen_cell 0,3  = {44} width=1 attrs={} fg=rgb(224,0,0) bg=rgb(0,0,0)
    -- SETDEFAULTCOL rgb(250,250,250) rgb(10,20,30)
    --   ?screen_cell 0,0  = {41} width=1 attrs={} fg=rgb(250,250,250) bg=rgb(10,20,30)
    --   ?screen_cell 0,3  = {44} width=1 attrs={} fg=rgb(224,0,0) bg=rgb(10,20,30)
  end)

  itp('65screen_protect', function()
    local vt = init()
    local screen = wantscreen(vt)

    -- Selective erase
    reset(nil, screen)
    push('A\x1b[1"qB\x1b["qC', vt)
    screen_row(0, 'ABC', screen)
    push('\x1b[G\x1b[?J', vt)
    screen_row(0, ' B', screen)

    -- Non-selective erase
    reset(nil, screen)
    push('A\x1b[1"qB\x1b["qC', vt)
    screen_row(0, 'ABC', screen)
    -- TODO(dundargoc): fix
    -- push('\x1b[G\x1b[J', vt)
    -- screen_row(0, '', screen)
  end)

  itp('66screen_extent', function()
    local vt = init()
    local screen = wantscreen(vt)

    -- Bold extent
    reset(nil, screen)
    push('AB\x1b[1mCD\x1b[mE', vt)
    screen_attrs_extent(0, 0, '0,0-1,1', screen)
    screen_attrs_extent(0, 1, '0,0-1,1', screen)
    screen_attrs_extent(0, 2, '0,2-1,3', screen)
    screen_attrs_extent(0, 3, '0,2-1,3', screen)
    screen_attrs_extent(0, 4, '0,4-1,79', screen)
  end)

  itp('67screen_dbl_wh', function()
    local vt = init()
    local screen = wantscreen(vt)

    reset(nil, screen)

    -- Single Width, Single Height
    reset(nil, screen)
    push('\x1b#5', vt)
    push('abcde', vt)
    screen_cell(0, 0, '{61} width=1 attrs={} fg=rgb(240,240,240) bg=rgb(0,0,0)', screen)

    -- Double Width, Single Height
    reset(nil, screen)
    push('\x1b#6', vt)
    push('abcde', vt)
    screen_cell(0, 0, '{61} width=1 attrs={} dwl fg=rgb(240,240,240) bg=rgb(0,0,0)', screen)

    -- Double Height
    reset(nil, screen)
    push('\x1b#3', vt)
    push('abcde', vt)
    push('\r\n\x1b#4', vt)
    push('abcde', vt)
    screen_cell(0, 0, '{61} width=1 attrs={} dwl dhl-top fg=rgb(240,240,240) bg=rgb(0,0,0)', screen)
    screen_cell(
      1,
      0,
      '{61} width=1 attrs={} dwl dhl-bottom fg=rgb(240,240,240) bg=rgb(0,0,0)',
      screen
    )

    -- Late change
    reset(nil, screen)
    push('abcde', vt)
    screen_cell(0, 0, '{61} width=1 attrs={} fg=rgb(240,240,240) bg=rgb(0,0,0)', screen)
    push('\x1b#6', vt)
    screen_cell(0, 0, '{61} width=1 attrs={} dwl fg=rgb(240,240,240) bg=rgb(0,0,0)', screen)

    -- DWL doesn't spill over on scroll
    reset(nil, screen)
    push('\x1b[25H\x1b#6Final\r\n', vt)
    screen_cell(23, 0, '{46} width=1 attrs={} dwl fg=rgb(240,240,240) bg=rgb(0,0,0)', screen)
    screen_cell(24, 0, '{} width=1 attrs={} fg=rgb(240,240,240) bg=rgb(0,0,0)', screen)
  end)

  itp('68screen_termprops', function()
    local vt = init()
    local screen = wantscreen(vt, { p = true })

    reset(nil, screen)
    expect('settermprop 1 true\nsettermprop 2 true\nsettermprop 7 1')

    -- Cursor visibility
    push('\x1b[?25h', vt)
    expect('settermprop 1 true')
    push('\x1b[?25l', vt)
    expect('settermprop 1 false')

    -- Title
    push('\x1b]2;Here is my title\a', vt)
    expect('settermprop 4 ["Here is my title"]')
  end)

  itp('69screen_pushline', function()
    local vt = init()
    -- Run these tests on a much smaller default screen, so debug output is nowhere near as noisy
    resize(5, 10, vt)
    local state = wantstate(vt)
    local screen = wantscreen(vt, { r = true })
    reset(state, screen)

    -- Resize wider reflows wide lines
    reset(state, screen)
    push(string.rep('A', 12), vt)
    screen_row(0, 'AAAAAAAAAA', screen, vt.cols)
    screen_row(1, 'AA', screen, vt.cols)
    lineinfo(1, { cont = true }, state)
    cursor(1, 2, state)
    resize(5, 15, vt)
    screen_row(0, 'AAAAAAAAAAAA', screen, vt.cols)
    -- TODO(dundargoc): fix
    -- screen_row(1, '', screen, vt.cols)
    lineinfo(1, {}, state)
    cursor(0, 12, state)
    resize(5, 20, vt)
    screen_row(0, 'AAAAAAAAAAAA', screen, vt.cols)
    -- TODO(dundargoc): fix
    -- screen_row( 1 ,'',screen, vt.cols)
    lineinfo(1, {}, state)
    cursor(0, 12, state)

    -- Resize narrower can create continuation lines
    reset(state, screen)
    resize(5, 10, vt)
    push('ABCDEFGHI', vt)
    screen_row(0, 'ABCDEFGHI', screen, vt.cols)
    -- TODO(dundargoc): fix
    -- screen_row( 1 , "",screen, vt.cols)
    lineinfo(1, {}, state)
    cursor(0, 9, state)
    resize(5, 8, vt)
    -- TODO(dundargoc): fix
    -- screen_row( 0 , "ABCDEFGH",screen,vt.cols)
    screen_row(1, 'I', screen, vt.cols)
    lineinfo(1, { cont = true }, state)
    cursor(1, 1, state)
    resize(5, 6, vt)
    screen_row(0, 'ABCDEF', screen, vt.cols)
    screen_row(1, 'GHI', screen, vt.cols)
    lineinfo(1, { cont = true }, state)
    cursor(1, 3, state)

    -- Shell wrapped prompt behaviour
    reset(state, screen)
    resize(5, 10, vt)
    push('PROMPT GOES HERE\r\n> \r\n\r\nPROMPT GOES HERE\r\n> ', vt)
    screen_row(0, '> ', screen, vt.cols)
    -- TODO(dundargoc): fix
    -- screen_row( 1 , "",screen,vt.cols)
    screen_row(2, 'PROMPT GOE', screen, vt.cols)
    screen_row(3, 'S HERE', screen, vt.cols)
    lineinfo(3, { cont = true }, state)
    screen_row(4, '> ', screen, vt.cols)
    cursor(4, 2, state)
    resize(5, 11, vt)
    screen_row(0, '> ', screen, vt.cols)
    -- TODO(dundargoc): fix
    -- screen_row( 1 , "",screen,vt.cols)
    screen_row(2, 'PROMPT GOES', screen, vt.cols)
    screen_row(3, ' HERE', screen, vt.cols)
    lineinfo(3, { cont = true }, state)
    screen_row(4, '> ', screen, vt.cols)
    cursor(4, 2, state)
    resize(5, 12, vt)
    screen_row(0, '> ', screen, vt.cols)
    -- TODO(dundargoc): fix
    -- screen_row( 1 , "",screen,vt.cols)
    screen_row(2, 'PROMPT GOES ', screen, vt.cols)
    screen_row(3, 'HERE', screen, vt.cols)
    lineinfo(3, { cont = true }, state)
    screen_row(4, '> ', screen, vt.cols)
    cursor(4, 2, state)
    resize(5, 16, vt)
    screen_row(0, '> ', screen, vt.cols)
    -- TODO(dundargoc): fix
    -- screen_row( 1 , "",screen,vt.cols)
    -- screen_row( 2 , "PROMPT GOES HERE",screen,vt.cols)
    lineinfo(3, {}, state)
    screen_row(3, '> ', screen, vt.cols)
    cursor(3, 2, state)

    -- Cursor goes missing
    -- For more context: https://github.com/neovim/neovim/pull/21124
    reset(state, screen)
    resize(5, 5, vt)
    resize(3, 1, vt)
    push('\x1b[2;1Habc\r\n\x1b[H', vt)
    resize(1, 1, vt)
    cursor(0, 0, state)
  end)

  pending('90vttest_01-movement-1', function() end)
  pending('90vttest_01-movement-2', function() end)

  itp('90vttest_01-movement-3', function()
    -- Test of cursor-control characters inside ESC sequences
    local vt = init()
    local state = wantstate(vt)
    local screen = wantscreen(vt)

    reset(state, screen)

    push('A B C D E F G H I', vt)
    push('\x0d\x0a', vt)
    push('A\x1b[2\bCB\x1b[2\bCC\x1b[2\bCD\x1b[2\bCE\x1b[2\bCF\x1b[2\bCG\x1b[2\bCH\x1b[2\bCI', vt)
    push('\x0d\x0a', vt)
    push(
      'A \x1b[\x0d2CB\x1b[\x0d4CC\x1b[\x0d6CD\x1b[\x0d8CE\x1b[\x0d10CF\x1b[\x0d12CG\x1b[\x0d14CH\x1b[\x0d16CI',
      vt
    )
    push('\x0d\x0a', vt)
    push(
      'A \x1b[1\x0bAB \x1b[1\x0bAC \x1b[1\x0bAD \x1b[1\x0bAE \x1b[1\x0bAF \x1b[1\x0bAG \x1b[1\x0bAH \x1b[1\x0bAI \x1b[1\x0bA',
      vt
    )

    -- Output

    for i = 0, 2 do
      screen_row(i, 'A B C D E F G H I', screen)
    end
    screen_row(3, 'A B C D E F G H I ', screen)

    cursor(3, 18, state)
  end)

  itp('90vttest_01-movement-4', function()
    -- Test of leading zeroes in ESC sequences
    local vt = init()
    local screen = wantscreen(vt)

    reset(nil, screen)

    push('\x1b[00000000004;000000001HT', vt)
    push('\x1b[00000000004;000000002Hh', vt)
    push('\x1b[00000000004;000000003Hi', vt)
    push('\x1b[00000000004;000000004Hs', vt)
    push('\x1b[00000000004;000000005H ', vt)
    push('\x1b[00000000004;000000006Hi', vt)
    push('\x1b[00000000004;000000007Hs', vt)
    push('\x1b[00000000004;000000008H ', vt)
    push('\x1b[00000000004;000000009Ha', vt)
    push('\x1b[00000000004;0000000010H ', vt)
    push('\x1b[00000000004;0000000011Hc', vt)
    push('\x1b[00000000004;0000000012Ho', vt)
    push('\x1b[00000000004;0000000013Hr', vt)
    push('\x1b[00000000004;0000000014Hr', vt)
    push('\x1b[00000000004;0000000015He', vt)
    push('\x1b[00000000004;0000000016Hc', vt)
    push('\x1b[00000000004;0000000017Ht', vt)
    push('\x1b[00000000004;0000000018H ', vt)
    push('\x1b[00000000004;0000000019Hs', vt)
    push('\x1b[00000000004;0000000020He', vt)
    push('\x1b[00000000004;0000000021Hn', vt)
    push('\x1b[00000000004;0000000022Ht', vt)
    push('\x1b[00000000004;0000000023He', vt)
    push('\x1b[00000000004;0000000024Hn', vt)
    push('\x1b[00000000004;0000000025Hc', vt)
    push('\x1b[00000000004;0000000026He', vt)

    -- Output

    screen_row(3, 'This is a correct sentence', screen)
  end)

  pending('90vttest_02-screen-1', function() end)
  pending('90vttest_02-screen-2', function() end)

  itp('90vttest_02-screen-3', function()
    -- Origin mode
    local vt = init()
    local screen = wantscreen(vt)

    reset(nil, screen)

    push('\x1b[?6h', vt)
    push('\x1b[23;24r', vt)
    push('\n', vt)
    push('Bottom', vt)
    push('\x1b[1;1H', vt)
    push('Above', vt)

    -- Output
    screen_row(22, 'Above', screen)
    screen_row(23, 'Bottom', screen)
  end)

  itp('90vttest_02-screen-4', function()
    -- Origin mode (2)
    local vt = init()
    local screen = wantscreen(vt)

    reset(nil, screen)

    push('\x1b[?6l', vt)
    push('\x1b[23;24r', vt)
    push('\x1b[24;1H', vt)
    push('Bottom', vt)
    push('\x1b[1;1H', vt)
    push('Top', vt)

    -- Output
    screen_row(23, 'Bottom', screen)
    screen_row(0, 'Top', screen)
  end)

  itp('Mouse reporting should not break by idempotent DECSM 1002', function()
    -- Regression test for https://bugs.launchpad.net/libvterm/+bug/1640917
    -- Related: https://github.com/neovim/neovim/issues/5583
    local vt = init()
    wantstate(vt, {})

    push('\x1b[?1002h', vt)
    mousemove(0, 0, vt)
    mousebtn('d', 1, vt)
    expect_output('\x1b[M\x20\x21\x21')
    mousemove(1, 0, vt)
    expect_output('\x1b[M\x40\x21\x22')
    push('\x1b[?1002h', vt)
    mousemove(2, 0, vt)
    expect_output('\x1b[M\x40\x21\x23')
  end)
end)
