#!/bin/bash
#
# editor_test.sh — Regression tests for the Klammertext editor support
# (doc/edit/: the shared Python core, the language server, and the Emacs,
# Sublime Text, Vim, and VS Code integrations built on them).
#
# Why this lives in tst/: the editor code implements the LANGUAGE's structural
# layer — @-run length, bar-run dimension, ^-escapes, # removal, literal
# spans, nesting depth — independently of both the SKS and the Klammermachine.
# tst/ is the SKS-independent tier, and these suites check that the
# implementations of Klammertext's structure agree with the canonical
# formatting conventions AND with each other.  The klammer names that appear
# in the fixtures (@ol, @table, @document, @code, @c) are seeded configuration of
# the editor tools, not SKS dependencies; nothing here runs ktext or loads a
# klammer set.
#
# What is checked, for every fixture pair <name>.kt / <name>_expected.kt in
# tst/editor/:
#   * the shared core's output equals the expected file — through the Python
#     API (what Sublime uses) AND through the core's CLI (what Vim uses)
#   * idempotence: the tool applied to the expected file leaves it unchanged
#   * the three Sublime adapter plugins import outside Sublime and resolve
#     to the shared core
#   * the language server passes its protocol test (tst/editor/ls_test.py:
#     handshake, diagnostics, formatting, matching, alignTable — the VS Code
#     extension's whole surface)
#   * when Emacs is installed: the Emacs units' output equals the expected
#     file, and equals the shared core's output byte for byte (the sync
#     check across the independent elisp implementation) — skipped otherwise
#   * when a Vim with +eval is installed: the Vim plugin's commands produce
#     the same bytes (exercising the CLI shell-out glue) — skipped otherwise
#
# The editor code lives in doc/edit/ (same layout in the development tree and
# the distribution).
#
# Usage: ./editor_test.sh          Requires python3; Emacs and Vim optional.
# Exit code: 0 if all tests pass, 1 otherwise.

PASS=0
FAIL=0

red=$'\033[31m'
green=$'\033[32m'
bold=$'\033[1m'
reset=$'\033[0m'

HERE="$(cd "$(dirname "$0")" && pwd)"
ROOT="$(cd "$HERE/.." && pwd)"
EDIT_DIR="$ROOT/doc/edit"
if [ ! -d "$EDIT_DIR/emacs" ]; then
    echo "editor_test.sh: cannot locate the editor support (doc/edit)" >&2
    exit 1
fi
EMACS_DIR="$EDIT_DIR/emacs"
SUBLIME_DIR="$EDIT_DIR/sublime"
SHARED_DIR="$EDIT_DIR/shared"
VIM_DIR="$EDIT_DIR/vim"
FIX="$HERE/editor"
OUT="$(mktemp -d)"
trap 'rm -rf "$OUT"' EXIT

INDENT_FIXTURES="indent_list indent_document indent_table indent_untouched
                 indent_defs indent_escapes indent_named_close indent_literal_c"
ALIGN_FIXTURES="align_mixed align_empty_cells align_boundary align_colspan
                align_escapes align_too_wide"

# Build the (MODE INFILE OUTFILE) triples: each fixture is run from its input
# and from its expected file (the idempotence check).
py_args=()
el_args=()
add_fixture() {   # add_fixture MODE NAME
    local mode="$1" name="$2"
    py_args+=("$mode"     "$FIX/$name.kt"            "$OUT/$name.py.out")
    py_args+=("$mode"     "$FIX/${name}_expected.kt" "$OUT/$name.py.idem")
    py_args+=("$mode-cli" "$FIX/$name.kt"            "$OUT/$name.cli.out")
    el_args+=("$mode"     "$FIX/$name.kt"            "$OUT/$name.el.out")
    el_args+=("$mode"     "$FIX/${name}_expected.kt" "$OUT/$name.el.idem")
}
for f in $INDENT_FIXTURES; do add_fixture indent "$f"; done
for f in $ALIGN_FIXTURES;  do add_fixture align  "$f"; done

check() {   # check NAME FILE_A FILE_B
    if diff -q "$2" "$3" >/dev/null 2>&1; then
        echo "${green}PASS${reset} $1"
        PASS=$((PASS + 1))
    else
        echo "${red}FAIL${reset} $1"
        diff "$2" "$3" | head -10
        FAIL=$((FAIL + 1))
    fi
}

# --- the shared core: API and CLI (required) -------------------------------
if ! command -v python3 >/dev/null 2>&1; then
    echo "editor_test.sh: python3 not found" >&2
    exit 1
fi
if ! PYTHONDONTWRITEBYTECODE=1 python3 "$FIX/editor_driver.py" "$SHARED_DIR" "${py_args[@]}"; then
    echo "editor_test.sh: the Python driver failed" >&2
    exit 1
fi

# --- the Sublime adapters resolve to the shared core -----------------------
if PYTHONDONTWRITEBYTECODE=1 python3 -c "
import sys
sys.path.insert(0, '$SUBLIME_DIR')
import Klammertext, Klammertext_indent, Klammertext_align
assert Klammertext.KE.__file__.startswith('$SHARED_DIR')
" 2>"$OUT/sublime.log"; then
    echo "${green}PASS${reset} sublime adapters import the shared core"
    PASS=$((PASS + 1))
else
    echo "${red}FAIL${reset} sublime adapters import the shared core"
    cat "$OUT/sublime.log"
    FAIL=$((FAIL + 1))
fi

# --- the language server protocol test -------------------------------------
if PYTHONDONTWRITEBYTECODE=1 python3 "$FIX/ls_test.py" "$SHARED_DIR" "$FIX" >"$OUT/ls.log" 2>&1; then
    echo "${green}PASS${reset} language server protocol ($(grep -c '^PASS' "$OUT/ls.log") checks)"
    PASS=$((PASS + 1))
else
    echo "${red}FAIL${reset} language server protocol:"
    cat "$OUT/ls.log"
    FAIL=$((FAIL + 1))
fi

# --- the Emacs units (optional) --------------------------------------------
HAVE_EMACS=0
if command -v emacs >/dev/null 2>&1; then
    if emacs --batch -L "$EMACS_DIR" \
             -l klammertext-mode -l klammertext-indent -l klammertext-align \
             -l "$FIX/editor_driver.el" "${el_args[@]}" 2>"$OUT/emacs.log"; then
        HAVE_EMACS=1
    else
        echo "${red}FAIL${reset} the Emacs driver failed:" >&2
        cat "$OUT/emacs.log" >&2
        FAIL=$((FAIL + 1))
    fi
else
    echo "(Emacs not installed — the Emacs half is skipped; the shared core still runs)"
fi

# --- the Vim plugin (optional) ---------------------------------------------
HAVE_VIM=0
VIM_BIN="$(command -v vim || true)"
if [ -n "$VIM_BIN" ] && "$VIM_BIN" --version 2>/dev/null | grep -q '+eval'; then
    HAVE_VIM=1
    run_vim() {   # run_vim MODE INFILE OUTFILE
        local cmd
        if [ "$1" = indent ]; then
            cmd='KlammertextReindent'
        else
            cmd='call search("|") | KlammertextAlign'
        fi
        "$VIM_BIN" -N -n -u NONE -i NONE -es --not-a-term \
            --cmd "set rtp^=$VIM_DIR" \
            -c 'filetype plugin on' \
            -c "edit! $2" -c 'set ft=klammertext' \
            -c "$cmd" -c "saveas! $3" -c 'qa!' </dev/null >/dev/null 2>&1
    }
    for f in $INDENT_FIXTURES; do
        run_vim indent "$FIX/$f.kt"           "$OUT/$f.vim.out"
        run_vim indent "$FIX/${f}_expected.kt" "$OUT/$f.vim.idem"
    done
    for f in $ALIGN_FIXTURES; do
        run_vim align "$FIX/$f.kt"           "$OUT/$f.vim.out"
        run_vim align "$FIX/${f}_expected.kt" "$OUT/$f.vim.idem"
    done
    # The comprehensive plugin checks: ftdetect (Kotlin override), syntax
    # token classes, jump-to-match (incl. multibyte columns), the location-
    # list check, and — with +python3 — indentexpr (gg=G) and the live
    # match highlighter.  Sections skip inside the driver per Vim feature.
    if KT_FIX="$FIX" KT_OUT="$OUT/vim_feature.txt" \
       "$VIM_BIN" -N -n -u NONE -i NONE -es --not-a-term \
           --cmd "set rtp^=$VIM_DIR" \
           -c "source $FIX/vim_feature_test.vim" </dev/null >/dev/null 2>&1; then
        echo "${green}PASS${reset} vim plugin features ($(grep -c '^PASS' "$OUT/vim_feature.txt") checks$(
            grep -q '^SKIP' "$OUT/vim_feature.txt" && printf '; %s' "$(grep -c '^SKIP' "$OUT/vim_feature.txt") section(s) skipped"))"
        PASS=$((PASS + 1))
    else
        echo "${red}FAIL${reset} vim plugin features:"
        grep -v '^PASS' "$OUT/vim_feature.txt" 2>/dev/null || echo "  (the Vim driver itself failed)"
        FAIL=$((FAIL + 1))
    fi
else
    echo "(no Vim with +eval installed — the Vim half is skipped; the shared core still runs)"
fi

# --- the VS Code extension (optional: needs a Node runtime) ----------------
# node itself, or VS Code's Electron binary run as Node.
run_node() {
    if command -v node >/dev/null 2>&1; then
        node "$@"
    elif [ -x /usr/share/code/code ]; then
        ELECTRON_RUN_AS_NODE=1 /usr/share/code/code "$@"
    else
        return 127
    fi
}
if command -v node >/dev/null 2>&1 || [ -x /usr/share/code/code ]; then
    if run_node "$FIX/vscode_ext_test.js" "$EDIT_DIR/vscode" "$FIX" >"$OUT/vscode.log" 2>&1; then
        echo "${green}PASS${reset} vscode extension ($(grep -c '^PASS' "$OUT/vscode.log") checks)"
        PASS=$((PASS + 1))
    else
        echo "${red}FAIL${reset} vscode extension:"
        cat "$OUT/vscode.log"
        FAIL=$((FAIL + 1))
    fi
else
    echo "(no Node runtime — the VS Code extension test is skipped; the language server is still tested)"
fi

# --- the VS Code .vsix package builds with the full payload ----------------
# (the no-marketplace install path; needs only bash + python3)
mkdir -p "$OUT/vsix"
cp -R "$EDIT_DIR/vscode" "$EDIT_DIR/shared" "$OUT/vsix/"
if ( cd "$OUT/vsix/vscode" && ./make_vsix.sh >/dev/null 2>&1 ) && \
   PYTHONDONTWRITEBYTECODE=1 python3 -c "
import sys, zipfile
names = set(zipfile.ZipFile('$OUT/vsix/vscode/klammertext.vsix').namelist())
need = {'extension.vsixmanifest', '[Content_Types].xml',
        'extension/package.json', 'extension/extension.js',
        'extension/language-configuration.json', 'extension/README.md',
        'extension/klammertext_edit.py', 'extension/klammertext_ls.py',
        'extension/syntaxes/klammertext.tmLanguage.json'}
sys.exit(1 if need - names else 0)
"; then
    echo "${green}PASS${reset} vscode .vsix package builds (make_vsix.sh)"
    PASS=$((PASS + 1))
else
    echo "${red}FAIL${reset} vscode .vsix package builds (make_vsix.sh)"
    FAIL=$((FAIL + 1))
fi

# --- compare ---------------------------------------------------------------
for f in $INDENT_FIXTURES $ALIGN_FIXTURES; do
    check "$f (python)"            "$OUT/$f.py.out"  "$FIX/${f}_expected.kt"
    check "$f (python idempotent)" "$OUT/$f.py.idem" "$FIX/${f}_expected.kt"
    check "$f (cli)"               "$OUT/$f.cli.out" "$FIX/${f}_expected.kt"
    if [ "$HAVE_EMACS" = 1 ]; then
        check "$f (emacs)"            "$OUT/$f.el.out"  "$FIX/${f}_expected.kt"
        check "$f (emacs idempotent)" "$OUT/$f.el.idem" "$FIX/${f}_expected.kt"
        check "$f (emacs == python)"  "$OUT/$f.el.out"  "$OUT/$f.py.out"
    fi
    if [ "$HAVE_VIM" = 1 ]; then
        check "$f (vim)"            "$OUT/$f.vim.out"  "$FIX/${f}_expected.kt"
        check "$f (vim idempotent)" "$OUT/$f.vim.idem" "$FIX/${f}_expected.kt"
    fi
done

echo
echo "${bold}editor_test: $PASS passed, $FAIL failed${reset}"
[ "$FAIL" -eq 0 ]
