#!/usr/bin/env bash
# qa.sh — TUI smoke-test suite for datasight
# Requires: tmux, a built binary at ./target/debug/datasight
# Usage: bash qa.sh
# Run from the repo root.

set -uo pipefail

BINARY="./target/debug/datasight"
SESSION="qa"
APP_PANE="$SESSION:0.0"
PASS=0
FAIL=0
FAILURES=()

# ── tmux setup ────────────────────────────────────────────────────────────────

tmux kill-session -t "$SESSION" 2>/dev/null || true
tmux new-session -d -s "$SESSION" -x 220 -y 50
# Suite O asserts on real clipboard content: with set-clipboard on, tmux accepts
# the app's OSC 52 sequence and stores it in a paste buffer we can read back.
tmux set-option -g set-clipboard on
sleep 0.3

# Scratch fixtures generated at run time. These deliberately do NOT live in
# tests/fixtures/ — Suite Z navigates that directory by hardcoded index, so an
# extra file there would shift the cursor and break the browse suites.
QA_TMP="$(mktemp -d)"
trap 'rm -rf "$QA_TMP"' EXIT

# Long values (issue #23): the widest cell far exceeds the old 40-char clamp.
cat > "$QA_TMP/long_values.csv" <<'CSV'
KeyName,LnkName,KeyLastWriteTimestamp
windows powershe|6d2a715ad3bf3395,C:\ProgramData\Microsoft\Windows\Start Menu\Programs\Windows PowerShell\Windows PowerShell ISE (x86).lnk,2025-05-23 10:52:37
wordpad.lnk|b071df8746c2a535,C:\ProgramData\Microsoft\Windows\Start Menu\Programs\Accessories\Wordpad.lnk,2025-05-23 10:52:34
wps office.lnk|1c3aad1926983f46,c:\users\otello.j\Desktop\WPS Office.lnk,2025-05-23 11:46:14
CSV

# Single data row: polars backs length-1 columns with a Scalar, which the
# renderer once mistook for "no data" and painted as null glyphs.
cat > "$QA_TMP/single_row.csv" <<'CSV'
name,qty
solitary,7
CSV

# Helpers
# send: send literal keys (no special key interpretation).
# The `--` is required: without it tmux parses a payload like "----" as flags.
send()   { tmux send-keys -t "$APP_PANE" -l -- "$1"; sleep "${2:-0.10}"; }
# key: send a named tmux key (Enter, Escape, PgDn, PgUp, etc.)
key()    { tmux send-keys -t "$APP_PANE" "$1"; sleep "${2:-0.15}"; }
esc()    { key Escape 0.15; }
enter()  { key Enter "${1:-0.20}"; }
pgdn()   { key PgDn  0.15; }
pgup()   { key PgUp  0.15; }
cap()    { tmux capture-pane -t "$APP_PANE" -p 2>/dev/null || true; }

start_app() {
  # Kill any running app cleanly; clear shell input line
  tmux send-keys -t "$APP_PANE" C-c; sleep 0.15
  tmux send-keys -t "$APP_PANE" C-u; sleep 0.10
  tmux send-keys -t "$APP_PANE" "$BINARY $*" Enter
  sleep 0.6
}

quit() {
  send "q" 0.20
}

assert_contains() {
  local label="$1" pattern="$2"
  if cap | grep -q "$pattern"; then
    echo "  PASS [$label]"
    PASS=$((PASS + 1))
  else
    echo "  FAIL [$label] — expected: '$pattern'"
    FAIL=$((FAIL + 1))
    FAILURES+=("[$label] expected '$pattern'")
  fi
}

assert_not_contains() {
  local label="$1" pattern="$2"
  if ! cap | grep -q "$pattern"; then
    echo "  PASS [$label]"
    PASS=$((PASS + 1))
  else
    echo "  FAIL [$label] — did NOT expect: '$pattern'"
    FAIL=$((FAIL + 1))
    FAILURES+=("[$label] did not expect '$pattern'")
  fi
}

# Clipboard helpers. drop_buffers clears tmux's paste-buffer stack so a stale
# buffer cannot satisfy the next assertion.
drop_buffers() { while tmux delete-buffer 2>/dev/null; do :; done; }

assert_buffer_contains() {
  local label="$1" pattern="$2"
  if tmux show-buffer 2>/dev/null | grep -qF -- "$pattern"; then
    echo "  PASS [$label]"
    PASS=$((PASS + 1))
  else
    echo "  FAIL [$label] — clipboard buffer missing: '$pattern'"
    FAIL=$((FAIL + 1))
    FAILURES+=("[$label] clipboard buffer missing '$pattern'")
  fi
}

# ── Suite A: File format loading ───────────────────────────────────────────────
echo ""
echo "=== Suite A: File format loading ==="

for fmt in csv tsv json ndjson; do
  start_app "tests/fixtures/orders.$fmt"
  assert_contains "A/$fmt-header" "order_id"
  assert_contains "A/$fmt-col"    "region"
  quit
done

# parquet fixture has a different schema (id, name, age, city, score, active)
start_app "tests/fixtures/orders.parquet"
assert_contains "A/parquet-header" "id"
assert_contains "A/parquet-col"    "city"
quit

start_app "tests/fixtures/wide.csv"
assert_contains "A/wide-header" "very_long"
send "llllllllll"
assert_contains "A/wide-hscroll" "col"
quit

# stdin CSV
tmux send-keys -t "$APP_PANE" "cat tests/fixtures/orders.csv | $BINARY" Enter
sleep 0.6
assert_contains "A/stdin-csv" "order_id"
quit

# stdin JSON
tmux send-keys -t "$APP_PANE" "cat tests/fixtures/orders.json | $BINARY" Enter
sleep 0.6
assert_contains "A/stdin-json" "order_id"
quit

# ── Suite B: Normal mode navigation ───────────────────────────────────────────
echo ""
echo "=== Suite B: Normal mode navigation ==="

start_app "tests/fixtures/orders.csv"
assert_contains "B/start" "order_id"

send "jjjjj"
send "kkk"
send "G" 0.30
assert_contains "B/last-row" "1100"   # order_id 1100 is last row

send "g" 0.25
assert_contains "B/first-row" "1001"  # back to top

pgdn
pgup
send "llll"
assert_contains "B/col-right" "region"
send "hh"
send "_"  0.15
send "="  0.2
assert_contains "B/autofit" "order_id"
quit

# ── Suite C: Search mode ───────────────────────────────────────────────────────
echo ""
echo "=== Suite C: Search mode ==="

start_app "tests/fixtures/orders.csv"
send "/"
assert_contains "C/search-mode" "/_"
send "Alice"
enter 0.25
assert_contains "C/found-alice" "Alice"
send "n" 0.1
send "N" 0.1
# exit search
send "/"
esc
# no-match search
send "/"
send "zzznomatch"
enter 0.2
assert_contains "C/no-crash" "order_id"
esc
quit

# ── Suite D: Filter mode ───────────────────────────────────────────────────────
echo ""
echo "=== Suite D: Filter mode ==="

start_app "tests/fixtures/orders.csv"

# D1: filter region=North (col index 4 — press l 4 times)
send "llll"
send "f"
send "North"
enter 0.25
assert_contains     "D/filter-north"    "North"
assert_not_contains "D/no-south"        "South"

# D2: chain filter quantity > 1 (3 more rights to quantity col)
send "lll"
send "f"
send "> 1"
enter 0.25
assert_contains "D/chained-filter" "North"

# D3: clear all filters
send "F" 0.25
assert_contains "D/clear-filters" "South"

# D4: invalid operator on string col
send "llll"  # region col
send "f"
send "> abc"
enter 0.15
assert_contains "D/filter-error" "requires a number"
esc

# D5: Esc discards filter
send "f"
send "Pending"
esc
assert_contains "D/esc-no-filter" "South"

# D6: Fix 4 — filter column is locked when f is pressed, not at Enter time
# Note: after all prior navigation we're on the status col (string), so "> 0" produces an
# error and Enter stays in Filter mode.  Add esc to cleanly return to Normal before cleanup.
send "lllllll"  # status col (col 11, clamped from wherever we are)
send "f"
send "> 0"
enter 0.25
assert_contains "D/fix4-no-crash" "total_amount"
esc             # exit Filter mode (Enter kept us here because of the type error on string col)
send "F" 0.25   # clear all filters now that we're back in Normal mode

quit

# ── Suite E: Unique values mode ────────────────────────────────────────────────
echo ""
echo "=== Suite E: Unique values mode ==="

start_app "tests/fixtures/orders.csv"
send "llll"   # region col
send "u" 0.3
assert_contains "E/popup-open" "Unique"
send "/"
send "Nor"
sleep 0.15
assert_contains "E/filter-narrow" "North"
enter 0.25
assert_contains     "E/filter-applied" "North"
assert_not_contains "E/no-south"       "South"
send "F" 0.25

# Esc without applying
send "u" 0.3
esc
assert_contains "E/esc-no-filter" "South"
quit

# E6-E7: null fixture
start_app "tests/fixtures/orders_nulls.csv"
send "lll"   # customer_name col
send "u" 0.4
assert_contains "E/null-popup" "Unique"
assert_contains "E/null-shown" "(null)"
esc
quit

# ── Suite F: Sort ──────────────────────────────────────────────────────────────
echo ""
echo "=== Suite F: Sort ==="

start_app "tests/fixtures/orders.csv"
send "lllllllll"   # total_amount col
send "s" 0.25
assert_contains "F/sort-asc" "order_id"
send "s" 0.25
assert_contains "F/sort-desc" "order_id"
send "hhhhhh"       # customer_name col (6 left from total_amount)
send "s" 0.25
assert_contains "F/sort-str" "Alice"
quit

# ── Suite G: Stats popup ───────────────────────────────────────────────────────
echo ""
echo "=== Suite G: Stats popup ==="

start_app "tests/fixtures/orders.csv"
send "lllllllll"   # total_amount col
send "e" 0.25
assert_contains "G/stats-open" "Count"
assert_contains "G/stats-mean" "Mean"
send "e" 0.25
assert_not_contains "G/stats-closed" "Count"

# non-numeric col
send "hhhhhh"
send "e" 0.25
assert_contains "G/stats-na" "N/A"
send "e" 0.25
quit

# null fixture stats
start_app "tests/fixtures/orders_nulls.csv"
send "lllllllll"
send "e" 0.25
assert_contains "G/null-stats" "Count"
send "e" 0.25
quit

# ── Suite H: Column Inspector ──────────────────────────────────────────────────
echo ""
echo "=== Suite H: Column Inspector ==="

start_app "tests/fixtures/orders.csv"
send "i" 0.3
assert_contains "H/inspector-open" "Column"
send "jjj"
send "kkk"
send "g" 0.1
send "G" 0.1
enter 0.25
assert_contains "H/inspector-select" "status"  # G selected last col (status); order_id may scroll off
send "i" 0.3
assert_contains "H/inspector-toggle" "order_id"  # inspector lists all cols; order_id is in the list
send "i" 0.3
esc
assert_contains "H/esc-close" "status"  # still at col 11 after close
quit

# ── Suite I: Group-by ─────────────────────────────────────────────────────────
echo ""
echo "=== Suite I: Group-by ==="

start_app "tests/fixtures/orders.csv"
send "llll"   # region col
send "b" 0.15
assert_contains "I/key-marked" "region"

send "lllll"  # total_amount col
send "a" 0.1
send "a" 0.1
send "a" 0.1
send "B" 0.4
assert_contains "I/groupby-applied" "region"

send "s" 0.25
assert_contains "I/grouped-sort" "region"

send "B" 0.4
assert_contains "I/groupby-cleared" "order_id"

# no key/agg — no crash
send "B" 0.25
assert_contains "I/no-key-no-crash" "order_id"
quit

# ── Suite J: Plot mode ─────────────────────────────────────────────────────────
echo ""
echo "=== Suite J: Plot mode ==="

start_app "tests/fixtures/orders.csv"

# J1: single-Y plot — PlotPickY → PlotPickX → Plot
send "lllllllll"          # total_amount (col 9)
send "p" 0.25
assert_contains "J/picky-mode"   "Toggle Y"   # in PlotPickY
assert_contains "J/picky-presel" "total_amount"   # pre-selected in status bar

enter 0.25                # confirm single Y, move to PlotPickX
assert_contains "J/pickx-prompt" "navigate to X"

send "hhhhhhhhh"          # 9 left → order_id (col 0)
enter 0.4
assert_contains "J/plot-rendered"  "total_amount"
send "t" 0.25
assert_contains "J/plot-bar"       "Bar"
send "t" 0.25
assert_contains "J/plot-hist"      "Histogram"   # single-Y: histogram available
send "t" 0.25
esc
sleep 0.2
assert_contains "J/plot-exit"      "order_id"

# J2: multi-Y plot — two columns, legend, histogram disabled
send "lllllllll"          # total_amount (col 9)
send "p" 0.25             # PlotPickY, total_amount pre-selected
key Left 0.15
key Left 0.15             # navigate to quantity (col 7)
send " " 0.25             # Space: toggle quantity into Y cols
assert_contains "J/picky-two-y"   "quantity"     # both cols now in status bar

enter 0.25                # confirm Y cols, move to PlotPickX
assert_contains "J/pickx-two-y"   "navigate to X"

key Left 0.15
key Left 0.15
key Left 0.15
key Left 0.15
key Left 0.15
key Left 0.15
key Left 0.15             # 7 left → order_id (col 0)
enter 0.4
assert_contains     "J/multi-rendered"  "total_amount"
assert_contains     "J/multi-legend"    "●"          # legend marker
send "t" 0.25
assert_contains     "J/multi-bar"       "Bar"
assert_not_contains "J/multi-no-hist"   "Histogram"  # histogram disabled for multi-Y
send "t" 0.25
assert_contains     "J/multi-line"      "Line"
esc
sleep 0.2
assert_contains "J/multi-exit" "order_id"

# J3: Esc from PlotPickY cancels entirely
send "lllllllll"
send "p" 0.25
esc
sleep 0.15
assert_contains "J/picky-esc" "order_id"

# J4: Esc from PlotPickX goes back to PlotPickY
send "lllllllll"
send "p" 0.25
enter 0.25                # go to PlotPickX
esc
sleep 0.15
assert_contains "J/pickx-esc-back" "Toggle Y"  # back in PlotPickY
esc
sleep 0.15
assert_not_contains "J/picky-esc2"  "Toggle Y"

quit

# ── Suite J2: Dual-panel plot mode ─────────────────────────────────────────────
echo ""
echo "=== Suite J2: Dual-panel plot mode ==="

start_app "tests/fixtures/orders.csv"

# J2-1: assign total_amount to panel 1 (pre-selected by p), quantity to panel 2
send "lllllllll"          # total_amount (col 9)
send "p" 0.25             # PlotPickY: total_amount pre-selected in y_cols
assert_contains "J2/picky-mode" "P1"  # status bar shows P1/P2 labels

key Left 0.15
key Left 0.15             # navigate to quantity (col 7)
send "2" 0.25             # toggle quantity into y2_cols (panel 2)
assert_contains "J2/p2-assigned" "quantity"  # quantity appears in P2 status

# J2-2: guard: pressing 2 again on quantity removes it from y2 (toggle off)
# navigate back to total_amount — pressing 2 on it (the only y1 col) must be a no-op
send "ll"                 # back to total_amount (col 9)
send "2" 0.15             # this would drain y_cols — must be refused
assert_contains "J2/drain-guard" "total_amount"  # total_amount still in P1

# J2-3: re-navigate to quantity, ensure it is still in P2 (guard didn't corrupt state)
key Left 0.15
key Left 0.15             # back to quantity
assert_contains "J2/p2-intact" "quantity"

# J2-4: press i to plot against row index
send "i" 0.40
assert_contains "J2/panel1-title" "total_amount"   # panel 1 chart title
assert_contains "J2/panel2-title" "quantity"       # panel 2 chart title

# J2-5: t cycles Line → Bar only (no Histogram) in dual mode
send "t" 0.25
assert_contains     "J2/dual-bar"     "Bar"
assert_not_contains "J2/dual-no-hist" "Histogram"
send "t" 0.25
assert_contains     "J2/dual-line"    "Line"

esc
sleep 0.15
assert_contains "J2/dual-exit" "order_id"

quit

# ── Suite K: Help popup ────────────────────────────────────────────────────────
echo ""
echo "=== Suite K: Help popup ==="

start_app "tests/fixtures/orders.csv"
send "?" 0.25
assert_contains "K/help-open" "Navigation"
send "jjj"
send "kkk"
pgdn
pgup
send "?" 0.25
assert_not_contains "K/help-closed" "Navigation"
send "?" 0.25
esc
assert_not_contains "K/esc-close" "Navigation"
quit

# ── Suite L: Edge cases ────────────────────────────────────────────────────────
echo ""
echo "=== Suite L: Edge cases ==="

start_app "tests/fixtures/orders.csv"

# L1: filter to 0 rows, then search
send "llll"   # region col
send "f"
send "= zzznomatch"
enter 0.25
send "/"
send "Alice"
enter 0.2
assert_contains "L/zero-search" "order_id"
send "F" 0.25

# L2: filter to 0 rows, then sort
send "f"
send "= zzznomatch"
enter 0.25
send "s" 0.2
assert_contains "L/zero-sort" "order_id"
send "F" 0.25

# L3: filter to 0 rows, then group-by
send "f"
send "= zzznomatch"
enter 0.25
send "b" 0.1
send "lll"
send "a" 0.1
send "B" 0.25
assert_contains "L/zero-groupby" "order_id"
send "B" 0.25
send "F" 0.25

# L4: filter to 0 rows, then plot
send "f"
send "= zzznomatch"
enter 0.25
send "lllllllll"
send "p" 0.25
esc
sleep 0.15
assert_contains "L/zero-plot" "order_id"
send "F" 0.25

quit

# ── Suite M: Row navigation clamping ──────────────────────────────────────────
echo ""
echo "=== Suite M: Row navigation clamping ==="

start_app "tests/fixtures/orders.csv"

# M1: spam Down past end, then Up once — cursor must visually move up
# With the bug (unclamped): G sets internal to usize::MAX or 99, then 50 Down
# accumulates to 149; 1 Up → 148; status bar still shows "Row 100/100".
# With the fix (clamped): Down clamps at last row (99); 1 Up → 98 → "Row 99/100".
key G 0.30
for _ in $(seq 1 50); do key Down 0.03; done
key Up 0.20
assert_contains     "M/down-spam-up" "Row 99/100"
assert_not_contains "M/down-spam-stuck" "Row 100/100"

# M2: same for j/k aliases
key G 0.30
for _ in $(seq 1 50); do send "j" 0.03; done
send "k" 0.20
assert_contains     "M/jk-spam-up" "Row 99/100"
assert_not_contains "M/jk-spam-stuck" "Row 100/100"

quit

# wide.csv edge cases
start_app "tests/fixtures/wide.csv"

# L6: horizontal scroll to far right
for _ in $(seq 1 30); do send "l" 0.03; done
assert_contains "L/wide-hscroll" "col"

# L7: autofit all on wide
send "=" 0.4
assert_contains "L/wide-autofit" "col"

# L8: rapid keystrokes
tmux send-keys -t "$APP_PANE" "jjjjjjjjjjkkkkkkkkkk" ""
sleep 0.4
assert_contains "L/rapid-keys" "col"

quit

# ── Suite N: Column widths (issue #23) ────────────────────────────────────────
echo ""
echo "=== Suite N: Column widths ==="

start_app "$QA_TMP/long_values.csv"

# N1: default widths truncate the long path column
assert_not_contains "N/default-truncates" "Wordpad.lnk"

# N2: '=' autofits every column — the full path is now on screen.
# The old MAX_COLUMN_WIDTH=40 clamp made this impossible at any terminal size.
send "=" 0.4
assert_contains "N/autofit-all-long" "Accessories.Wordpad.lnk"
assert_contains "N/autofit-all-header" "KeyLastWriteTimestamp"

# N3: '_' on the selected column fits it, and pressing again resets it
send "hh" 0.15          # ensure cursor is on the first column
send "_" 0.25           # column 0 is already fitted from N2 → resets to default
assert_not_contains "N/autofit-toggle-reset" "windows powershe|6d2a715ad3bf3395"
send "_" 0.25           # fit it again
assert_contains "N/autofit-toggle-refit" "windows powershe|6d2a715ad3bf3395"

# N4: '-' narrows the current column, '+' widens it back
send "----" 0.30
assert_not_contains "N/shrink" "windows powershe|6d2a715ad3bf3395"
send "++++" 0.30
assert_contains "N/grow" "windows powershe|6d2a715ad3bf3395"

# N5: shrinking past the floor must not wedge the column at zero width.
# MIN_COLUMN_WIDTH is 6, so the 7-char header renders clipped to "KeyNam" —
# present at width 6, absent entirely at width 0.
send "----------------" 0.40
assert_contains     "N/shrink-floor"      "KeyNam"
assert_not_contains "N/shrink-floor-min"  "KeyName"

quit

# N6: a single-row file renders its values, not null glyphs.
# Regression for Column::as_series() returning None on Scalar-backed columns.
start_app "$QA_TMP/single_row.csv"
assert_contains     "N/single-row-value" "solitary"
assert_not_contains "N/single-row-not-null" "∅"
quit

# ── Suite O: Clipboard copy (issue #24) ───────────────────────────────────────
echo ""
echo "=== Suite O: Clipboard copy ==="

# O1: y copies the selected cell — confirmed on screen and in tmux's paste buffer.
start_app "tests/fixtures/orders.csv"
drop_buffers
send "y" 0.25
assert_contains        "O/cell-status"  "Copied cell"
assert_buffer_contains "O/cell-buffer"  "1001"

# O2: the confirmation lasts exactly one keystroke.
send "j" 0.20
assert_not_contains "O/status-transient" "Copied cell"

# O3: Y copies the whole row, tab-separated.
drop_buffers
send "Y" 0.25
assert_contains        "O/row-status" "Copied row"
assert_buffer_contains "O/row-buffer" "Bob Smith"
if tmux show-buffer 2>/dev/null | grep -q "$(printf '\t')"; then
  echo "  PASS [O/row-tab-separated]"
  PASS=$((PASS + 1))
else
  echo "  FAIL [O/row-tab-separated] — no tab in clipboard buffer"
  FAIL=$((FAIL + 1))
  FAILURES+=("[O/row-tab-separated] no tab in clipboard buffer")
fi

# O4: both keys are discoverable in the help popup ("Other" section, two pages down).
send "?" 0.30
pgdn
pgdn
assert_contains "O/help-cell" "Copy cell to clipboard"
assert_contains "O/help-row"  "Copy row to clipboard"
esc
quit

# O5: copying a null cell copies nothing without panicking, and the app stays live.
# Row 2 / column total_amount is empty in the null fixture.
start_app "tests/fixtures/orders_nulls.csv"
send "j" 0.15
send "lllllllll" 0.25
drop_buffers
send "y" 0.25
assert_contains "O/null-cell-status" "Copied cell"
send "k" 0.20
assert_contains "O/null-cell-app-alive" "order_id"
quit

# ── Suite X: browse subcommand ────────────────────────────────────────────────
echo ""
echo "=== Suite X: browse subcommand ==="

# X1: browse opens with fixture directory — browser pane visible
start_app "browse tests/fixtures/"
assert_contains "X1/browser-pane-visible" "orders.csv"
quit

# X2: open a file from the browser — viewer pane appears
start_app "browse tests/fixtures/"
# Press Enter to open whichever file is selected at cursor
enter 0.4
assert_contains "X2/viewer-loads-file" "order_id"
send "q" 0.20

# X3: ctrl-e toggles the browser pane off
# Use "wide.csv" as the sentinel — it lives in the directory listing but is NOT
# the file that opens (orders.csv is first alphabetically), so it only appears
# when the browser pane is actually visible.
start_app "browse tests/fixtures/"
enter 0.4  # open a file first
tmux send-keys -t "$APP_PANE" C-e; sleep 0.30
assert_not_contains "X3/browser-hidden" "wide.csv"
send "q" 0.20

# X4: ctrl-e toggles back on
# Use "wide.csv" — present only in the browser listing, not in viewer title.
start_app "browse tests/fixtures/"
enter 0.4
tmux send-keys -t "$APP_PANE" C-e; sleep 0.30
tmux send-keys -t "$APP_PANE" C-e; sleep 0.30
assert_contains "X4/browser-shown-again" "wide.csv"
send "q" 0.20

# X5: ctrl-h brings focus back to browser (browser visible)
# "wide.csv" is only rendered when the browser pane is on screen.
start_app "browse tests/fixtures/"
enter 0.4  # open file, focus on viewer
tmux send-keys -t "$APP_PANE" C-h; sleep 0.20
send "j" 0.15
assert_contains "X5/browser-focused" "wide.csv"
send "q" 0.20

# X6: browse with no path arg opens current dir
REPO_ROOT=$(git rev-parse --show-toplevel)
tmux send-keys -t "$APP_PANE" "cd $REPO_ROOT/tests/fixtures" Enter; sleep 0.3
start_app "browse"
assert_contains "X6/browse-cwd-default" "orders.csv"
send "q" 0.20
tmux send-keys -t "$APP_PANE" "cd $REPO_ROOT" Enter; sleep 0.2

# ── Suite Y: Theme picker ─────────────────────────────────────────────────────
echo ""
echo "=== Suite Y: Theme picker ==="

# Start clean — remove any persisted state from a prior run.
STATE_FILE="${HOME}/.config/datasight/state.toml"
rm -f "$STATE_FILE"

# Reset cwd to repo root in case earlier suites left it elsewhere.
tmux send-keys -t "$APP_PANE" C-c; sleep 0.10
tmux send-keys -t "$APP_PANE" C-u; sleep 0.05
tmux send-keys -t "$APP_PANE" "cd $REPO_ROOT" Enter; sleep 0.20

# Y1: T opens the picker; the popup lists Base16 themes
start_app "tests/fixtures/orders.csv"
send "T" 0.60
assert_contains "Y1/picker-title" "Theme"
assert_contains "Y1/picker-list"  "nord"

# Y2: Esc cancels the picker — popup gone, no state file written
esc
assert_not_contains "Y2/picker-closed" "nord"
if [ ! -f "$STATE_FILE" ]; then
  echo "  PASS [Y2/no-state-after-cancel]"
  PASS=$((PASS + 1))
else
  echo "  FAIL [Y2/no-state-after-cancel] — state.toml written despite Esc"
  FAIL=$((FAIL + 1))
  FAILURES+=("[Y2/no-state-after-cancel] state.toml exists after cancel")
fi
quit

# Y3: Re-open picker, navigate down once, Enter persists the choice
start_app "tests/fixtures/orders.csv"
send "T" 0.60
send "j" 0.15
enter 0.30
assert_contains "Y3/picker-closed" "order_id"
quit

# Y4: State file exists and contains a theme name
if [ -f "$STATE_FILE" ] && grep -q '^theme *=' "$STATE_FILE"; then
  echo "  PASS [Y4/state-persisted]"
  PASS=$((PASS + 1))
else
  echo "  FAIL [Y4/state-persisted] — expected theme= in $STATE_FILE"
  FAIL=$((FAIL + 1))
  FAILURES+=("[Y4/state-persisted] state file missing or malformed")
fi

# Y5: T also opens the picker in browse mode
start_app "browse tests/fixtures/"
send "T" 0.60
assert_contains "Y5/browse-picker-open" "Theme"
esc
send "q" 0.20

# Reset theme state so the next QA run (and the dev's normal use) starts fresh.
rm -f "$STATE_FILE"

# ── Suite Z: Text viewer ──────────────────────────────────────────────────────
echo ""
echo "=== Suite Z: Text viewer ==="

# Browser listing in tests/fixtures/ (alphabetical):
#   0 orders.csv  1 orders.json  2 orders.ndjson  3 orders.parquet
#   4 orders.tsv  5 orders_nulls.csv  6 sample.json  7 sample.txt
#   8 sample_binary.png  9 wide.csv

# Z1: open sample.txt — text viewer renders content with UTF-8 title marker
start_app "browse tests/fixtures/"
send "jjjjjjj" 0.30   # cursor → sample.txt
enter 0.40
assert_contains "Z1/text-content"   "Datasight text viewer fixture"
assert_contains "Z1/title-utf8"     "UTF-8"
assert_contains "Z1/title-lines"    "lines"
quit

# Z2: j scrolls down, k scrolls back up — content remains rendered
start_app "browse tests/fixtures/"
send "jjjjjjj" 0.30
enter 0.40
send "jjj" 0.20
send "kkk" 0.20
assert_contains "Z2/scroll-roundtrip" "Datasight text viewer fixture"
quit

# Z3: G then gg returns to top of file (covered by unit tests; this is a
# smoke check that the keys don't crash the viewer)
start_app "browse tests/fixtures/"
send "jjjjjjj" 0.30
enter 0.40
send "G" 0.30
send "gg" 0.30
assert_contains "Z3/back-to-top" "Datasight text viewer fixture"
quit

# Z4: / opens search; typing a query shows match count; Enter jumps; n cycles
start_app "browse tests/fixtures/"
send "jjjjjjj" 0.30
enter 0.40
send "/" 0.20
send "needle" 0.30
# Still in Search mode — bottom status row reports match count.
assert_contains "Z4/match-count" "match"
key Enter 0.30
# Confirm scrolled to first occurrence of the token.
assert_contains "Z4/jumped-to-needle" "needle"
send "n" 0.20
assert_contains "Z4/cycle-still-on-match" "needle"
quit

# Z5: L toggles line numbers (rendering still succeeds, content still visible)
start_app "browse tests/fixtures/"
send "jjjjjjj" 0.30
enter 0.40
send "L" 0.20
assert_contains "Z5/text-still-visible" "Datasight text viewer"
send "L" 0.20
assert_contains "Z5/text-after-retoggle" "Datasight text viewer"
quit

# Z6: w toggles word wrap (rendering survives the toggle)
start_app "browse tests/fixtures/"
send "jjjjjjj" 0.30
enter 0.40
send "w" 0.20
assert_contains "Z6/wrap-off-content" "Datasight text viewer"
send "w" 0.20
assert_contains "Z6/wrap-on-content"  "Datasight text viewer"
quit

# Z7: non-tabular .json falls through to text viewer with pretty-print
start_app "browse tests/fixtures/"
send "jjjjjj" 0.30   # cursor → sample.json
enter 0.50
# Pretty-printed JSON spans multiple lines; the title flags it as such.
assert_contains "Z7/json-pretty-title"   "pretty JSON"
assert_contains "Z7/json-pretty-content" "version"
quit

# Z8: opening a binary file shows a status message and does not load a viewer
start_app "browse tests/fixtures/"
send "jjjjjjjj" 0.30  # cursor → sample_binary.png
enter 0.40
assert_contains "Z8/binary-status" "Cannot preview"
# Browser still has focus and no viewer loaded — q quits cleanly.
send "q" 0.20

# ── Summary ────────────────────────────────────────────────────────────────────
echo ""
echo "════════════════════════════════════════"
echo "  QA Results: $PASS passed, $FAIL failed"
echo "════════════════════════════════════════"

if [ "${#FAILURES[@]}" -gt 0 ]; then
  echo ""
  echo "Failures:"
  for f in "${FAILURES[@]}"; do
    echo "  • $f"
  done
  echo ""
  exit 1
fi

echo ""
echo "All checks passed. Safe to release."
exit 0
