#!/usr/bin/env bash
# multi-user-customer-test — validate staging from a customer's perspective.
#
# Per operator directive 2026-05-18 (operational-organization-activation-wave-1).
# Tests how a real human user would experience ARQERA on staging:
#   - public surfaces reachable
#   - auth path navigable
#   - onboarding / Command / Trust / Ledger / support routes alive
#   - error pages don't leak substrate vocabulary
#   - customer-safe language on customer surfaces
#
# Habitat-portable. No mutations. Read-only HTTP probes against staging.
#
# Auth-gated routes: we follow redirects to verify the gate exists +
# does not 500. Actual authenticated journeys are BLOCKED until the
# staging UAT credential gap (UAT cycle-1 gap #21) is closed.
#
# Findings emit as substrate UAT findings if --emit is set.

set -euo pipefail

FORMAT="human"
EMIT="false"
STAGING="${ARQERA_STAGING_URL:-https://staging.arqera.io}"
while [[ $# -gt 0 ]]; do
  case "$1" in
    --json) FORMAT="json"; shift ;;
    --emit) EMIT="true"; FORMAT="json"; shift ;;
    --staging) STAGING="$2"; shift 2 ;;
    --help|-h) sed -n '2,20p' "$0"; exit 0 ;;
    *) echo "unknown arg: $1" >&2; exit 2 ;;
  esac
done

for tool in curl python3; do
  if ! command -v "$tool" >/dev/null 2>&1; then
    echo "error: $tool not on PATH" >&2; exit 2
  fi
done

RESULTS=""
record() {
  RESULTS+="$1|$2|$3|$4|$5"$'\n'
}

# ----------------------------------------------------------------------------
# Substrate-aware classification per arq://doc/principle/...:
# read frontend/lib/surface-declarations.ts to learn each route's
# declared audience. The classifier honours that audience instead of
# inventing PASS/FAIL from raw HTTP codes.
#
# audience semantics:
#   anonymous → must be reachable unauthenticated (HTTP 2xx without
#               redirect to /auth/login)
#   personal  → auth-gated by design; redirect to login is CORRECT
#   operator  → auth-gated by design; redirect to login is CORRECT
#   undeclared → governance gap (γ-rail); record honestly as GAP
# ----------------------------------------------------------------------------
DECLS_FILE="frontend/lib/surface-declarations.ts"

declared_audience() {
  # Read the audience for a given route name from surface-declarations.ts.
  # Output: anonymous | personal | operator | undeclared
  local route="$1"
  if [[ ! -f "$DECLS_FILE" ]]; then
    echo "undeclared"; return
  fi
  python3 - "$route" "$DECLS_FILE" <<'PY'
import sys, re
route, path = sys.argv[1], sys.argv[2]
src = open(path).read()
# Find blocks: { name: '<route>', ... audience: '<aud>', ... }
pattern = re.compile(
    r"\{\s*name:\s*['\"]" + re.escape(route) + r"['\"][^}]*?audience:\s*['\"]([^'\"]+)['\"]",
    re.S,
)
m = pattern.search(src)
print(m.group(1).strip() if m else "undeclared")
PY
}

# Probe a route honestly per substrate audience declaration.
#
# Behaviour:
#   - Probes WITHOUT following redirects first → captures the real
#     pre-redirect status. Then if 3xx, follows once to see destination.
#   - Reads substrate-declared audience.
#   - Verdict tagged with declared audience so cycle output is honest:
#       audience=anonymous + 2xx no-redirect → PASS
#       audience=anonymous + redirect-to-login → FAIL (should be public)
#       audience=personal/operator + redirect-to-login → PASS-by-design
#       audience=undeclared + anything → GAP (governance gap)
probe_route() {
  local label="$1" route="$2"
  local url="${STAGING}${route}"
  local audience t0 t1 ms code_noredir redirect_target
  audience=$(declared_audience "$route")
  t0=$(python3 -c 'import time;print(time.time())')
  # First probe: no -L. Captures the actual server status.
  code_noredir=$(curl -s -o /tmp/cust_body_$$ -w '%{http_code}' --max-time 15 "$url" 2>/dev/null || echo "000")
  redirect_target=""
  if [[ "$code_noredir" =~ ^3 ]]; then
    redirect_target=$(curl -s -o /dev/null -w '%{redirect_url}' --max-time 5 "$url" 2>/dev/null || echo "")
  fi
  t1=$(python3 -c 'import time;print(time.time())')
  ms=$(python3 -c "print(int(($t1-$t0)*1000))")

  # Detect "redirect to login" specifically.
  local is_login_redirect="no"
  if [[ "$redirect_target" == *"/auth/login"* ]] || [[ "$redirect_target" == *"/auth/signup"* ]]; then
    is_login_redirect="yes"
  fi

  # Substrate-declared audience values (from surface-declarations.ts):
  #   personal | business | operator | legacy-canonicalization
  # (No `anonymous` value exists in substrate — earlier classifier arm
  # was unreachable; Sentry MEDIUM on #3930.)
  case "$audience" in
    personal|business)
      # Customer-facing surfaces. Two valid renderings:
      #   - direct render (public marketing version) → PASS
      #   - login-redirect (auth-gated post-signin version) → PASS-BY-DESIGN
      if [[ "$is_login_redirect" == "yes" ]]; then
        record "customer-surfaces" "$label[audience=$audience]" "PASS-BY-DESIGN" "${url} http=${code_noredir} → ${redirect_target} (customer surface per substrate; login redirect — post-signin variant)" ""
      elif [[ "$code_noredir" =~ ^2 ]]; then
        record "customer-surfaces" "$label[audience=$audience]" "PASS" "${url} http=${code_noredir} ${ms}ms (customer-facing; renders pre-signin)" ""
      else
        record "customer-surfaces" "$label[audience=$audience]" "FAIL" "${url} http=${code_noredir}" ""
      fi ;;
    operator)
      # Operator surfaces are intentionally login-gated; PASS-BY-DESIGN.
      if [[ "$is_login_redirect" == "yes" ]]; then
        record "customer-surfaces" "$label[audience=operator]" "PASS-BY-DESIGN" "${url} http=${code_noredir} → ${redirect_target} (operator surface; auth-gated as designed)" ""
      elif [[ "$code_noredir" =~ ^2 ]]; then
        record "customer-surfaces" "$label[audience=operator]" "PASS" "${url} http=${code_noredir} ${ms}ms (operator surface rendered without login — verify intent)" ""
      else
        record "customer-surfaces" "$label[audience=operator]" "FAIL" "${url} http=${code_noredir}" ""
      fi ;;
    legacy-canonicalization)
      # Transitional state per surface-declarations.ts; supersession or
      # retirement pending. Don't FAIL; tag so retirement progress is
      # visible.
      record "customer-surfaces" "$label[audience=legacy-canonicalization]" "GAP" "${url} http=${code_noredir}${redirect_target:+ → $redirect_target} (substrate-declared legacy-canonicalization; supersession or retirement pending)" "" ;;
    undeclared)
      if [[ "$code_noredir" =~ ^2 ]] || [[ "$is_login_redirect" == "yes" ]]; then
        record "customer-surfaces" "$label[audience=undeclared]" "GAP" "${url} http=${code_noredir}${redirect_target:+ → $redirect_target} (γ-rail gap — route has no substrate audience declaration in surface-declarations.ts)" ""
      else
        record "customer-surfaces" "$label[audience=undeclared]" "FAIL" "${url} http=${code_noredir}" ""
      fi ;;
    *)
      # Unknown audience value (substrate added something we don't yet
      # handle). Tag honestly so the test surfaces the gap rather than
      # silently dropping the row.
      record "customer-surfaces" "$label[audience=$audience]" "GAP" "${url} http=${code_noredir}${redirect_target:+ → $redirect_target} (classifier doesn't yet handle audience='$audience' — extend probe_route case arms)" "" ;;
  esac
  rm -f /tmp/cust_body_$$
}

# --- All probed routes — substrate audience determines verdict ---
# No more public/auth-gated/either classification at call-site; the
# probe_route function reads surface-declarations.ts.
for route_pair in \
    "landing:/" \
    "auth-login:/auth/login" \
    "auth-register:/auth/register" \
    "auth-forgot-password:/auth/forgot-password" \
    "pricing:/pricing" \
    "trust:/trust" \
    "onboarding:/onboarding" \
    "command:/command" \
    "ledger:/ledger" \
    "home:/home" \
    "market:/market" \
    "workforce:/workforce" \
    "marketplace:/marketplace" \
    "agents:/agents" \
    "help:/help" \
    "docs:/docs"; do
  label="${route_pair%%:*}"
  route="${route_pair#*:}"
  probe_route "$label" "$route"
done

# --- Section 4: API health endpoint (operator surface) ---
HEALTH_CODE=$(curl -s -o /dev/null -w '%{http_code}' --max-time 10 "${STAGING}/api/health" 2>/dev/null || echo 000)
if [[ "$HEALTH_CODE" =~ ^2 ]]; then
  record "api-health" "/api/health-reachable" "PASS" "http=$HEALTH_CODE" ""
else
  record "api-health" "/api/health-reachable" "FAIL" "http=$HEALTH_CODE" ""
fi

# --- Section 5: substrate-vocabulary leak check on landing HTML ---
HTML=$(curl -s -L --max-time 10 "${STAGING}/" 2>/dev/null || echo "")
LEAKED_TOKENS=""
for token in "substrate_truth_reconciliation" "capability_concentration_score" "operator_tier" "best_habitat" "sovereignty_distribution"; do
  if echo "$HTML" | grep -q "$token"; then
    LEAKED_TOKENS+="$token "
  fi
done
if [[ -n "$LEAKED_TOKENS" ]]; then
  record "customer-safe-language" "landing-vocab-leak" "FAIL" "leaked tokens: $LEAKED_TOKENS" ""
else
  record "customer-safe-language" "landing-vocab-leak" "PASS" "no substrate vocab in landing HTML" ""
fi

# --- Section 6: 404 page behaviour ---
FOUR_O_FOUR=$(curl -s -L -o /dev/null --max-time 10 -w '%{http_code}' "${STAGING}/nonexistent-page-12345" 2>/dev/null || echo 000)
if [[ "$FOUR_O_FOUR" == "404" ]]; then
  record "error-pages" "404-returns-404" "PASS" "explicit 404 for nonexistent route" ""
elif [[ "$FOUR_O_FOUR" =~ ^2 ]]; then
  # SPA fallbacks return 200 with a "not found" client view — acceptable
  record "error-pages" "404-spa-fallback" "PASS" "200 with SPA fallback (client renders 404)" ""
else
  record "error-pages" "404-handling" "GAP" "unexpected http=$FOUR_O_FOUR" ""
fi

# --- Section 7: gap: UAT credentials + Turnstile bypass substrate-evidenced? ---
# Updated 2026-05-18: investigating cycle-1's "/auth/register 422" + "/auth/login 403"
# verdicts revealed BOTH were test-script bugs, not customer-blocking bugs:
#   - 422 register = test sent first_name+last_name; backend schema needs `name`
#   - 403 login = Cloudflare Turnstile correctly rejecting scriptless callers
# Real users using the staging UI work for both. The genuine substrate gap is
# the absence of a test-credential + Turnstile-bypass-token substrate-evidence
# pair that would let CI/UAT exercise auth-gated journeys end-to-end.
CRED_REF=$(twin --use-keychain index --type test_credential_issued --since 2026-05-17 2>&1 | head -1 | grep -oE "arq://act/[^ ]+" || echo "")
BYPASS_REF=$(twin --use-keychain index --type turnstile_bypass_evidenced --since 2026-05-17 2>&1 | head -1 | grep -oE "arq://act/[^ ]+" || echo "")
if [[ -n "$CRED_REF" && -n "$BYPASS_REF" ]]; then
  record "auth-gated-journeys" "test-credentials-and-bypass-substrate-evidenced" "PASS" "$CRED_REF + $BYPASS_REF" ""
else
  record "auth-gated-journeys" "test-credentials-and-bypass-substrate-evidenced" "GAP" "no substrate-evidenced test_credential AND turnstile_bypass_evidenced pair; auth-gated journeys cannot run end-to-end from CI" ""
fi

# --- Tally + output ---
RESULTS_JSON=$(RAW="$RESULTS" STAGING="$STAGING" python3 - <<'PY'
import os, json, datetime
checks = []
totals = {"PASS": 0, "PASS-BY-DESIGN": 0, "GAP": 0, "FAIL": 0}
for line in os.environ.get("RAW", "").strip().splitlines():
    if not line:
        continue
    parts = line.split('|', 4)
    if len(parts) != 5:
        continue
    section, check, verdict, evidence, _ = parts
    checks.append({"section": section, "check": check, "verdict": verdict, "evidence": evidence})
    totals[verdict] = totals.get(verdict, 0) + 1
print(json.dumps({
    "schema_version": 1,
    "test": "multi-user-customer-test",
    "tested_at": datetime.datetime.now(datetime.timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ"),
    "staging_base": os.environ.get("STAGING", ""),
    "totals": totals,
    "checks": checks,
}, indent=2))
PY
)

if [[ "$EMIT" == "true" ]]; then
  TODAY=$(date -u +%Y-%m-%d)
  PAYLOAD=$(echo "$RESULTS_JSON" | python3 -c "import json,sys;print(json.dumps(json.load(sys.stdin)))")
  twin --use-keychain act emit act multi_user_customer_test_completed "$TODAY" \
    --payload "$PAYLOAD" --source twin-multi-user-customer-test --sync >/dev/null
fi

if [[ "$FORMAT" == "json" ]]; then
  echo "$RESULTS_JSON"
  FAIL=$(echo "$RESULTS_JSON" | python3 -c "import json,sys;print(json.load(sys.stdin)['totals'].get('FAIL',0))")
  [[ "$FAIL" -gt 0 ]] && exit 1 || exit 0
fi

echo "=== multi-user-customer-test ($STAGING) ==="
echo ""
P=$(echo "$RESULTS_JSON" | python3 -c "import json,sys;print(json.load(sys.stdin)['totals'].get('PASS',0))")
PD=$(echo "$RESULTS_JSON" | python3 -c "import json,sys;print(json.load(sys.stdin)['totals'].get('PASS-BY-DESIGN',0))")
G=$(echo "$RESULTS_JSON" | python3 -c "import json,sys;print(json.load(sys.stdin)['totals'].get('GAP',0))")
F=$(echo "$RESULTS_JSON" | python3 -c "import json,sys;print(json.load(sys.stdin)['totals'].get('FAIL',0))")
echo "  PASS: $P  PASS-BY-DESIGN: $PD  GAP: $G  FAIL: $F"
echo ""
printf "  %-22s %-40s %-6s %s\n" "section" "check" "verdict" "evidence"
echo "$RESULTS_JSON" | python3 -c "
import json,sys
d=json.load(sys.stdin)
for c in d['checks']:
    print(f\"  {c['section']:<22} {c['check']:<40} {c['verdict']:<6} {c['evidence'][:80]}\")"
echo ""
if [[ "$F" -gt 0 ]]; then
  echo "  verdict: FAIL ($F customer surfaces broken)"
  exit 1
elif [[ "$G" -gt 0 ]]; then
  echo "  verdict: PASS-with-gaps ($G gaps routed as operational pressure)"
else
  echo "  verdict: PASS (customer surfaces all reachable)"
fi
