#!/usr/bin/env bash
# scripts/dev/cli_smoke.sh — v2.0.17 N.47 (TSK-N.31 closeout)
#
# Comprehensive real-CLI smoke matrix for the scheduler orchestrator.
# Codifies the 30-command sweep that surfaced G19/G20/G21/G22/G29 in v2.0.15.
# Each step is tagged with the FR / G-finding it covers.
#
# Usage:
#   ./scripts/dev/cli_smoke.sh           # run against localhost:8005
#   VDS_SCHEDULER_BASE_URL=http://other:8005 ./scripts/dev/cli_smoke.sh
#
# Prerequisites:
#   - Running scheduler container (docker compose --profile scheduler up -d)
#   - VDS_SCHEDULER_ENABLED=true (kill switch on)
#   - VDS_SCHEDULER_CAPABILITY=admin (for write ops; falls back to error path)
#
# Each test step prints PASS / FAIL / SKIP and a one-line summary. The script
# exits 0 only if every PASS-required step passed. Cleanup uses the platform
# capability to delete test artifacts.

set -uo pipefail

VDS_SCHEDULER_BASE_URL="${VDS_SCHEDULER_BASE_URL:-http://localhost:8005}"
CLI="${VDS_CLI:-./.venv/bin/vds-cli}"
SUFFIX="$(date +%Y%m%d-%H%M%S)"

# ── Output helpers ────────────────────────────────────────────────────────────
PASS_COUNT=0
FAIL_COUNT=0
SKIP_COUNT=0
FAILURES=()

pass() { echo "  ✅ PASS — $1"; PASS_COUNT=$((PASS_COUNT + 1)); }
fail() { echo "  ❌ FAIL — $1"; FAIL_COUNT=$((FAIL_COUNT + 1)); FAILURES+=("$1"); }
skip() { echo "  ⏭  SKIP — $1"; SKIP_COUNT=$((SKIP_COUNT + 1)); }
section() { echo ""; echo "═══ $1 ═══"; }

assert_status_eq() {
    local desc="$1" expected="$2" actual="$3"
    if [[ "$actual" == *"$expected"* ]]; then pass "$desc"; else fail "$desc — expected '$expected', got '$actual'"; fi
}

# ── Pre-flight ────────────────────────────────────────────────────────────────
section "Pre-flight"
if ! curl -s --max-time 3 "$VDS_SCHEDULER_BASE_URL/health" > /dev/null 2>&1; then
    echo "FATAL: scheduler unreachable at $VDS_SCHEDULER_BASE_URL"
    exit 2
fi
HEALTH=$(curl -s "$VDS_SCHEDULER_BASE_URL/health" | python3 -c "import sys,json; print(json.load(sys.stdin)['status'])" 2>/dev/null || echo "unknown")
assert_status_eq "/health returns ok" "ok" "$HEALTH"

KILL_SWITCH=$(curl -s "$VDS_SCHEDULER_BASE_URL/health" | python3 -c "import sys,json; print(json.load(sys.stdin)['scheduler_enabled'])" 2>/dev/null)
if [[ "$KILL_SWITCH" != "True" && "$KILL_SWITCH" != "true" ]]; then
    skip "kill switch is OFF — write-side tests will be incomplete (set VDS_SCHEDULER_ENABLED=true to fully exercise)"
fi

# ── Auth-aware curl helper (v2.0.18 — auto-attach API key when AUTH is on) ────
# When VDS_SCHEDULER_AUTH_REQUIRED=true is set in the running scheduler, every
# /api/v1/* probe needs X-VDS-Scheduler-Key. Detect via /health and auto-attach
# from VDS_SCHEDULER_API_KEY env so downstream tests don't all 401 in auth-mode.
AUTH_HEADER=()
if [[ -n "${VDS_SCHEDULER_API_KEY:-}" ]]; then
    POSTURE_PROBE=$(curl -s "$VDS_SCHEDULER_BASE_URL/health" | python3 -c "import sys,json; print(json.load(sys.stdin).get('auth_posture','unknown'))" 2>/dev/null || echo "unknown")
    if [[ "$POSTURE_PROBE" == "required" ]]; then
        AUTH_HEADER=(-H "X-VDS-Scheduler-Key: $VDS_SCHEDULER_API_KEY")
        echo "  ℹ  AUTH detected as required — auto-attaching X-VDS-Scheduler-Key to /api/v1/* probes"
    fi
fi

# ── Health endpoint contract ──────────────────────────────────────────────────
section "Health endpoint contract"
FULL=$(curl -s "${AUTH_HEADER[@]:-}" "$VDS_SCHEDULER_BASE_URL/api/v1/health/full")
echo "$FULL" | python3 -c "import sys,json; d=json.load(sys.stdin); assert 'layers' in d and 'dbos' in d['layers'] and 'db' in d['layers']" 2>/dev/null \
    && pass "/api/v1/health/full has dbos + db layer keys" \
    || fail "/api/v1/health/full missing required layer keys"

# ── Output extractor: strips uv/typer warnings, captures meaningful response ──
# CLI may emit warnings (`warning: The package …`) ahead of the real response.
# Extract by content (HTTP NNN or JSON) rather than by line position.
extract_resp() {
    # Concatenate the entire CLI output, then pull the first HTTP NNN match
    # OR the first {…}-style JSON line. Fallback: the last non-warning line.
    local all="$1"
    local http
    http=$(echo "$all" | grep -oE 'HTTP [0-9]{3}' | head -1)
    if [[ -n "$http" ]]; then
        # Include the trailing detail body (everything after the first HTTP NNN)
        echo "$all" | sed -nE '/HTTP [0-9]{3}/,$p' | tr -d '\n'
        return
    fi
    # No HTTP code → return JSON-or-trailing line, with warnings stripped
    echo "$all" | grep -vE '^warning:' | tail -1
}

# ── G19: unknown FQN-shaped alias is permitted with deprecation warning ──────
# FR-11.3: FQN-based create is permitted as a fallback but emits a deprecation
# warning. Unknown FQN → schedule created (HTTP 201), warning emitted; failure
# only at fire time when the dispatcher tries to import the module. (Was "must
# be 400" pre-v2.0.20 — corrected to match the spec contract. Warn-to-reject
# promotion deferred to v2.1.0+ when FR-11.3 deprecation matures.)
section "G19 — unknown FQN alias permitted with deprecation warning (FR-11.3)"
SCHED_NAME="smoke-g19-$SUFFIX"
RESP=$(extract_resp "$("$CLI" scheduler schedules create "$SCHED_NAME" --workflow "totally.fake.alias" --cron "0 9 * * *" --queue ops-default 2>&1)")
if [[ "$RESP" == *"HTTP 201"* || "$RESP" == *'"schedule_name"'* ]]; then
    pass "FQN alias create returns 201 per FR-11.3"
else
    fail "FQN alias create unexpected response: ${RESP:0:120}"
fi
# Cleanup: delete the orphan schedule (would never fire successfully).
curl -s -o /dev/null -X DELETE "${AUTH_HEADER[@]:-}" -H "X-Capability: platform" "$VDS_SCHEDULER_BASE_URL/api/v1/schedules/$SCHED_NAME"

# ── G20: unknown queue on chains → 400 (was 201) ──────────────────────────────
section "G20 — chains create with unknown queue maps to 400"
RESP=$(extract_resp "$("$CLI" scheduler chains create "smoke-g20-$SUFFIX" --trigger "event:foo" --target "scheduler.autoscale" --queue "fake_q" 2>&1)")
assert_status_eq "chains bad queue → 400" "HTTP 400" "$RESP"

# ── G21: secret scanner — all 10 patterns ─────────────────────────────────────
section "G21 — secret scanner (10 pattern classes)"
test_secret() {
    local name="$1" payload="$2"
    local resp
    resp=$(extract_resp "$("$CLI" scheduler events publish "smoke-g21.$name.$SUFFIX" --payload "$payload" 2>&1)")
    if [[ "$resp" == *"HTTP 400"* ]] && [[ "$resp" == *"secret"* ]]; then
        pass "$name pattern blocked"
    elif [[ "$resp" == *event_id* ]]; then
        fail "$name pattern NOT blocked (HTTP 201 — security regression)"
    else
        fail "$name pattern unexpected response: ${resp:0:80}"
    fi
}
test_secret "aws_key" '{"k":"AKIAIOSFODNN7EXAMPLE"}'
test_secret "github_pat" '{"k":"ghp_1234567890abcdefghij1234567890abcdef12"}'
test_secret "jwt" '{"jwt":"eyJabc.eyJxyz.signature"}'
test_secret "slack" '{"k":"xoxb-1234567890-abcdefg"}'
test_secret "gcp" '{"k":"AIzaSyDuMmYyAaBbCcDdEeFfGgHhIiJjKkLlMm0"}'
test_secret "stripe" '{"k":"sk_live_1234567890abcdefghij1234"}'
test_secret "bearer" '{"k":"Bearer ya29.A0AfH6SMA-fake"}'

# ── TSK-N.62: chains capability gating ────────────────────────────────────────
# Verifies the full guest→403 / platform→201/204 matrix. Auto-attaches API key
# via $AUTH_HEADER when AUTH_REQUIRED=true so capability test isn't masked by 401.
section "TSK-N.62 — chains capability gating (guest 403 / platform 201/204)"
CHAIN_NAME="smoke-n62-$SUFFIX"
# Step 1: create as platform (must succeed) so we have a chain to delete-test against
CREATE_RESP=$(curl -s -o /dev/null -w "%{http_code}" -X POST "${AUTH_HEADER[@]:-}" \
  -H "Content-Type: application/json" \
  -H "X-Capability: platform" \
  -d "{\"chain_name\":\"$CHAIN_NAME\",\"trigger_kind\":\"on_complete\",\"trigger_source\":\"scheduler.autoscale\",\"target_workflow_fqn\":\"scheduler.health_check\"}" \
  "$VDS_SCHEDULER_BASE_URL/api/v1/chains")
case "$CREATE_RESP" in
    201) pass "chain create as platform → 201" ;;
    *)   fail "chain create as platform unexpected status: $CREATE_RESP" ;;
esac
# Step 2: delete as guest (must be 403)
DEL_RESP=$(curl -s -o /dev/null -w "%{http_code}" -X DELETE "${AUTH_HEADER[@]:-}" \
  -H "X-Capability: guest" \
  "$VDS_SCHEDULER_BASE_URL/api/v1/chains/$CHAIN_NAME")
case "$DEL_RESP" in
    403) pass "chain delete as guest → 403 (capability gate enforced)" ;;
    204) fail "TSK-N.62 regression — guest deleted platform-owned chain (privilege escalation)" ;;
    401) skip "AUTH on but VDS_SCHEDULER_API_KEY not in env — set it to exercise capability gate" ;;
    *)   fail "chain delete as guest unexpected status: $DEL_RESP" ;;
esac
# Step 3: cleanup as platform
curl -s -o /dev/null -X DELETE "${AUTH_HEADER[@]:-}" -H "X-Capability: platform" "$VDS_SCHEDULER_BASE_URL/api/v1/chains/$CHAIN_NAME"

# ── TSK-N.63: auth-flag posture check ─────────────────────────────────────────
section "TSK-N.63 — auth posture check"
AUTH_HEALTH=$(curl -s "$VDS_SCHEDULER_BASE_URL/health" | python3 -c "import sys,json; print(json.load(sys.stdin).get('auth_posture','unknown'))" 2>/dev/null || echo "unknown")
if [[ "$AUTH_HEALTH" == "required" ]]; then
    pass "auth_posture=required detected"
    API_KEY="${VDS_SCHEDULER_API_KEY:-}"
    if [[ -n "$API_KEY" ]]; then
        RESP=$(curl -s -o /dev/null -w "%{http_code}" -H "X-VDS-Scheduler-Key: $API_KEY" "$VDS_SCHEDULER_BASE_URL/api/v1/schedules")
        if [[ "$RESP" == "200" ]]; then pass "auth enabled — valid key returns 200 ($RESP)"
        else fail "auth enabled — valid key returned $RESP (expected 200)"; fi
    else
        skip "VDS_SCHEDULER_API_KEY not set — cannot test valid key path"
    fi
    RESP=$(curl -s -o /dev/null -w "%{http_code}" "$VDS_SCHEDULER_BASE_URL/api/v1/schedules")
    if [[ "$RESP" == "401" ]]; then pass "auth enabled — no key returns 401 ($RESP)"
    else fail "auth enabled — no key returned $RESP (expected 401)"; fi
elif [[ "$AUTH_HEALTH" == "optional" ]]; then
    pass "auth_posture=optional (default)"
    RESP=$(curl -s -o /dev/null -w "%{http_code}" "$VDS_SCHEDULER_BASE_URL/api/v1/schedules")
    if [[ "$RESP" == "200" ]]; then pass "auth optional — no key returns 200 ($RESP)"
    else fail "auth optional — no key returned $RESP (expected 200)"; fi
else
    skip "auth_posture unknown: $AUTH_HEALTH"
fi

# ── G22: events publish --trace-id flows ──────────────────────────────────────
section "G22 — events publish --trace-id round-trips"
TID="smoke-tid-$SUFFIX"
RESP=$("$CLI" scheduler events publish "smoke-g22.$SUFFIX" --payload '{"x":1}' --trace-id "$TID" 2>&1 | grep -vE '^warning:')
if [[ "$RESP" == *"\"trace_id\": \"$TID\""* ]]; then pass "trace_id round-trips"; else fail "trace_id missing — got: ${RESP:0:80}"; fi

# ── G29: capability mismatch → 403 (was 500) ──────────────────────────────────
section "G29 — capability mismatch maps to HTTP 403"
# Test against the seed schedule `evolution-auto-promote` (owner =
# evolution_orchestrator). Send X-Capability: guest, which doesn't match
# the owner and isn't 'platform' — must surface as HTTP 403, not 500.
# Pre-v2.0.17 G29 used `audit-deep-monthly` which is no longer in the
# shared-infra seed; the smoke step was masked by the idempotent NOT-FOUND
# 204 path. v2.0.17 G29-followup landed X-Capability header extraction so
# this step now exercises the route → domain wiring end-to-end.
RESP=$(curl -s -o /dev/null -w "%{http_code}" -X DELETE "${AUTH_HEADER[@]:-}" -H "X-Capability: guest" "$VDS_SCHEDULER_BASE_URL/api/v1/schedules/evolution-auto-promote")
case "$RESP" in
    403) pass "capability mismatch returns 403 ($RESP)" ;;
    404) skip "schedule not present — 404 (seed missing evolution-auto-promote)" ;;
    500) fail "G29 regression — DELETE returned 500 (CapabilityError leaked)" ;;
    204) fail "G29 regression — DELETE returned 204 (privilege escalation: caller capability ignored)" ;;
    401) skip "AUTH on but VDS_SCHEDULER_API_KEY not in env — set it to exercise capability gate" ;;
    *) skip "unexpected status $RESP — manual investigation needed" ;;
esac

# ── G24 + N.39 + N.40: evolution.auto-promote workflow alive ──────────────────
section "G24+N.39+N.40 — evolution.auto-promote workflow firing"
EVO_COUNT=$("$CLI" scheduler workflows list --status all --since 5m --limit 20 2>&1 | python3 -c "import sys,json; d=json.load(sys.stdin); print(sum(1 for w in d if 'evolution_auto_promote' in w.get('name','')))" 2>/dev/null || echo "0")
if [[ "$EVO_COUNT" -gt 0 ]]; then pass "evolution_auto_promote_workflow appeared $EVO_COUNT times in last 5m"; else skip "no evolution_auto_promote runs in window — kill switch OFF or wait longer"; fi

# ── N.49: health endpoint DSN sanitization ────────────────────────────────────
section "N.49 — health endpoint redacts DSN credentials"
# This test requires the DB to be unreachable to surface the credential-leak path.
# In a healthy system we just verify the response shape doesn't have plaintext DSN.
ERROR_FIELD=$(echo "$FULL" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d['layers']['db'].get('error',''))" 2>/dev/null)
if [[ -n "$ERROR_FIELD" ]] && [[ "$ERROR_FIELD" == *"@"* ]] && [[ "$ERROR_FIELD" != *"[redacted]"* ]]; then
    fail "DSN credentials leaked in /api/v1/health/full error: ${ERROR_FIELD:0:80}"
else
    pass "health/full does not leak DSN credentials (error: '$ERROR_FIELD')"
fi

# ── Read endpoints (reachability + shape) ─────────────────────────────────────
section "Read endpoints"
"$CLI" scheduler schedules list 2>&1 | python3 -c "import sys,json; d=json.load(sys.stdin); assert isinstance(d, list)" 2>/dev/null \
    && pass "schedules list returns a JSON array" \
    || fail "schedules list returned non-array"
"$CLI" scheduler workflows registry list 2>&1 | python3 -c "import sys,json; d=json.load(sys.stdin); assert isinstance(d, list)" 2>/dev/null \
    && pass "workflows registry list returns a JSON array" \
    || fail "workflows registry list returned non-array"

# ── Cleanup test artifacts ────────────────────────────────────────────────────
section "Cleanup"
docker exec vds-postgres psql -U postgres -d vds_platform -c "DELETE FROM sched.events_outbox WHERE topic LIKE 'smoke-%';" > /dev/null 2>&1 \
    && pass "test events removed via SQL" \
    || skip "SQL cleanup unavailable (not running against vds-postgres) — operator may need manual cleanup"

# ── Summary ───────────────────────────────────────────────────────────────────
echo ""
echo "═══════════════════════════════════════════════"
echo "Smoke matrix: $PASS_COUNT passed · $FAIL_COUNT failed · $SKIP_COUNT skipped"
if [[ "$FAIL_COUNT" -gt 0 ]]; then
    echo ""
    echo "Failures:"
    for f in "${FAILURES[@]}"; do echo "  - $f"; done
    exit 1
fi
echo "═══════════════════════════════════════════════"
exit 0
