#!/usr/bin/env bash
# =====================================================================
# 丰宁夜市跑腿 —— 全项目自检
#   1. PHP / JS / JSON 语法
#   2. API 接口（C 端 / 骑手端 / 后台）
#   3. WAP 页面（C 端 / 骑手端）
#   4. 运营后台页面
#   5. 小程序文件完整性
# 用法: bash tools/smoke_all.sh [BASE]
# =====================================================================
set -u

BASE="${1:-http://localhost:8558}"
API="$BASE/api/index.php"
# 运行时路径统一探测，不写死版本号（环境变更会导致误报）
source "$(dirname "$0")/_runtime.sh"
PY="$PYTHON"

cd "$(dirname "$0")/.." || exit 1

PASS=0; FAIL=0
ok()  { printf '  \033[32mPASS\033[0m  %s\n' "$1"; PASS=$((PASS+1)); }
bad() { printf '  \033[31mFAIL\033[0m  %s  %s\n' "$1" "$2"; FAIL=$((FAIL+1)); }
sec() { printf '\n\033[1m[%s]\033[0m %s\n' "$1" "$2"; }

jget() { "$PY" -c "import sys,json;d=json.load(sys.stdin);print(d$1)" 2>/dev/null; }

# 拼 URL：已有 ? 时用 & 连接，否则用 ?
uadd() { case "$1" in *\?*) echo "$1&$2";; *) echo "$1?$2";; esac; }

echo "=============================================="
echo " 丰宁夜市跑腿 · 全项目自检"
echo " BASE=$BASE"
echo "=============================================="

# ---------------------------------------------------------------------
# 先确认运行时可用。否则后面每一项都会失败，堆出一屏假失败，
# 反而掩盖真正的问题（曾因 node 目录改名导致 46 项误报）。
sec 0 "运行时"
RUNTIME_OK=1
for pair in "Node:$NODE" "PHP:$PHP" "Python:$PYTHON" "MySQL:$MYSQL"; do
  name="${pair%%:*}"; path="${pair#*:}"
  if [ -f "$path" ] || command -v "$path" >/dev/null 2>&1; then
    ok "$name  $path"
  else
    bad "$name" "找不到：$path"
    RUNTIME_OK=0
  fi
done
if [ "$RUNTIME_OK" -eq 0 ]; then
  echo
  echo "运行时缺失，后续检查无意义，已中止。"
  echo "检查 tools/_runtime.sh 的探测规则。"
  exit 2
fi

# ---------------------------------------------------------------------
sec 1 "PHP 语法"
n=0; e=0
for f in $(find api wap admin -name "*.php" | sort); do
  if "$PHP" -l "$f" 2>&1 | grep -q "No syntax errors"; then n=$((n+1)); else bad "$f" "语法错误"; e=$((e+1)); fi
done
[ "$e" -eq 0 ] && ok "$n 个 PHP 文件"

sec 2 "JS 语法"
n=0; e=0
for f in $(find wap/assets/js admin/assets/js miniprogram -name "*.js" | sort); do
  if "$NODE" --check "$f" >/dev/null 2>&1; then n=$((n+1)); else bad "$f" "语法错误"; e=$((e+1)); fi
done
[ "$e" -eq 0 ] && ok "$n 个 JS 文件"

sec 2.5 "密码服务（MD5 → bcrypt 透明升级）"
R=$("$PHP" tools/test_password.php 2>&1 | tail -3 | grep -oE '[0-9]+ (通过|失败)')
P1=$(printf '%s' "$R" | grep -oE '[0-9]+ 通过' | grep -oE '[0-9]+')
F1=$(printf '%s' "$R" | grep -oE '[0-9]+ 失败' | grep -oE '[0-9]+')
if [ "${F1:-1}" = "0" ]; then
  ok "密码服务 $P1 项断言全部通过"
else
  bad "密码服务" "$P1 通过 / $F1 失败（运行 php tools/test_password.php 看详情）"
fi

sec 2.6 "设计令牌一致性"
# 三端共用一套「灯下烟火」设计语言。这里防的是两类漂移：
#   1. 引用了不存在的令牌（改令牌名但忘改引用 → 样式静默失效）
#   2. 三端主色不一致（各改各的 → 用户觉得是两个产品）

# 1) WAP 内部：引用的令牌是否都有定义
grep -rhoE 'var\(--[a-z0-9-]+' wap/assets/css/*.css | sed 's/var(//' | sort -u > ./.tok_used
grep -oE '^[[:space:]]+--[a-z0-9-]+' wap/assets/css/tokens.css | tr -d ' ' | sort -u > ./.tok_def
MISSING=$(comm -23 ./.tok_used ./.tok_def | tr '\n' ' ')
if [ -z "$MISSING" ]; then
  ok "WAP 无失效令牌引用"
else
  bad "失效令牌" "$MISSING"
fi
rm -f ./.tok_used ./.tok_def

# 2) 三端主色是否一致
C_WAP=$(grep -oE '\-\-primary:[[:space:]]*#[0-9A-Fa-f]{6}' wap/assets/css/tokens.css | head -1 | grep -oE '#[0-9A-Fa-f]{6}')
C_ADM=$(grep -oE '\-\-primary:[[:space:]]*#[0-9A-Fa-f]{6}' admin/assets/css/admin.css | head -1 | grep -oE '#[0-9A-Fa-f]{6}')
C_MP=$(grep -oE '\-\-primary:[[:space:]]*#[0-9A-Fa-f]{6}' miniprogram/app.wxss | head -1 | grep -oE '#[0-9A-Fa-f]{6}')
if [ "$C_WAP" = "$C_ADM" ] && [ "$C_WAP" = "$C_MP" ]; then
  ok "三端主色一致（$C_WAP）"
else
  bad "主色不一致" "WAP=$C_WAP 后台=$C_ADM 小程序=$C_MP"
fi

# 3) 背景色是否为暖白（冷灰底会让食物图发灰）
B_WAP=$(grep -oE '\-\-bg:[[:space:]]*#[0-9A-Fa-f]{6}' wap/assets/css/tokens.css | head -1 | grep -oE '#[0-9A-Fa-f]{6}')
if [ "$B_WAP" = "#FBF7F3" ]; then
  ok "WAP 背景为暖白（$B_WAP）"
else
  bad "背景色" "应为 #FBF7F3，实际 $B_WAP"
fi

# 4) 是否还有硬编码的旧冷色残留
#    注意要排除注释行 —— 设计说明里会刻意提到旧色（如「冷灰底 #F4F5F7 会让食物图发灰」），
#    那是文档不是残留，误报会让人不再信任这条检查。
OLD=$(grep -rnE '#(F4F5F7|1A1D1F|FF5B29|EFF1F3)' wap/ admin/ miniprogram/ 2>/dev/null \
      | grep -vE ':[[:space:]]*(\*|/\*|//)' \
      | grep -v '\.md:' \
      | cut -d: -f1 | sort -u | tr '\n' ' ')
if [ -z "$OLD" ]; then
  ok "无旧冷色残留（已排除注释）"
else
  bad "旧色残留" "$OLD"
fi

sec 3 "JSON 校验"
r=$("$PY" -c "
import json, glob, io
bad = []
for f in glob.glob('miniprogram/**/*.json', recursive=True) + glob.glob('*.json'):
    try: json.load(io.open(f, encoding='utf-8'))
    except Exception as e: bad.append(f)
print(len(bad))
" 2>/dev/null)
[ "$r" = "0" ] && ok "小程序与工程 JSON 全部合法" || bad "JSON" "$r 个文件非法"

# ---------------------------------------------------------------------
sec 4 "API · C 端"
UTOKEN=$(curl -s -X POST "$API/client/auth/wxLogin" -H "Content-Type: application/json" \
  -d '{"code":"smoke_all"}' | jget "['data']['token']")
[ -n "$UTOKEN" ] && ok "微信登录换取 token" || bad "wxLogin" "未取到 token"

UA="Authorization: Bearer $UTOKEN"
for r in "client/config" "client/home" "client/merchant/list?page=1&page_size=5" \
         "client/merchant/detail?merchant_id=1" "client/goods/detail?goods_id=1" \
         "client/coupon/available" "client/search?keyword=%E7%83%A7%E7%83%A4"; do
  c=$(curl -s "$API/$r" -H "$UA" | jget "['code']")
  [ "$c" = "0" ] && ok "$r" || bad "$r" "code=$c"
done
for r in "client/cart/list" "client/address/list" "client/order/list?page=1&page_size=5" "client/user/info" \
         "client/delivery/zones" "client/goods/detail?goods_id=1"; do
  c=$(curl -s "$API/$r" -H "$UA" | jget "['code']")
  [ "$c" = "0" ] && ok "$r" || bad "$r" "code=$c"
done

sec 5 "API · 骑手端"
RTOKEN=$(curl -s -X POST "$API/rider/login" -H "Content-Type: application/json" \
  -d '{"phone":"13800000201","password":"123456"}' | jget "['data']['token']")
[ -n "$RTOKEN" ] && ok "骑手登录" || bad "rider/login" "未取到 token"

RA="Authorization: Bearer $RTOKEN"
for r in "rider/info" "rider/setting" "rider/hall/list?page=1&page_size=5" \
         "rider/hall/count" "rider/hall/pending" "rider/task/list?tab=pick" \
         "rider/wallet" "rider/wallet/log?page=1&page_size=5" \
         "rider/withdraw/list" "rider/help"; do
  c=$(curl -s "$API/$r" -H "$RA" | jget "['code']")
  [ "$c" = "0" ] && ok "$r" || bad "$r" "code=$c"
done

sec 6 "API · 运营后台"
ATOKEN=$(curl -s -X POST "$API/admin/login" -H "Content-Type: application/json" \
  -d '{"username":"admin","password":"admin888"}' | jget "['data']['token']")
[ -n "$ATOKEN" ] && ok "管理员登录" || bad "admin/login" "未取到 token"

AA="Authorization: Bearer $ATOKEN"
for r in "admin/info" "admin/dispatch/board" "admin/dispatch/orders?page=1&page_size=5" \
         "admin/dispatch/riders" "admin/dispatch/mapData" \
         "admin/dispatch/logs?page=1&page_size=5" "admin/dispatch/config" \
         "admin/merchant/list?page=1&page_size=3" "admin/merchant/tags" "admin/merchant/options" \
         "admin/goods/list?page=1&page_size=3" "admin/goods/detail?id=1" "admin/goods/categories" \
         "admin/order/list?page=1&page_size=3" "admin/order/detail?id=1" "admin/order/stat?days=7" \
         "admin/rider/list?page=1&page_size=3" "admin/rider/detail?id=1" "admin/rider/options" \
         "admin/user/list?page=1&page_size=3" "admin/user/detail?id=1" \
         "admin/coupon/list" "admin/banner/list" "admin/todo/count" \
         "admin/comment/list?page=1&page_size=3" "admin/aftersale/list" "admin/withdraw/list" \
         "admin/stat/overview" "admin/stat/trend?days=7" "admin/stat/merchantRank" \
         "admin/stat/riderRank" "admin/stat/goodsRank" \
         "admin/system/config" "admin/system/admins" "admin/system/roles" \
         "admin/delivery/zones" "admin/dispatch/mode"; do
  c=$(curl -s "$API/$r" -H "$AA" | jget "['code']")
  [ "$c" = "0" ] && ok "$r" || bad "$r" "code=$c"
done

# 运营账号权限验证（应能进入调度大厅）
YTOKEN=$(curl -s -X POST "$API/admin/login" -H "Content-Type: application/json" \
  -d '{"username":"yunying","password":"yunying123"}' | jget "['data']['token']")
c=$(curl -s "$API/admin/dispatch/board" -H "Authorization: Bearer $YTOKEN" | jget "['code']")
[ "$c" = "0" ] && ok "运营账号可访问调度大厅" || bad "运营账号权限" "code=$c"

# 无 token 应被拦截
c=$(curl -s "$API/admin/dispatch/board" | jget "['code']")
[ "$c" = "401" ] && ok "无 token 被正确拦截 (401)" || bad "鉴权" "code=$c"

# ---------------------------------------------------------------------
sec 7 "WAP · C 端页面"
for u in "index.php" "market.php" "shop.php?id=1" "search.php?keyword=%E7%BE%8A" \
         "cart.php" "mine.php" "login.php" "order-list.php" "about.php"; do
  H=$(curl -s "$BASE/wap/$u")
  if printf '%s' "$H" | grep -q 'Fatal error\|Warning:\|Notice:'; then
    bad "$u" "PHP 报错"
  else
    ok "$u  ($(printf '%s' "$H" | wc -c) 字节)"
  fi
done

sec 8 "WAP · 需登录页面"
for u in "address.php" "coupon.php?tab=center" "aftersale.php" "confirm.php?merchant_id=1"; do
  H=$(curl -s "$(uadd "$BASE/wap/$u" "token=$UTOKEN")")
  if printf '%s' "$H" | grep -q 'Fatal error\|Warning:\|Notice:'; then
    bad "$u" "PHP 报错"
  else
    ok "$u  ($(printf '%s' "$H" | wc -c) 字节)"
  fi
done

sec 9 "WAP · 骑手端页面"
for u in "index.php" "task.php" "task.php?tab=delivering" "wallet.php" "withdraw.php" \
         "mine.php" "help.php" "password.php"; do
  H=$(curl -s "$(uadd "$BASE/wap/rider/$u" "token=$RTOKEN")")
  if printf '%s' "$H" | grep -q 'Fatal error\|Warning:\|Notice:'; then
    bad "$u" "PHP 报错"
  else
    ok "$u  ($(printf '%s' "$H" | wc -c) 字节)"
  fi
done

sec 10 "运营后台页面"
for u in "index.php" "dashboard.php" "logs.php" "config.php" \
         "order.php" "merchant.php" "goods.php" "rider.php" "user.php" \
         "coupon.php" "banner.php" "comment.php" "aftersale.php" "withdraw.php" \
         "stat.php" "system.php"; do
  H=$(curl -s "$BASE/admin/$u")
  if printf '%s' "$H" | grep -q 'Fatal error\|Warning:\|Notice:'; then
    bad "$u" "PHP 报错"
  else
    ok "$u  ($(printf '%s' "$H" | wc -c) 字节)"
  fi
done

# ---------------------------------------------------------------------
# ---------------------------------------------------------------------
sec 10.4 "调度大厅数据口径"
# 顶栏「待处理」数字与主区「需要处理」列表必须一致 ——
# 曾经因为两处用了不同的判定条件，顶栏显示 0 而主区显示 15，
# 运营会以为系统坏了。
B=$(curl -s "$BASE/api/index.php/admin/dispatch/board" -H "Authorization: Bearer $ATOKEN" \
     | "$PY" -c "import sys,json;print(json.load(sys.stdin)['data']['need_manual'])" 2>/dev/null)
T=$(curl -s "$BASE/api/index.php/admin/dispatch/todo" -H "Authorization: Bearer $ATOKEN" \
     | "$PY" -c "import sys,json;print(json.load(sys.stdin)['data']['count'])" 2>/dev/null)
if [ "$B" = "$T" ]; then
  ok "顶栏待处理($B) 与主区待处理($T) 口径一致"
else
  bad "待处理口径不一致" "顶栏=$B 主区=$T"
fi

# 待处理列表必须带推荐骑手字段（前端靠它做一键派单）
HAS_CAND=$(curl -s "$BASE/api/index.php/admin/dispatch/todo?cand_limit=2" -H "Authorization: Bearer $ATOKEN" \
     | "$PY" -c "
import sys,json
d=json.load(sys.stdin)['data']['list']
print('yes' if (not d or 'candidates' in d[0]) else 'no')" 2>/dev/null)
if [ "$HAS_CAND" = "yes" ]; then
  ok "待处理列表含推荐骑手字段"
else
  bad "待处理列表" "缺少 candidates 字段"
fi

sec 10.5 "登录态贯通（回归：登录后跳转丢失身份）"
# 现象：登录提示成功但不跳转 —— 因为 token 只在 localStorage，
#       服务端页面从 Cookie/URL 读身份，不带过去会被弹回登录页。
# 这里验证「带 token 跳转 → 服务端写 Cookie → 后续仅靠 Cookie 可访问」。
CJAR="./.smoke_c_cookie.txt"; RJAR="./.smoke_r_cookie.txt"
rm -f "$CJAR" "$RJAR"

# C 端
curl -s -c "$CJAR" -o /dev/null "$BASE/wap/index.php?token=$UTOKEN"
c=$(curl -s -b "$CJAR" -o /dev/null -w '%{http_code}' "$BASE/wap/address.php")
[ "$c" = "200" ] && ok "C 端：带 token 跳转后靠 Cookie 可访问需登录页" \
                 || bad "C 端登录态贯通" "HTTP $c"

c=$(curl -s -o /dev/null -w '%{http_code}' "$BASE/wap/address.php")
[ "$c" = "302" ] && ok "C 端：无身份访问被正确拦截 (302)" \
                 || bad "C 端未登录拦截" "HTTP $c"

# 骑手端
curl -s -c "$RJAR" -o /dev/null "$BASE/wap/rider/index.php?token=$RTOKEN"
c=$(curl -s -b "$RJAR" -o /dev/null -w '%{http_code}' "$BASE/wap/rider/index.php")
[ "$c" = "200" ] && ok "骑手端：带 token 跳转后靠 Cookie 可访问接单大厅" \
                 || bad "骑手端登录态贯通" "HTTP $c"

c=$(curl -s -o /dev/null -w '%{http_code}' "$BASE/wap/rider/index.php")
[ "$c" = "302" ] && ok "骑手端：无身份访问被正确拦截 (302)" \
                 || bad "骑手端未登录拦截" "HTTP $c"

# 两个身份互不覆盖
n1=$(grep -c 'fn_token' "$CJAR" 2>/dev/null || echo 0)
n2=$(grep -c 'fn_rider_token' "$RJAR" 2>/dev/null || echo 0)
if [ "$n1" -ge 1 ] && [ "$n2" -ge 1 ]; then
  ok "用户与骑手使用独立 Cookie（互不覆盖）"
else
  bad "身份隔离" "用户 $n1 条 / 骑手 $n2 条"
fi

# 两个 Cookie 同时存在时，各自页面都能正常打开
c=$(curl -s -b "$CJAR" -b "$RJAR" -o /dev/null -w '%{http_code}' "$BASE/wap/mine.php")
d=$(curl -s -b "$CJAR" -b "$RJAR" -o /dev/null -w '%{http_code}' "$BASE/wap/rider/index.php")
if [ "$c" = "200" ] && [ "$d" = "200" ]; then
  ok "同时持有两种身份时，各自页面均正常"
else
  bad "双身份并存" "用户页 $c / 骑手页 $d"
fi

rm -f "$CJAR" "$RJAR"

# ---------------------------------------------------------------------
sec 11 "小程序完整性"
MISS=0
for f in app.js app.json app.wxss sitemap.json project.config.json \
         utils/config.js \
         pages/login/index.js pages/login/index.json pages/login/index.wxml pages/login/index.wxss \
         pages/webview/index.js pages/webview/index.json pages/webview/index.wxml pages/webview/index.wxss \
         pages/pay/index.js pages/pay/index.json pages/pay/index.wxml pages/pay/index.wxss; do
  [ -f "miniprogram/$f" ] || { bad "miniprogram/$f" "缺失"; MISS=$((MISS+1)); }
done
[ "$MISS" -eq 0 ] && ok "小程序 18 个文件齐全"

# 登录页必须包含 getPhoneNumber 与协议勾选（审核硬要求）
if grep -q "getPhoneNumber" miniprogram/pages/login/index.wxml; then
  ok "登录页使用 getPhoneNumber 获取手机号"
else
  bad "登录页" "缺少 getPhoneNumber"
fi
if grep -q "用户协议" miniprogram/pages/login/index.wxml; then
  ok "登录页含协议勾选（审核要求）"
else
  bad "登录页" "缺少协议勾选"
fi

# ---------------------------------------------------------------------
sec 12 "数据库"
T=$("$MYSQL" -ufner -pyuan1314 -N -e \
  "SELECT COUNT(*) FROM information_schema.TABLES WHERE TABLE_SCHEMA='fner';" 2>/dev/null | tr -d '\r\n')
[ "$T" = "37" ] && ok "37 张表" || bad "表数量" "实际 $T（预期 37）"
C=$("$MYSQL" -ufner -pyuan1314 -N -e \
  "SELECT COUNT(*) FROM fner.dispatch_config;" 2>/dev/null | tr -d '\r\n')
[ "$C" = "26" ] && ok "26 项调度参数" || bad "调度参数" "实际 $C（预期 26）"

echo
echo "=============================================="
printf " 结果: \033[32m%d 通过\033[0m  \033[31m%d 失败\033[0m\n" "$PASS" "$FAIL"
echo "=============================================="
[ "$FAIL" -eq 0 ] && exit 0 || exit 1
