#!/bin/bash
# Hard rule 6's live battery for the write operations: every one through the real key or menu path,
# asserted on the filesystem and on the app's own IPC, never on an eyeball.
set -u
# Hard rule 9's guard, which owns FIXTURE_ROOT and every create and delete below.
. "$(dirname "$0")/../tools/flea-sandbox-guard"
SB="$FIXTURE_ROOT/flea-live-ops"
TREE=${FLEA_TREE:-$HOME/Work/claude/flea}
export PATH="$HOME/.local/bin:$PATH"
eval "$(omarchy-drive env)" 2>/dev/null
fail=0

# A path is not an instance selector, so this resolves the pid once and addresses it by number.
. "$TREE/tools/flea-ipc-pid"
FLEA_PID=""
q() { qs ipc --pid "$FLEA_PID" call flea "$@" 2>/dev/null; }

# Pressing Return on a row hands the file to the default handler, and that handler outlives this
# script: three xdg-open and nvim pairs were still resident seventeen hours later, holding the load
# average up high enough to refuse a field run. A test that leaves a process behind is a test that
# changes the box it measures.
#
# Matched on the whole command line rather than by a pattern, and every process this script descends
# from is excluded first, so it can never reap its own caller.
reap_handlers() {
  local pid rest p ancestors=" $$ "
  p=$$
  while [ "${p:-1}" -gt 1 ]; do
    p=$(grep '^PPid:' "/proc/$p/status" 2>/dev/null | tr -d ' \t' | cut -d: -f2)
    [ -n "$p" ] || break
    ancestors="$ancestors$p "
  done
  while read -r pid rest; do
    case "$ancestors" in *" $pid "*) continue ;; esac
    case "$rest" in
      *" $SB/"*) kill "$pid" 2>/dev/null ;;
    esac
  done < <(ps -eo pid=,args=)
}

check() {
  local label="$1" expected="$2" actual="$3"
  if [ "$expected" != "$actual" ]; then
    echo "FAIL $label"
    echo "  expected: $expected"
    echo "  actual:   $actual"
    fail=1
  else
    echo "ok   $label"
  fi
}

press() { omarchy-drive key --window flea -k "$1" >/dev/null 2>&1; sleep 0.5; }

# rowCentre answers in window coordinates, and a Wayland client is never told where its surface is,
# so the window origin is added here exactly as tests/ui.sh's own click_row does.
window_box() {
  omarchy-drive windows --json | jq -r '.windows[] | select(.title == "Flea") | "\(.at[0]) \(.at[1])"'
}

click_row() {
  local index="$1" button="$2" cx cy wx wy
  read -r cx cy <<< "$(q rowCentre "$index")"
  read -r wx wy <<< "$(window_box)"
  omarchy-drive click "$((cx + wx))" "$((cy + wy))" "$button" >/dev/null 2>&1
}
typek() { omarchy-drive key --window flea "$1" >/dev/null 2>&1; sleep 0.4; }
listing() { ls "$SB" | tr '\n' ' '; }

# The cursor goes to the top first and then walks down, because a row the sort puts above the cursor
# is otherwise unreachable and the check that follows would pass vacuously against an untouched file.
seek_row() {
  local want="$1" i
  press g
  for i in $(seq 1 30); do
    case "$(q rowAt "$(q cursor)")" in "$want|"*) return 0 ;; esac
    press j
  done
  echo "  could not reach row: $want"
  return 1
}

setup() {
  sandbox_make "$SB"
  mkdir -p "$SB/dest" "$SB/subdir" || exit 1
  printf 'alpha body' > "$SB/alpha.txt"
  printf 'beta body'  > "$SB/beta.txt"
  printf 'gamma body' > "$SB/gamma.txt"
  printf 'inside'     > "$SB/subdir/inner.txt"
}

# The launcher runs target/release/flea, so a debug-only build would drive a stale binary and
# every rows-form request would silently resolve to nothing. Build it here rather than trust it.
( cd "$TREE" && cargo build --release 2>&1 | grep -E "^(error|warning)" ) && { echo "release build is not clean"; exit 1; }
echo "release binary: $(cd "$TREE" && git rev-parse --short HEAD) tree, built $(date -r "$TREE/target/release/flea" +%H:%M:%S)"

setup
# Only this tree's own instances. pkill -x qs matched every Quickshell on the box: other lanes'
# windows, the operator's Flea, and Omarchy's own shell.
for _pid in $(pgrep -x qs 2>/dev/null); do
    [ -r "/proc/$_pid/cmdline" ] || continue
    if tr '\0' ' ' < "/proc/$_pid/cmdline" | grep -Fq "$TREE/ui"; then
        kill "$_pid" 2>/dev/null
    fi
done
sleep 1
"$HOME/tmp/flea-launch.sh" "$TREE" "$SB" >/dev/null 2>&1 </dev/null &
for i in $(seq 1 20); do
  FLEA_PID=$(flea_ipc_pid "$TREE/ui" 2>/dev/null)
  [ -n "$FLEA_PID" ] && [ "$(q ready)" = "true" ] && break
  sleep 0.5
done
omarchy-drive focus flea >/dev/null 2>&1; sleep 0.5
check "the app came up on the fixture" "$SB" "$(q path)"

echo "--- rename through r, and z puts it back ---"
seek_row "alpha.txt" || fail=1
press r
typek "renamed"
press Return
sleep 0.8
check "the new name is on disk" "yes" "$([ -f "$SB/renamed.txt" ] && echo yes || echo no)"
check "the old name is gone" "no" "$([ -e "$SB/alpha.txt" ] && echo yes || echo no)"
check "the bytes survived the rename" "alpha body" "$(cat "$SB/renamed.txt")"
check "committing the rename did not also open the row" "" "$(q lastMessage)"
check "the cursor followed the renamed row" "renamed.txt" "$(q rowAt "$(q cursor)" | cut -d'|' -f1)"
press z
sleep 0.8
check "z undid the rename" "yes" "$([ -f "$SB/alpha.txt" ] && echo yes || echo no)"
check "and took the new name away" "no" "$([ -e "$SB/renamed.txt" ] && echo yes || echo no)"
check "the bar said what it reversed" "Undid the rename." "$(q lastMessage)"

echo "--- trash through Delete, with no confirmation, and z restores ---"
seek_row "gamma.txt" || fail=1
press Delete
sleep 1.2
check "the row left the directory" "no" "$([ -e "$SB/gamma.txt" ] && echo yes || echo no)"
check "the bar named the reversal" "Moved 1 item to Trash · z undoes" "$(q lastMessage)"
press z
sleep 1.5
check "z restored it from the trash" "gamma body" "$(cat "$SB/gamma.txt" 2>/dev/null)"
check "the bar said so" "Put it back from Trash." "$(q lastMessage)"

echo "--- cut with x, paste with p in another directory, and z moves it back ---"
seek_row "beta.txt" || fail=1
press x
sleep 0.8
check "the clipboard reports what it took" "Cut 1 item, p pastes." "$(q lastMessage)"
seek_row "dest" || fail=1
press Return
sleep 1.0
check "the pane is in the destination" "$SB/dest" "$(q path)"
press p
sleep 1.5
check "the file landed in the destination" "beta body" "$(cat "$SB/dest/beta.txt" 2>/dev/null)"
check "and left its old directory" "no" "$([ -e "$SB/beta.txt" ] && echo yes || echo no)"
# The verb comes off the wire now. Reading it from the clipboard reported every move as a copy,
# because paste spends a cut before transferstarted arrives.
check "the bar called it a move, not a copy" "Moved 1 item · z undoes" "$(q lastMessage)"
check "and the sticky slot is clear once the operation ends" "" "$(q stickyMessage)"
press z
sleep 1.2
check "z moved it back where it came from" "beta body" "$(cat "$SB/beta.txt" 2>/dev/null)"
check "and took it out of the destination" "no" "$([ -e "$SB/dest/beta.txt" ] && echo yes || echo no)"

echo "--- copy with y, paste with p, and the same slot says copy ---"
# The cut case above left the pane inside dest, so this one climbs out before it looks for a row.
press BackSpace
sleep 1.2
check "the copy case starts in the parent directory" "$SB" "$(q path)"
seek_row "gamma.txt" || fail=1
press y
sleep 0.8
check "the clipboard reports a copy" "Copied 1 item, p pastes." "$(q lastMessage)"
seek_row "dest" || fail=1
press Return
sleep 1.0
press p
sleep 1.5
check "the copy landed in the destination" "gamma body" "$(cat "$SB/dest/gamma.txt" 2>/dev/null)"
check "and the source is still there" "gamma body" "$(cat "$SB/gamma.txt" 2>/dev/null)"
check "the bar called it a copy" "Copied 1 item · z undoes" "$(q lastMessage)"
press z
sleep 1.2
check "z removed only the copy" "no" "$([ -e "$SB/dest/gamma.txt" ] && echo yes || echo no)"

echo "--- duplicate through the context menu, and z removes only the copy ---"
press BackSpace
sleep 1.2
check "backspace returned to the parent directory" "$SB" "$(q path)"
seek_row "beta.txt" || fail=1
check "the cursor is on the row the menu will open on" "beta.txt" "$(q rowAt "$(q cursor)" | cut -d'|' -f1)"
# Right click the row, then pick the entry: this is the only path in the battery that goes through
# the menu rather than a key, because Duplicate is deliberately not bound to one.
omarchy-drive focus flea >/dev/null 2>&1
sleep 0.5
# The app's own rowCentre is used rather than OCR: click-text reads the screen and could not find a
# row this test had already put the cursor on, which is a harness limit and not an app fact.
click_row "$(q cursor)" right
sleep 1.0
check "the menu opened on the row" "true" "$(q contextMenuVisible)"
# The canvas's own order and grouping, on a text row outside Dropbox: Extract and Convert are absent
# because this row is neither an archive nor an image, which is the gating the design asks for.
check "the menu draws the canvas's own rows and grouping" \
      "Open|-|Rename|Duplicate|-|Compress|-|Send with Taildrop|Move to Dropbox|-|Move to Trash|Show hidden files" \
      "$(q contextMenuEntries)"
# The two brand rows name their reproduction rather than a cut glyph, which is what the reader
# reports for them; every other row is a cut glyph name.
check "every action row carries a mark, and only the separators do not" \
      "folder-open|-|rename|file-plus|-|archive|-|tailscale|dropbox|-|trash|eye" \
      "$(q contextMenuGlyphs)"
check "the menu opens on its first real row" "0" "$(q contextMenuCursor)"
press Down
check "down steps over the separator to Rename" "2" "$(q contextMenuCursor)"
press Down
check "and again to Duplicate" "3" "$(q contextMenuCursor)"
press Return
sleep 1.5
check "the menu closed when the entry was chosen" "false" "$(q contextMenuVisible)"
check "the bar named the copy and its reversal" "Duplicated to beta copy.txt · z undoes" "$(q lastMessage)"
check "the copy is on disk under the design's own name" "beta body" "$(cat "$SB/beta copy.txt" 2>/dev/null)"
press z
sleep 1.0
check "z removed the copy" "no" "$([ -e "$SB/beta copy.txt" ] && echo yes || echo no)"
check "and left the original" "beta body" "$(cat "$SB/beta.txt" 2>/dev/null)"

echo "--- undo with an empty journal is a message, not a crash ---"
for i in $(seq 1 6); do press z; done
sleep 0.6
check "the app is still answering" "true" "$(q ready)"
check "and says there is nothing left" "There is nothing to undo." "$(q lastMessage)"

# Before the last assertion, so a handler that is still holding the file cannot affect it, and
# before the exit, so a failing run cleans up as thoroughly as a passing one.
reap_handlers

echo "--- the directory is back to where it started ---"
check "every fixture file is present and nothing extra" "alpha.txt beta.txt dest gamma.txt subdir " "$(listing)"

# Teardown kills only what this run launched, for the same reason as the setup above.
for _pid in $(pgrep -x qs 2>/dev/null); do
    [ -r "/proc/$_pid/cmdline" ] || continue
    if tr "\0" " " < "/proc/$_pid/cmdline" | grep -Fq "$TREE/ui"; then
        kill "$_pid" 2>/dev/null
    fi
done
echo
if [ "$fail" = 0 ]; then echo "flea-live-ops: all checks passed"; else echo "flea-live-ops: FAILURES above"; fi
exit "$fail"
