" vim_feature_test.vim — comprehensive checks of the Klammertext Vim plugin,
" beyond the fixture runs in editor_test.sh (which cover the Reindent and
" Align commands' output bytes).  Run headlessly:
"
"   KT_FIX=<fixture dir> KT_OUT=<result file> \
"   vim -N -n -u NONE -i NONE -es --not-a-term \
"       --cmd 'set rtp^=<doc/edit/vim>' -c 'source vim_feature_test.vim'
"
" What is checked:
"   * ftdetect: .kt maps to the klammertext filetype, overriding the stock
"     runtime's .kt -> kotlin mapping
"   * syntax: token classes at known positions (three @-tiers open/close,
"     removal line/block/file with markers, verbatim @code interior,
"     ^-escapes, abbreviated @name-arg)
"   * commands: KlammertextJumpToMatch (open->close, close->open,
"     multibyte columns), KlammertextCheck (location list)
"   * with +python3: 'indentexpr' drives the = operator (gg=G equals the
"     expected fixture), and the CursorMoved match highlighter sets
"     KlammertextMatch / KlammertextMismatch matches
"
" Writes PASS/FAIL lines to $KT_OUT; exits nonzero if anything failed.

let s:results = []
function! s:Check(name, ok, ...) abort
  let detail = (a:ok || !a:0) ? '' : ' -- ' . string(a:1)
  call add(s:results, (a:ok ? 'PASS ' : 'FAIL ') . a:name . detail)
endfunction

function! s:Syn(l, c) abort
  return synIDattr(synID(a:l, a:c, 1), 'name')
endfunction

filetype plugin on
if has('syntax')
  syntax enable
endif

" --- ftdetect: .kt is klammertext (the Kotlin override) --------------------
execute 'edit! ' . fnameescape($KT_FIX . '/indent_list.kt')
call s:Check('ftdetect .kt -> klammertext (overrides kotlin)',
      \ &filetype ==# 'klammertext', &filetype)

" --- syntax token classes ---------------------------------------------------
if has('syntax')
enew!
call setline(1, [
      \ '@i abc @ x',
      \ '@@def : x @@',
      \ '@@@target x @@@',
      \ '# removed line',
      \ '#[ rem ]# after',
      \ '^@ escaped',
      \ '@code x @ y code@',
      \ 'name@ x',
      \ '@name-arg x',
      \ '## tail',
      \ 'still removed'])
set filetype=klammertext
call s:Check('syntax loaded', b:current_syntax ==# 'klammertext')
call s:Check('app open @i',        s:Syn(1, 1)  ==# 'klammertextAppOpen',  s:Syn(1, 1))
call s:Check('app bare close',     s:Syn(1, 8)  ==# 'klammertextAppClose', s:Syn(1, 8))
call s:Check('plain text',         s:Syn(1, 10) ==# '',                    s:Syn(1, 10))
call s:Check('def open @@def',     s:Syn(2, 1)  ==# 'klammertextDefOpen',  s:Syn(2, 1))
call s:Check('def bare close',     s:Syn(2, 11) ==# 'klammertextDefClose', s:Syn(2, 11))
call s:Check('sys open @@@target', s:Syn(3, 1)  ==# 'klammertextSysOpen',  s:Syn(3, 1))
call s:Check('sys bare close',     s:Syn(3, 13) ==# 'klammertextSysClose', s:Syn(3, 13))
call s:Check('line marker #',      s:Syn(4, 1)  ==# 'klammertextMarkerLine', s:Syn(4, 1))
call s:Check('line removed text',  s:Syn(4, 5)  ==# 'klammertextRemovedLine', s:Syn(4, 5))
call s:Check('block marker #[',    s:Syn(5, 1)  ==# 'klammertextMarker',   s:Syn(5, 1))
call s:Check('block removed text', s:Syn(5, 5)  ==# 'klammertextRemovedBlock', s:Syn(5, 5))
call s:Check('block marker ]#',    s:Syn(5, 8)  ==# 'klammertextMarker',   s:Syn(5, 8))
call s:Check('text after block',   s:Syn(5, 12) ==# '',                    s:Syn(5, 12))
call s:Check('escape ^@ consumed', s:Syn(6, 1)  ==# 'klammertextEscape'
      \ && s:Syn(6, 2) ==# 'klammertextEscape', s:Syn(6, 2))
call s:Check('verbatim open',      s:Syn(7, 1)  ==# 'klammertextAppOpen',  s:Syn(7, 1))
call s:Check('verbatim interior @', s:Syn(7, 9) ==# 'klammertextVerbatim', s:Syn(7, 9))
call s:Check('verbatim close',     s:Syn(7, 17) ==# 'klammertextAppClose', s:Syn(7, 17))
call s:Check('named close name@',  s:Syn(8, 1)  ==# 'klammertextAppClose'
      \ && s:Syn(8, 5) ==# 'klammertextAppClose', s:Syn(8, 1))
call s:Check('abbreviated @name-',  s:Syn(9, 1) ==# 'klammertextAppOpen'
      \ && s:Syn(9, 6) ==# '' && s:Syn(9, 7) ==# '', s:Syn(9, 6))
call s:Check('file marker ##',     s:Syn(10, 1) ==# 'klammertextMarker',   s:Syn(10, 1))
call s:Check('## removes to EOF',  s:Syn(11, 3) ==# 'klammertextRemovedFile', s:Syn(11, 3))

" @c is the single-letter literal klammer (inline @code, 2026-08-16).  A
" fresh buffer: the ## in the buffer above removes to EOF.  The traps a
" single-letter name adds: a word ending in c ("basic@") must not close the
" region, and "@caption" must not open one.
enew!
call setline(1, [
      \ '@c y @ basic@ z c@ tail',
      \ '@caption arg @'])
set filetype=klammertext
call s:Check('c verbatim open @c',   s:Syn(1, 1)  ==# 'klammertextAppOpen',  s:Syn(1, 1))
call s:Check('c interior @ verbatim', s:Syn(1, 6)  ==# 'klammertextVerbatim', s:Syn(1, 6))
call s:Check('basic@ does not close @c', s:Syn(1, 13) ==# 'klammertextVerbatim', s:Syn(1, 13))
call s:Check('real close c@',        s:Syn(1, 17) ==# 'klammertextAppClose', s:Syn(1, 17))
call s:Check('text after c@ plain',  s:Syn(1, 20) ==# '',                    s:Syn(1, 20))
call s:Check('@caption opens @caption, not @c', s:Syn(2, 1) ==# 'klammertextAppOpen', s:Syn(2, 1))
else
  call add(s:results, 'SKIP syntax checks (this Vim lacks +syntax)')
endif

" --- jump-to-match (the CLI shell-out path) ---------------------------------
enew!
call setline(1, ['@i abc @ x'])
set filetype=klammertext
call cursor(1, 1)
KlammertextJumpToMatch
call s:Check('jump open -> close', col('.') == 8, col('.'))
KlammertextJumpToMatch
call s:Check('jump close -> open', col('.') == 1, col('.'))

" multibyte: 2-byte umlauts before the delimiters
enew!
call setline(1, ['üü @i x @'])
set filetype=klammertext
call cursor(1, 6)                  " byte col 6 = the @ of @i (2 x 2-byte ü)
KlammertextJumpToMatch
call s:Check('jump with multibyte line',
      \ (exists('*charcol') ? charcol('.') : col('.'))
      \    == (exists('*charcol') ? 9 : 11),
      \ [col('.'), getline('.')])

" --- KlammertextCheck: diagnostics into the location list -------------------
enew!
call setline(1, ['@i abc', '@ol x ul@'])
set filetype=klammertext
KlammertextCheck
let s:ll = getloclist(0)
call s:Check('check fills the location list', len(s:ll) == 2, s:ll)
call s:Check('check positions', len(s:ll) == 2
      \ && s:ll[0].lnum == 1 && s:ll[0].col == 1
      \ && s:ll[1].lnum == 2 && s:ll[1].col == 7, s:ll)
call s:Check('check messages', len(s:ll) == 2
      \ && s:ll[0].text =~# 'never closed' && s:ll[1].text =~# 'ul@', s:ll)
lclose

enew!
call setline(1, ['@i balanced @'])
set filetype=klammertext
KlammertextCheck
call s:Check('check clean buffer -> empty list', len(getloclist(0)) == 0)

" --- +python3: indentexpr (= operator) and live match highlighting ---------
if has('python3')
  for s:fix in ['indent_list', 'indent_document', 'indent_named_close']
    execute 'edit! ' . fnameescape($KT_FIX . '/' . s:fix . '.kt')
    call s:Check(s:fix . ': indentexpr is set',
          \ &indentexpr ==# 'klammertext#IndentExpr()', &indentexpr)
    normal! gg=G
    call s:Check(s:fix . ': gg=G equals expected fixture',
          \ getline(1, '$') ==# readfile($KT_FIX . '/' . s:fix . '_expected.kt'))
    edit!
  endfor

  " live match highlighting via the CursorMoved autocmd
  enew!
  call setline(1, ['@i abc @ x'])
  set filetype=klammertext
  call cursor(1, 1)
  doautocmd CursorMoved
  let s:m = filter(getmatches(), 'v:val.group =~# "^Klammertext"')
  call s:Check('highlight: matched pair boxed',
        \ len(s:m) == 1 && s:m[0].group ==# 'KlammertextMatch'
        \ && len(filter(keys(s:m[0]), 'v:val =~# "^pos"')) == 2, s:m)
  call cursor(1, 5)                " not on a delimiter
  doautocmd CursorMoved
  call s:Check('highlight: cleared off-delimiter',
        \ empty(filter(getmatches(), 'v:val.group =~# "^Klammertext"')))

  enew!
  call setline(1, ['@ol x ul@'])
  set filetype=klammertext
  call cursor(1, 1)
  doautocmd CursorMoved
  let s:m = filter(getmatches(), 'v:val.group =~# "^Klammertext"')
  call s:Check('highlight: mismatch in red group',
        \ len(s:m) == 1 && s:m[0].group ==# 'KlammertextMismatch', s:m)

  " default mappings exist (buffer-local, LocalLeader)
  call s:Check('mappings defined',
        \ !empty(maparg('<LocalLeader>j', 'n'))
        \ && !empty(maparg('<LocalLeader>i', 'n'))
        \ && !empty(maparg('<LocalLeader>a', 'n')))
else
  call add(s:results, 'SKIP +python3 checks (this Vim lacks python3)')
endif

" --- write results and exit -------------------------------------------------
call writefile(s:results, $KT_OUT)
if len(filter(copy(s:results), 'v:val =~# "^FAIL"')) > 0
  cquit!
endif
qa!
