#!/usr/bin/env bash
# e2e_preview_pages.sh — End-to-end parity runner for preview, watch, and pages.
#
# Exercises the full lifecycle:
#   1. Export a pages bundle then preview with status check
#   2. Watch-export with file modification and debounce
#   3. Wizard non-TTY help output
#   4. Failure path: preview with missing bundle
#   5. Failure path: watch-export without --export-pages
#   6. Bundle artifact completeness and JSON validity
#   7. Preview live-reload endpoint stability
#   8. Export with include-closed filtering
#
# Usage:
#   scripts/e2e_preview_pages.sh [path-to-bvr-binary]
#
# Env vars:
#   BVR_BIN          — path to bvr binary (default: target/debug/bvr or $1)
#   E2E_KEEP_TMPDIR  — if set, preserve temp dir on success
#   E2E_VERBOSE      — if set, show full command output
#
# Exit codes:
#   0 = all scenarios passed
#   1 = one or more scenarios failed (logs preserved)

set -euo pipefail

# ── Setup ────────────────────────────────────────────────────────────

SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"

BVR="${BVR_BIN:-${1:-$PROJECT_ROOT/target/debug/bvr}}"
FIXTURE="$PROJECT_ROOT/tests/testdata/minimal.jsonl"
TMPDIR_ROOT=$(mktemp -d "${TMPDIR:-/tmp}/bvr_e2e_preview_XXXXXX")

PASS=0
FAIL=0
SCENARIOS=()

log()    { echo "  [e2e] $*"; }
pass()   { log "PASS: $1"; PASS=$((PASS + 1)); SCENARIOS+=("PASS: $1"); }
fail()   { log "FAIL: $1 — $2"; FAIL=$((FAIL + 1)); SCENARIOS+=("FAIL: $1 — $2"); }
banner() { echo ""; echo "═══ $1 ═══"; }

extract_preview_port() {
    local log_file="$1"
    local port=""
    port=$(grep -oP '(?:localhost|127\.0\.0\.1):\K[0-9]+' "$log_file" 2>/dev/null | head -1 || true)
    if [ -z "$port" ]; then
        port=$(grep -oP ':\K[0-9]+' "$log_file" 2>/dev/null | head -1 || true)
    fi
    printf '%s' "$port"
}

wait_for_process_exit() {
    local pid="$1"
    local timeout_secs="$2"
    local elapsed=0
    while kill -0 "$pid" 2>/dev/null; do
        if [ "$elapsed" -ge "$timeout_secs" ]; then
            return 1
        fi
        sleep 1
        elapsed=$((elapsed + 1))
    done
    return 0
}

cleanup() {
    if [ "$FAIL" -gt 0 ] || [ "${E2E_KEEP_TMPDIR:-}" != "" ]; then
        log "Workspace preserved: $TMPDIR_ROOT"
    else
        rm -rf "$TMPDIR_ROOT"
    fi
}
trap cleanup EXIT

# Verify binary exists
if [ ! -x "$BVR" ]; then
    echo "error: bvr binary not found at $BVR"
    echo "Build it first: cargo build"
    exit 1
fi

if [ ! -f "$FIXTURE" ]; then
    echo "error: fixture not found at $FIXTURE"
    exit 1
fi

log "Binary:   $BVR"
log "Fixture:  $FIXTURE"
log "Workspace: $TMPDIR_ROOT"

# ── Scenario 1: Export + Preview ─────────────────────────────────────

banner "Scenario 1: Export bundle then preview with status check"
S1_DIR="$TMPDIR_ROOT/s1_export_preview"
S1_BUNDLE="$S1_DIR/pages"
mkdir -p "$S1_DIR"

# Export
if "$BVR" --export-pages "$S1_BUNDLE" --beads-file "$FIXTURE" \
    > "$S1_DIR/export_stdout.log" 2> "$S1_DIR/export_stderr.log"; then
    if [ -f "$S1_BUNDLE/index.html" ]; then
        log "Export produced index.html"
    else
        fail "s1-export" "missing index.html"
    fi
else
    fail "s1-export" "export command failed (exit $?)"
fi

# Preview with max-requests=2 (status + index)
if [ -d "$S1_BUNDLE" ]; then
    BVR_NO_BROWSER=1 BVR_PREVIEW_MAX_REQUESTS=2 "$BVR" --preview-pages "$S1_BUNDLE" --no-live-reload \
        > "$S1_DIR/preview_stdout.log" 2> "$S1_DIR/preview_stderr.log" &
    PREVIEW_PID=$!

    # Wait for server to start
    sleep 1

    PORT=$(extract_preview_port "$S1_DIR/preview_stdout.log")

    if [ -n "$PORT" ]; then
        log "Preview server on port $PORT"

        # Check status endpoint
        STATUS_RESPONSE=$(curl -sf "http://localhost:$PORT/__preview__/status" 2>/dev/null || echo "")
        if printf '%s' "$STATUS_RESPONSE" | python3 -c '
import json
import pathlib
import sys

payload = json.load(sys.stdin)
expected = pathlib.Path(sys.argv[1]).resolve()
bundle_path = pathlib.Path(payload["bundle_path"]).resolve()
assert payload["status"] == "running"
assert payload["has_index"] is True
assert bundle_path == expected
print(json.dumps(payload, indent=2))
' "$S1_BUNDLE" > "$S1_DIR/status_response.json" 2>/dev/null
        then
            log "Status endpoint returned valid JSON"
        else
            fail "s1-preview-status" "status endpoint payload invalid"
        fi

        # Fetch index page
        if curl -sf "http://localhost:$PORT/" > "$S1_DIR/index_response.html" 2>/dev/null \
            && grep -qi "html" "$S1_DIR/index_response.html"; then
            log "Index page served HTML content"
        else
            fail "s1-preview-index" "index response did not look like HTML"
        fi
    else
        fail "s1-preview-port" "could not determine preview port"
    fi

    # Wait for server to exit (max-requests should stop it)
    wait "$PREVIEW_PID" 2>/dev/null || true
    if ! printf '%s\n' "${SCENARIOS[@]}" | grep -q '^FAIL: s1-preview'; then
        pass "s1-export-preview"
    fi
else
    fail "s1-preview" "no bundle directory to preview"
fi

# ── Scenario 2: Watch-export with file change ────────────────────────

banner "Scenario 2: Watch-export detects file change and rebuilds"
S2_DIR="$TMPDIR_ROOT/s2_watch"
S2_BUNDLE="$S2_DIR/pages"
S2_BEADS="$S2_DIR/issues.jsonl"
mkdir -p "$S2_DIR"
cp "$FIXTURE" "$S2_BEADS"

# Modify file after a short delay
(
    sleep 1
    echo '{"id":"e2e-injected","title":"E2E Test Issue","status":"open","priority":1,"created_at":"2026-01-01T00:00:00Z"}' >> "$S2_BEADS"
) &
MODIFIER_PID=$!

BVR_WATCH_MAX_LOOPS=5 BVR_WATCH_INTERVAL_MS=200 BVR_WATCH_DEBOUNCE_MS=100 \
    "$BVR" --export-pages "$S2_BUNDLE" --watch-export \
    --beads-file "$S2_BEADS" \
    > "$S2_DIR/watch_stdout.log" 2> "$S2_DIR/watch_stderr.log" &
WATCH_PID=$!

wait "$MODIFIER_PID" 2>/dev/null || true

# Let the watch run until it exits naturally or time out and stop it ourselves.
if ! wait_for_process_exit "$WATCH_PID" 15; then
    log "Watch process did not exit within 15s; terminating"
fi

# Kill watch if still running
kill "$WATCH_PID" 2>/dev/null || true
wait "$WATCH_PID" 2>/dev/null || true

WATCH_STDERR=$(cat "$S2_DIR/watch_stderr.log" 2>/dev/null)

if echo "$WATCH_STDERR" | grep -q "Exported pages bundle"; then
    log "Initial export succeeded"
    if echo "$WATCH_STDERR" | grep -q "watch: regenerated\|Watching.*source file"; then
        pass "s2-watch-rebuild"
    else
        fail "s2-watch-rebuild" "watch log never showed a regeneration after the file change"
    fi
else
    fail "s2-watch" "initial export not found in stderr"
fi

# ── Scenario 3: Pages wizard non-TTY ────────────────────────────────

banner "Scenario 3: Pages wizard prints help in non-TTY mode"
S3_DIR="$TMPDIR_ROOT/s3_wizard"
mkdir -p "$S3_DIR"

"$BVR" --pages --beads-file "$FIXTURE" \
    > "$S3_DIR/wizard_stdout.log" 2> "$S3_DIR/wizard_stderr.log" || true

WIZARD_OUT=$(cat "$S3_DIR/wizard_stdout.log" 2>/dev/null)
if echo "$WIZARD_OUT" | grep -q "Deploy targets\|bvr --export-pages"; then
    pass "s3-wizard-help"
else
    fail "s3-wizard-help" "expected wizard help output"
fi

# ── Scenario 4: Preview with missing bundle (failure path) ───────────

banner "Scenario 4: Preview with missing bundle fails cleanly"
S4_DIR="$TMPDIR_ROOT/s4_missing"
mkdir -p "$S4_DIR"

"$BVR" --preview-pages "/nonexistent/bundle/path" \
    > "$S4_DIR/stdout.log" 2> "$S4_DIR/stderr.log" && S4_EXIT=0 || S4_EXIT=$?

if [ "$S4_EXIT" -ne 0 ]; then
    PREVIEW_ERR=$(cat "$S4_DIR/stderr.log" 2>/dev/null)
    if echo "$PREVIEW_ERR" | grep -qi "not found\|error\|does not exist"; then
        pass "s4-missing-bundle"
    else
        fail "s4-missing-bundle" "error message not descriptive: $PREVIEW_ERR"
    fi
else
    fail "s4-missing-bundle" "expected non-zero exit for missing bundle"
fi

# ── Scenario 5: Watch-export without --export-pages (error path) ─────

banner "Scenario 5: Watch-export without --export-pages fails"
S5_DIR="$TMPDIR_ROOT/s5_watch_noexport"
mkdir -p "$S5_DIR"

"$BVR" --watch-export --beads-file "$FIXTURE" \
    > "$S5_DIR/stdout.log" 2> "$S5_DIR/stderr.log" && S5_EXIT=0 || S5_EXIT=$?

if [ "$S5_EXIT" -eq 2 ]; then
    pass "s5-watch-requires-export"
else
    fail "s5-watch-requires-export" "expected exit 2, got $S5_EXIT"
fi

# ── Scenario 6: Export bundle artifact completeness ───────────────────

banner "Scenario 6: Export bundle contains all expected artifacts"
S6_DIR="$TMPDIR_ROOT/s6_artifacts"
S6_BUNDLE="$S6_DIR/pages"
mkdir -p "$S6_DIR"

"$BVR" --export-pages "$S6_BUNDLE" --beads-file "$FIXTURE" \
    > "$S6_DIR/export_stdout.log" 2> "$S6_DIR/export_stderr.log" || true

S6_MISSING=""
for artifact in index.html styles.css viewer.js beads.sqlite3 \
    beads.sqlite3.config.json _headers coi-serviceworker.js \
    data/issues.json data/meta.json data/triage.json data/insights.json \
    data/export_summary.json; do
    if [ ! -f "$S6_BUNDLE/$artifact" ]; then
        S6_MISSING="$S6_MISSING $artifact"
    fi
done

if [ -z "$S6_MISSING" ]; then
    pass "s6-bundle-completeness"
else
    fail "s6-bundle-completeness" "missing:$S6_MISSING"
fi

# Verify data files are valid JSON
S6_BAD_JSON=""
for jfile in data/issues.json data/meta.json data/triage.json data/insights.json \
    data/export_summary.json beads.sqlite3.config.json; do
    if [ -f "$S6_BUNDLE/$jfile" ]; then
        if ! python3 -m json.tool "$S6_BUNDLE/$jfile" > /dev/null 2>&1; then
            S6_BAD_JSON="$S6_BAD_JSON $jfile"
        fi
    fi
done

if [ -z "$S6_BAD_JSON" ]; then
    pass "s6-json-validity"
else
    fail "s6-json-validity" "invalid JSON:$S6_BAD_JSON"
fi

# Verify meta.json has expected fields
if [ -f "$S6_BUNDLE/data/meta.json" ]; then
    META=$(cat "$S6_BUNDLE/data/meta.json")
    META_OK=true
    for field in title generated_at issue_count generator version; do
        if ! echo "$META" | python3 -c "import sys,json; d=json.load(sys.stdin); assert '$field' in d" 2>/dev/null; then
            META_OK=false
        fi
    done
    if $META_OK; then
        pass "s6-meta-fields"
    else
        fail "s6-meta-fields" "meta.json missing required fields"
    fi
fi

# Log artifact sizes for diagnostics
log "Artifact inventory:"
find "$S6_BUNDLE" -type f -printf "  %8s  %P\n" 2>/dev/null | sort -k2 > "$S6_DIR/artifact_inventory.txt"
cat "$S6_DIR/artifact_inventory.txt" | head -20
TOTAL_FILES=$(find "$S6_BUNDLE" -type f | wc -l)
TOTAL_SIZE=$(du -sh "$S6_BUNDLE" 2>/dev/null | cut -f1)
log "Total: $TOTAL_FILES files, $TOTAL_SIZE"

# ── Scenario 7: Preview live-reload endpoint ─────────────────────────

banner "Scenario 7: Preview live-reload endpoint responds correctly"
S7_DIR="$TMPDIR_ROOT/s7_livereload"
S7_BUNDLE="$S7_DIR/pages"
mkdir -p "$S7_DIR"

"$BVR" --export-pages "$S7_BUNDLE" --beads-file "$FIXTURE" \
    > /dev/null 2>&1 || true

if [ -d "$S7_BUNDLE" ]; then
    BVR_NO_BROWSER=1 BVR_PREVIEW_MAX_REQUESTS=3 "$BVR" --preview-pages "$S7_BUNDLE" \
        > "$S7_DIR/preview_stdout.log" 2> "$S7_DIR/preview_stderr.log" &
    LR_PID=$!
    sleep 1

    PORT=$(grep -oP '(?:localhost|127\.0\.0\.1):\K[0-9]+' "$S7_DIR/preview_stdout.log" 2>/dev/null | head -1 || echo "")
    if [ -n "$PORT" ]; then
        # Test live-reload endpoint returns a token
        LR_RESPONSE=$(curl -sf "http://localhost:$PORT/.bvr/livereload" 2>/dev/null || echo "")
        if [ -n "$LR_RESPONSE" ]; then
            log "Live-reload token: $LR_RESPONSE"

            # Second request should return same token (no changes)
            LR_RESPONSE2=$(curl -sf "http://localhost:$PORT/.bvr/livereload" 2>/dev/null || echo "")
            if [ "$LR_RESPONSE" = "$LR_RESPONSE2" ]; then
                pass "s7-livereload-stable-token"
            else
                fail "s7-livereload-stable-token" "token changed without file modification"
            fi
        else
            fail "s7-livereload" "empty response from reload endpoint"
        fi

        # Consume remaining request to let server exit
        curl -sf "http://localhost:$PORT/" > /dev/null 2>&1 || true
    else
        fail "s7-livereload" "could not determine preview port"
    fi

    wait "$LR_PID" 2>/dev/null || true
else
    fail "s7-livereload" "no bundle to preview"
fi

# ── Scenario 8: Export with include-closed=false ─────────────────────

banner "Scenario 8: Export respects include-closed=false"
S8_DIR="$TMPDIR_ROOT/s8_no_closed"
S8_BUNDLE="$S8_DIR/pages"
mkdir -p "$S8_DIR"

"$BVR" --export-pages "$S8_BUNDLE" --beads-file "$FIXTURE" \
    --pages-include-closed=false \
    > "$S8_DIR/export_stdout.log" 2> "$S8_DIR/export_stderr.log" || true

if [ -f "$S8_BUNDLE/data/meta.json" ]; then
    INCLUDE_CLOSED=$(python3 -c "import json; d=json.load(open('$S8_BUNDLE/data/meta.json')); print(d.get('include_closed', 'MISSING'))" 2>/dev/null || echo "ERROR")
    if [ "$INCLUDE_CLOSED" = "False" ]; then
        pass "s8-exclude-closed"
    else
        fail "s8-exclude-closed" "include_closed=$INCLUDE_CLOSED (expected False)"
    fi
else
    fail "s8-exclude-closed" "no meta.json produced"
fi

# ── Summary ──────────────────────────────────────────────────────────

banner "Summary"
echo ""
for s in "${SCENARIOS[@]}"; do
    echo "  $s"
done
echo ""
log "Results: $PASS passed, $FAIL failed"
log "Workspace: $TMPDIR_ROOT"

if [ "$FAIL" -gt 0 ]; then
    log "FAILURE — see logs in $TMPDIR_ROOT"
    exit 1
fi

log "All scenarios passed."
exit 0
