#!/usr/bin/env bash
# smoke-schema-validation.sh  -  verifies schema files are well-formed and
# self-consistent, and that their immediate artifacts (preferences template,
# live preferences after migration) match the expected shape.
#
# This is a lightweight structural check. Deep JSONSchema validation using
# ajv/jsonschema is deferred to CI (where package deps are installed).

set -uo pipefail

PASS=0
FAIL=0
pass() { PASS=$((PASS + 1)); echo "  ✓ $1"; }
fail() { FAIL=$((FAIL + 1)); echo "  ✗ $1"; }

SCHEMAS_DIR="$HOME/.claude/schemas"
PREFS_SCHEMA="$SCHEMAS_DIR/prefs.schema.json"
STATE_SCHEMA="$SCHEMAS_DIR/agent-state.schema.json"
# Repo checkout first (derived from this script's location), then the
# maintainer's legacy layout. The template ships only in the repo - when the
# smoke runs from an installed tree with no checkout, the check is skipped.
SMOKE_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
TEMPLATE="$SMOKE_DIR/../preferences-template.json"
[ -f "$TEMPLATE" ] || TEMPLATE="$HOME/multi-agent-pipeline/pipeline/preferences-template.json"
LIVE_PREFS="$HOME/.claude/multi-agent-preferences.json"

# ──────────────────────────────────────────────────────────────────────────
echo "→ 1. Schema files parse as JSON"
for f in "$PREFS_SCHEMA" "$STATE_SCHEMA"; do
  if [ ! -f "$f" ]; then
    fail "missing: $f"
    continue
  fi
  if node -e "JSON.parse(require('fs').readFileSync('$f', 'utf8'))" 2>/dev/null; then
    pass "$(basename "$f") parses as JSON"
  else
    fail "$(basename "$f") JSON parse error"
  fi
done

# ──────────────────────────────────────────────────────────────────────────
echo ""
echo "→ 2. Required schema metadata present"
check_meta() {
  local f="$1"
  local k="$2"
  local v
  v=$(node -e "const s=JSON.parse(require('fs').readFileSync('$f','utf8')); console.log(s['$k'] || '')" 2>/dev/null)
  if [ -n "$v" ]; then
    pass "$(basename "$f"): $k present"
  else
    fail "$(basename "$f"): $k missing"
  fi
}
for f in "$PREFS_SCHEMA" "$STATE_SCHEMA"; do
  [ -f "$f" ] || continue
  check_meta "$f" "\$schema"
  check_meta "$f" "\$id"
  check_meta "$f" "title"
done

# ──────────────────────────────────────────────────────────────────────────
echo ""
echo "→ 3. prefs.schema.json  -  v2.1.0 required fields present"
REQUIRED_V21=(
  "global.properties.identities"
  "global.properties.keychainMapping"
  "global.properties.platformIdentityRouting"
  "global.properties.recentGroups"
  "global.properties.recentBranches"
  "global.properties.serviceStatus"
  "global.properties.settings"
)
for p in "${REQUIRED_V21[@]}"; do
  EXISTS=$(node -e "
    const s = JSON.parse(require('fs').readFileSync('$PREFS_SCHEMA', 'utf8'));
    const parts = 'properties.$p'.split('.');
    let cur = s; for (const k of parts) cur = cur?.[k];
    console.log(cur ? 'yes' : 'no');
  " 2>/dev/null)
  if [ "$EXISTS" = "yes" ]; then
    pass "schema has $p"
  else
    fail "schema missing $p"
  fi
done

# ──────────────────────────────────────────────────────────────────────────
echo ""
echo "→ 4. prefs.schema.json  -  keychainMapping covers every resolved service"
# A floor, not a pin: new credentials get added over time, and pinning the count
# made every addition a false failure. What matters is that the keys shipped
# flows resolve are all declared.
# process.stdout.write of a String, not console.log of a Number: console.log
# inspects non-string arguments, so with FORCE_COLOR set in the environment the
# count comes back wrapped in ANSI codes and the -ge comparison below silently
# fails on a value that is actually in range.
KM_COUNT=$(node -e "
  const s = JSON.parse(require('fs').readFileSync('$PREFS_SCHEMA', 'utf8'));
  const km = s.properties.global.properties.keychainMapping.properties;
  process.stdout.write(String(Object.keys(km).length));
" 2>/dev/null)
if [ "${KM_COUNT:-0}" -ge 12 ] 2>/dev/null; then
  pass "keychainMapping declares $KM_COUNT services (≥12 expected)"
else
  fail "keychainMapping declares ${KM_COUNT:-0} services, expected ≥12"
fi

MISSING_KEYS=$(node -e "
  const s = JSON.parse(require('fs').readFileSync('$PREFS_SCHEMA', 'utf8'));
  const km = Object.keys(s.properties.global.properties.keychainMapping.properties);
  const required = ['jira','bitbucket_token','bitbucket_user','github','confluence','figma','figma_pat','figma_mcp','firebase','npm'];
  console.log(required.filter((k) => !km.includes(k)).join(','));
" 2>/dev/null)
if [ -z "$MISSING_KEYS" ]; then
  pass "every credential the pipeline resolves is declared"
else
  fail "keychainMapping missing declared-and-used key(s): $MISSING_KEYS"
fi

# ──────────────────────────────────────────────────────────────────────────
echo ""
echo "→ 5. agent-state.schema.json  -  v2.1.0 multi-repo projects[] present"
HAS_PROJECTS=$(node -e "
  const s = JSON.parse(require('fs').readFileSync('$STATE_SCHEMA', 'utf8'));
  console.log(s.properties.projects ? 'yes' : 'no');
" 2>/dev/null)
if [ "$HAS_PROJECTS" = "yes" ]; then
  pass "agent-state schema has projects[] (multi-repo support)"
else
  fail "agent-state schema missing projects[]  -  multi-repo not supported"
fi

# ──────────────────────────────────────────────────────────────────────────
echo ""
echo "→ 6. preferences-template.json  -  ships at the migration target"
if [ -f "$TEMPLATE" ]; then
  TVER=$(node -e "console.log(JSON.parse(require('fs').readFileSync('$TEMPLATE','utf8')).schemaVersion || 'none')" 2>/dev/null)
  # The target is read from migrate-prefs.mjs, never hardcoded here  -  same
  # reasoning as the live-prefs block below, which already had to learn it.
  #
  # This check used to demand exactly "2.1.0". The migration target moved four
  # times since, and each time this gate kept the template pinned to the oldest
  # version instead of flagging it: a fresh install therefore landed on 2.1.0 and
  # every old entry in the migrator's accepted set became load-bearing purely to
  # rescue the template this gate was holding back. A template behind the target
  # is a defect, not the expected shape.
  TEMPLATE_TARGET=$(grep -oE 'TARGET_VERSION = "[0-9.]+"' "$SMOKE_DIR/migrate-prefs.mjs" 2>/dev/null \
    | grep -oE '[0-9]+\.[0-9]+\.[0-9]+')
  if [ -z "$TEMPLATE_TARGET" ]; then
    fail "cannot read TARGET_VERSION from migrate-prefs.mjs  -  the check has no reference point"
  elif [ "$TVER" = "$TEMPLATE_TARGET" ]; then
    pass "template schemaVersion: $TVER (at the migration target)"
  else
    fail "template schemaVersion: $TVER but the migration target is $TEMPLATE_TARGET  -  a fresh install would start behind and depend on a migration to catch up"
  fi

  # Check template has all new v2.1.0 fields
  for field in "platformIdentityRouting" "recentGroups" "recentBranches" "serviceStatus" "settings"; do
    EXISTS=$(node -e "
      const t = JSON.parse(require('fs').readFileSync('$TEMPLATE','utf8'));
      console.log(t.global && ('$field' in t.global) ? 'yes' : 'no');
    " 2>/dev/null)
    if [ "$EXISTS" = "yes" ]; then
      pass "template has global.$field"
    else
      fail "template missing global.$field"
    fi
  done

  # Ensure deprecated fields are not in template
  for obsolete in "defaultPriority" "defaultIssueType" "gitIdentities"; do
    EXISTS=$(node -e "
      const t = JSON.parse(require('fs').readFileSync('$TEMPLATE','utf8'));
      console.log(t.global && ('$obsolete' in t.global) ? 'yes' : 'no');
    " 2>/dev/null)
    if [ "$EXISTS" = "no" ]; then
      pass "template clean of obsolete '$obsolete'"
    else
      fail "template still has obsolete '$obsolete'"
    fi
  done
else
  # No repo checkout in reach (installed-tree execution) - the template only
  # ships in the repo, so there is nothing to validate here.
  pass "template checks skipped (no repo checkout at $TEMPLATE)"
fi

# ──────────────────────────────────────────────────────────────────────────
echo ""
echo "→ 7. Live preferences (if exists)  -  post-migration check"
if [ -f "$LIVE_PREFS" ]; then
  LVER=$(node -e "console.log(JSON.parse(require('fs').readFileSync('$LIVE_PREFS','utf8')).schemaVersion || 'none')" 2>/dev/null)

  # The target is read from migrate-prefs.mjs, never hardcoded here.
  #
  # This block used to say `if [ "$LVER" = "2.1.0" ]; then pass "already v2.1.0"`
  # with everything else falling through to `fail "unknown schemaVersion"`. The
  # migration target had since moved to 2.4.0, which inverted the gate: a prefs
  # file left behind at 2.1.0 reported PASS as "already current", while a file
  # correctly migrated to 2.4.0 hit the else arm and FAILED as "unknown". The
  # gate was rejecting the only fully-migrated state it exists to encourage.
  #
  # Reading the target from the migrator, and the accepted set from the schema
  # enum, means this can never disagree with them again.
  MIGRATOR="$SMOKE_DIR/migrate-prefs.mjs"
  TARGET=$(grep -oE 'TARGET_VERSION = "[0-9.]+"' "$MIGRATOR" 2>/dev/null | grep -oE '[0-9]+\.[0-9]+\.[0-9]+')
  KNOWN=$(node -e "
    const s = require('$PREFS_SCHEMA');
    process.stdout.write((s.properties.schemaVersion.enum || []).join(' '));
  " 2>/dev/null)

  if [ -z "$TARGET" ]; then
    fail "cannot read TARGET_VERSION from migrate-prefs.mjs - the check has no reference point"
  elif [ "$LVER" = "$TARGET" ]; then
    pass "live prefs at the migration target (v$TARGET)"
  elif [ "$LVER" = "none" ]; then
    echo "  ⓘ live prefs carries no schemaVersion  -  migration pending"
    pass "live prefs present, needs migration (schemaVersion: none)"
  elif printf '%s\n' $KNOWN | grep -qxF "$LVER"; then
    # A known-but-older version is legitimately "migration pending", not a
    # failure: migration is its own install step. Say how far behind it is, so a
    # file sitting three versions back is visible instead of reading as current.
    echo "  ⓘ live prefs at v$LVER, target v$TARGET  -  migration pending"
    pass "live prefs present, needs migration (v$LVER -> v$TARGET)"
  else
    fail "live prefs has a schemaVersion the schema does not know: $LVER (known: $KNOWN)"
  fi
else
  echo "  ⓘ no live prefs at $LIVE_PREFS  -  ok for fresh install"
  pass "no live prefs (clean setup state)"
fi

# ──────────────────────────────────────────────────────────────────────────
echo ""
echo "══ schema-validation smoke: $PASS passed, $FAIL failed ══"
[ "$FAIL" -eq 0 ]
