#!/usr/bin/env bash
# ── pup CHARACTERIZATION SUITE (restructure step 0) ────────────────────────────────────────────
#
# Locks the CURRENT observable behaviour of every verb BEFORE the restructure moves any code.
# This is not a correctness suite — it does not assert what pup *should* do. It records what pup
# DOES do, so that when the 3,601-line handler is split into verbs/ modules, any behavioural drift
# shows up as a diff instead of as a user-reported bug three days later.
#
# Usage:
#   tests/characterize.sh baseline     # record current behaviour  -> tests/baseline.json
#   tests/characterize.sh check        # re-run and diff against the baseline
#
# Rules this suite obeys (from dev-skills/pup-bridge-test):
#   - up-gate first: never test a bridge that is still reconstructing
#   - never >/dev/null an open: capture ok + error + errorCode
#   - clean up every window it creates
#   - use a throwaway thread name so no real thread's window is touched
set -u
TARGET="${ADOM_TARGET:-AdomLapper}"
THREAD="pup-characterize"
MODE="${1:-check}"
DIR="$(cd "$(dirname "$0")" && pwd)"
OUT="$DIR/baseline.json"
TMP="$(mktemp -d)"
trap 'rm -rf "$TMP"' EXIT

AD() { timeout 75 adom-desktop --target "$TARGET" --ai-thread "$THREAD" "$@" 2>/dev/null; }

# Record only the SHAPE + key invariants of a response, not volatile values (timings, ids, paths).
# A restructure must preserve shape and invariants; it is allowed to change a duration.
shape() {
  python3 -c "
import sys, json
raw = sys.stdin.read()
try:
    d = json.loads(raw)
except Exception:
    print(json.dumps({'_unparseable': raw[:200]})); raise SystemExit
o = d.get('output', d)
if isinstance(o, str):
    try: o = json.loads(o)
    except Exception: pass
if not isinstance(o, dict): o = {'_scalar': type(o).__name__}
# Volatile = values that legitimately change run to run, PLUS advisory fields that pup emits
# INTERMITTENTLY by design (issue-report nudges, cleanup reports, occasional hints). Those come and
# go on purpose, so comparing them produces false drift — and a suite that cries wolf gets ignored,
# which is worse than no suite. Presence is normalised, not just the value.
VOLATILE = {'durationMs','ts','timestamp','createdAt','updatedAt','ageMinutes','pid','cdpPort',
            'hwnd','shotId','path','localSafePath','fullPath','tabId','sessionId','url','title',
            'progressBarMs','progressBarCount','flashCount','bounds','rect','_hint','_next',
            '_timeoutHint','_reportIssues','_cleanupReport','_degraded','_urlLint','lastAgentUpdate',
            # intermittent-by-design advisory/status fields:
            'active','activeSession','activeTabId','hint','_verifyRender','_autoLogin','_flashCount',
            'errorCount','tabCount','overlay','ownerSource','owner','background','flash','emulatedViewport',
            'lastForeground','restoredTabsClosed','_appOverlay','degraded','degradedReason','profile',
            'windows','sessions','tabs','count','_screenshots','_related','_cacheHint','_skillsHint',
            '_staleWarning','staleBridges','staleCheckedAt','summary','bridges','current_os'}
def keyshape(x, depth=0):
    if depth > 3: return '…'
    if isinstance(x, dict):
        return {k: ('<volatile>' if k in VOLATILE else keyshape(v, depth+1))
                for k, v in sorted(x.items())}
    if isinstance(x, list):
        return [keyshape(x[0], depth+1)] if x else []
    if isinstance(x, bool): return 'bool'
    if isinstance(x, (int, float)): return 'num'
    if x is None: return 'null'
    return 'str'
print(json.dumps({'ok': d.get('ok', o.get('ok')), 'errorCode': d.get('errorCode') or o.get('errorCode'),
                  'shape': keyshape(o)}, sort_keys=True))
"
}

record() {  # record <name> <<< response
  local name="$1"
  python3 -c "
import sys, json
name = sys.argv[1]
body = sys.stdin.read().strip() or '{}'
print(json.dumps({'verb': name, 'result': json.loads(body)}))
" "$name"
}

echo "── pup characterization ($MODE) ──"

# ── up-gate ────────────────────────────────────────────────────────────────────────────────────
for i in $(seq 1 15); do
  AD browser_list_windows | python3 -c "
import sys, json
try: sys.exit(0 if json.load(sys.stdin).get('count') is not None else 1)
except Exception: sys.exit(1)" && break
  sleep 6
done

VER=$(AD bridge_list | python3 -c "
import sys, json
try:
    print(next((b.get('version') for b in json.load(sys.stdin).get('bridges', []) if b.get('name') == 'puppeteer'), '?'))
except Exception: print('?')")
echo "  bridge: $VER"

: > "$TMP/results.jsonl"
SID="char-$$"

# ── read-only verbs (safe, no side effects) ────────────────────────────────────────────────────
for v in browser_readiness browser_status browser_list_windows browser_describe browser_deps; do
  AD "$v" '{}' | shape | record "$v" >> "$TMP/results.jsonl"
done

# ── lifecycle: open → drive → close ────────────────────────────────────────────────────────────
AD browser_open_window "{\"sessionId\":\"$SID\",\"url\":\"https://example.com\",\"reason\":\"characterization suite\"}" \
  | shape | record browser_open_window >> "$TMP/results.jsonl"
sleep 4

AD browser_eval        "{\"sessionId\":\"$SID\",\"expr\":\"1+1\"}"                        | shape | record browser_eval        >> "$TMP/results.jsonl"
AD browser_navigate    "{\"sessionId\":\"$SID\",\"url\":\"https://example.org\"}"          | shape | record browser_navigate    >> "$TMP/results.jsonl"
AD browser_open_tab    "{\"sessionId\":\"$SID\",\"url\":\"https://www.iana.org\"}"         | shape | record browser_open_tab    >> "$TMP/results.jsonl"
AD browser_list_tabs   "{\"sessionId\":\"$SID\"}"                                          | shape | record browser_list_tabs   >> "$TMP/results.jsonl"
AD browser_scroll      "{\"sessionId\":\"$SID\",\"y\":200}"                                | shape | record browser_scroll      >> "$TMP/results.jsonl"
AD browser_set_viewport "{\"sessionId\":\"$SID\",\"width\":900,\"height\":600}"            | shape | record browser_set_viewport >> "$TMP/results.jsonl"
AD browser_set_viewport "{\"sessionId\":\"$SID\",\"reset\":true}"                          | shape | record browser_set_viewport_reset >> "$TMP/results.jsonl"
AD browser_screenshot  "{\"sessionId\":\"$SID\"}"                                          | shape | record browser_screenshot  >> "$TMP/results.jsonl"
AD browser_errors      "{\"sessionId\":\"$SID\"}"                                          | shape | record browser_errors      >> "$TMP/results.jsonl"
AD browser_maximize    "{\"sessionId\":\"$SID\"}"                                          | shape | record browser_maximize    >> "$TMP/results.jsonl"

# ── refusals: these MUST keep refusing, with the same errorCode ────────────────────────────────
AD browser_open_window '{"url":"https://example.com"}'                                     | shape | record refusal_no_identity  >> "$TMP/results.jsonl"
AD browser_eval        '{"sessionId":"definitely-not-a-real-session","expr":"1"}'           | shape | record refusal_no_session   >> "$TMP/results.jsonl"
AD browser_open_window '{"sessionId":"lint-check","url":"file:///home/adom/x.html"}'        | shape | record refusal_container_path >> "$TMP/results.jsonl"
AD browser_raise_os_window "{\"sessionId\":\"$SID\"}"                                       | shape | record refusal_bare_raise   >> "$TMP/results.jsonl"

# ── cleanup ────────────────────────────────────────────────────────────────────────────────────
AD browser_close_window "{\"sessionId\":\"$SID\",\"reason\":\"characterization done\"}" \
  | shape | record browser_close_window >> "$TMP/results.jsonl"

python3 -c "
import json, sys
rows = [json.loads(l) for l in open('$TMP/results.jsonl') if l.strip()]
print(json.dumps({'bridge': '$VER', 'cases': rows}, indent=2, sort_keys=True))
" > "$TMP/current.json"

if [ "$MODE" = "baseline" ]; then
  cp "$TMP/current.json" "$OUT"
  echo "  baseline written: $OUT ($(python3 -c "
import json; print(len(json.load(open('$OUT'))['cases']))") cases)"
  exit 0
fi

if [ ! -f "$OUT" ]; then
  echo "  NO BASELINE. Run: tests/characterize.sh baseline"
  exit 2
fi

python3 -c "
import json
base = {c['verb']: c['result'] for c in json.load(open('$OUT'))['cases']}
cur  = {c['verb']: c['result'] for c in json.load(open('$TMP/current.json'))['cases']}
drift = []
for verb in sorted(set(base) | set(cur)):
    b, c = base.get(verb), cur.get(verb)
    if b != c:
        drift.append((verb, b, c))
print(f'  cases: {len(cur)} | drift: {len(drift)}')
for verb, b, c in drift:
    print(f'   ✗ {verb}')
    print(f'      baseline: {json.dumps(b)[:150]}')
    print(f'      current : {json.dumps(c)[:150]}')
raise SystemExit(1 if drift else 0)
"
