#!/usr/bin/env bash
# =============================================================================
# CrimsonGate LAB TEST — maximum offline / non-destructive probes
# Does NOT install services, does NOT change sshd/nginx system configs permanently.
# Goal: surface real problems before production install.
#
# Usage:
#   bash crimson-lab-test.sh
#   CRIMSON_SCRIPT=/path/to/crimsongate-1.5.1.sh bash crimson-lab-test.sh
# =============================================================================
set -uo pipefail
export LC_ALL=C.UTF-8 2>/dev/null || true

SCRIPT="${CRIMSON_SCRIPT:-}"
if [ -z "$SCRIPT" ]; then
  for c in \
    "$HOME/crimsongate-1.5.1.sh" \
    "$HOME/crimsongate.sh" \
    ./crimsongate-1.5.1.sh \
    ./crimsongate.sh \
    /home/workdir/artifacts/crimsongate-1.5.1.sh \
    /home/workdir/artifacts/crimsongate-1.5.0.sh
  do
    [ -f "$c" ] && SCRIPT=$c && break
  done
fi

XRAY_BIN="${XRAY_BIN:-}"
[ -x "$XRAY_BIN" ] || XRAY_BIN=/tmp/xraytest/xray
[ -x "$XRAY_BIN" ] || XRAY_BIN=$(command -v xray 2>/dev/null || true)

WORK=$(mktemp -d /tmp/crimson-lab.XXXXXX)
trap 'rm -rf "$WORK"' EXIT

PASS=0
FAIL=0
WARN=0
SKIP=0

R=$'\e[31m'; G=$'\e[32m'; Y=$'\e[33m'; C=$'\e[36m'; W=$'\e[1m'; N=$'\e[0m'
[ -t 1 ] || { R=; G=; Y=; C=; W=; N=; }

ok()   { printf '%s  ✓%s %s\n' "$G" "$N" "$*"; PASS=$((PASS+1)); }
bad()  { printf '%s  ✗%s %s\n' "$R" "$N" "$*"; FAIL=$((FAIL+1)); }
wrn()  { printf '%s  !%s %s\n' "$Y" "$N" "$*"; WARN=$((WARN+1)); }
skp()  { printf '%s  ·%s %s\n' "$C" "$N" "$*"; SKIP=$((SKIP+1)); }
sec()  { printf '\n%s══ %s ══%s\n' "$W" "$*" "$N"; }

need() { command -v "$1" >/dev/null 2>&1; }

# -----------------------------------------------------------------------------
sec "0. Environment"
# -----------------------------------------------------------------------------
echo "  host     : $(uname -srm)"
echo "  script   : ${SCRIPT:-NOT FOUND}"
echo "  xray bin : ${XRAY_BIN:-NOT FOUND}"
echo "  work dir : $WORK"

need bash && ok "bash present: $(bash --version | head -1)" || bad "bash missing"
need openssl && ok "openssl present" || bad "openssl missing"
if need curl; then ok "curl present"
elif need wget; then ok "wget present (curl missing — OK for lab)"
else wrn "neither curl nor wget — cannot auto-fetch xray"
fi
need python3 && ok "python3 present: $(python3 --version 2>&1)" || bad "python3 missing"
need jq && ok "jq present" || wrn "jq missing (some tests limited)"
need nginx && ok "nginx present" || skp "nginx not installed (skip nginx -t live)"
need certbot && ok "certbot present" || skp "certbot not installed (skip live LE)"

# Auto-fetch Xray into /tmp for lab only (non-destructive)
if [ ! -x "$XRAY_BIN" ]; then
  arch=$(uname -m)
  case "$arch" in
    x86_64|amd64) xa=64 ;;
    aarch64|arm64) xa=arm64-v8a ;;
    *) xa=64 ;;
  esac
  url="https://github.com/XTLS/Xray-core/releases/download/v25.9.11/Xray-linux-${xa}.zip"
  mkdir -p /tmp/xraytest
  if need curl; then
    curl -fsSL "$url" -o /tmp/xraytest/x.zip 2>/dev/null || true
  elif need wget; then
    wget -qO /tmp/xraytest/x.zip "$url" 2>/dev/null || true
  fi
  if [ -f /tmp/xraytest/x.zip ]; then
    (cd /tmp/xraytest && unzip -qo x.zip xray 2>/dev/null || unzip -qo x.zip 2>/dev/null) || true
    chmod +x /tmp/xraytest/xray 2>/dev/null || true
    [ -x /tmp/xraytest/xray ] && XRAY_BIN=/tmp/xraytest/xray
  fi
fi
if [ -x "$XRAY_BIN" ]; then
  ok "xray binary: $($XRAY_BIN version 2>/dev/null | head -1)"
else
  wrn "xray binary not found — protocol matrix skipped"
fi

if [ -f /etc/os-release ]; then
  # shellcheck disable=SC1091
  . /etc/os-release
  echo "  os       : ${PRETTY_NAME:-unknown}"
  case "${VERSION_ID:-}" in
    22.04|24.04|12) ok "OS in CrimsonGate support list" ;;
    18.04|20.04)
      wrn "OS $VERSION_ID older than CrimsonGate support (need 22.04/24.04/Debian 12)"
      wrn "Lab probes tools only — full install NOT recommended here"
      ;;
    *) wrn "OS ${VERSION_ID:-?} not in official support list — lab only" ;;
  esac
fi

# -----------------------------------------------------------------------------
sec "1. CrimsonGate script static checks"
# -----------------------------------------------------------------------------
if [ ! -f "$SCRIPT" ]; then
  wrn "CrimsonGate script file not found — static checks skipped"
  wrn "Place crimsongate-1.5.1.sh in \$HOME or set CRIMSON_SCRIPT=/path/to/script"
else
  if bash -n "$SCRIPT" 2>"$WORK/syntax.err"; then
    ok "bash -n syntax OK ($(wc -l < "$SCRIPT") lines)"
  else
    bad "bash -n FAILED"
    sed 's/^/    /' "$WORK/syntax.err" | head -20
  fi

  # Critical symbols must exist
  for fn in \
    write_xray_config_initial issue_cert setup_cert_auto_renew write_nginx_full \
    main_menu self_test deep_audit lock_profile_to_server mklink cfg_apply \
    validate_xray_config_file gen_tls_cert do_install
  do
    if grep -qE "^${fn}\\(\\)" "$SCRIPT"; then
      ok "function defined: $fn"
    else
      bad "function MISSING: $fn"
    fi
  done

  VER=$(grep -E '^VERSION=' "$SCRIPT" | head -1 | cut -d= -f2 | tr -d '"')
  [ -n "$VER" ] && ok "VERSION=$VER" || bad "VERSION not set"

  grep -q 'listen": "127.0.0.1"' "$SCRIPT" || grep -q 'listen.: .127.0.0.1' "$SCRIPT" \
    && ok "Xray template binds 127.0.0.1" || bad "localhost bind not found in template"

  if grep -q 'Remote Proxy/SNI/Host' "$SCRIPT" && ! grep -q 'removed in v1.4' "$SCRIPT"; then
    wrn "old Client Profile menu string still present"
  else
    ok "Client Profile interactive path not in main menu (1.4+)"
  fi

  if grep -q 'cert_badge_plain\|cert_badge_color' "$SCRIPT"; then
    ok "certificate ONLINE/OFFLINE badges present"
  else
    wrn "cert badges missing"
  fi
fi

# -----------------------------------------------------------------------------
sec "2. Protocol config matrix (xray run -test)"
# -----------------------------------------------------------------------------
xray_test() {
  local name=$1 file=$2
  if [ ! -x "$XRAY_BIN" ]; then
    skp "skip $name (no xray)"
    return
  fi
  if "$XRAY_BIN" run -test -format json -config "$file" >"$WORK/xray-$name.log" 2>&1; then
    ok "xray -test OK: $name"
  else
    bad "xray -test FAIL: $name"
    tail -n 8 "$WORK/xray-$name.log" | sed 's/^/    /'
  fi
}

UUID=$(cat /proc/sys/kernel/random/uuid 2>/dev/null || echo "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee")
SSPASS=$(openssl rand -base64 32 2>/dev/null || echo "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=")
TPASS="trojan-lab-password-32chars!!"

# Individual protocols
for proto in vless vmess trojan ss; do
  case $proto in
    vless)
      cat > "$WORK/$proto.json" <<EOF
{"log":{"loglevel":"error"},"inbounds":[{"tag":"vless-ws","listen":"127.0.0.1","port":11000,"protocol":"vless","settings":{"clients":[{"id":"$UUID","level":0}],"decryption":"none"},"streamSettings":{"network":"ws","security":"none","wsSettings":{"path":"/lab-vless"}}}],"outbounds":[{"protocol":"freedom","tag":"direct"}]}
EOF
      ;;
    vmess)
      cat > "$WORK/$proto.json" <<EOF
{"log":{"loglevel":"error"},"inbounds":[{"tag":"vmess-ws","listen":"127.0.0.1","port":11001,"protocol":"vmess","settings":{"clients":[{"id":"$UUID","alterId":0,"security":"auto","level":0}]},"streamSettings":{"network":"ws","security":"none","wsSettings":{"path":"/lab-vmess"}}}],"outbounds":[{"protocol":"freedom","tag":"direct"}]}
EOF
      ;;
    trojan)
      cat > "$WORK/$proto.json" <<EOF
{"log":{"loglevel":"error"},"inbounds":[{"tag":"trojan-ws","listen":"127.0.0.1","port":11002,"protocol":"trojan","settings":{"clients":[{"password":"$TPASS","level":0}]},"streamSettings":{"network":"ws","security":"none","wsSettings":{"path":"/lab-trojan"}}}],"outbounds":[{"protocol":"freedom","tag":"direct"}]}
EOF
      ;;
    ss)
      cat > "$WORK/$proto.json" <<EOF
{"log":{"loglevel":"error"},"inbounds":[{"tag":"ss-ws","listen":"127.0.0.1","port":11003,"protocol":"shadowsocks","settings":{"method":"2022-blake3-aes-256-gcm","password":"$SSPASS","network":"tcp"},"streamSettings":{"network":"ws","security":"none","wsSettings":{"path":"/lab-ss"}}}],"outbounds":[{"protocol":"freedom","tag":"direct"}]}
EOF
      ;;
  esac
  xray_test "$proto-ws" "$WORK/$proto.json"
done

# Full stack (all four like CrimsonGate)
cat > "$WORK/full.json" <<EOF
{
  "log": {"loglevel": "error"},
  "inbounds": [
    {"tag":"vless-ws","listen":"127.0.0.1","port":10000,"protocol":"vless","settings":{"clients":[{"id":"$UUID","level":0}],"decryption":"none"},"streamSettings":{"network":"ws","security":"none","wsSettings":{"path":"/assets/js/chunk.vless.js"}}},
    {"tag":"vmess-ws","listen":"127.0.0.1","port":10001,"protocol":"vmess","settings":{"clients":[{"id":"$UUID","alterId":0,"security":"auto","level":0}]},"streamSettings":{"network":"ws","security":"none","wsSettings":{"path":"/assets/js/chunk.vmess.js"}}},
    {"tag":"trojan-ws","listen":"127.0.0.1","port":10002,"protocol":"trojan","settings":{"clients":[{"password":"$TPASS","level":0}]},"streamSettings":{"network":"ws","security":"none","wsSettings":{"path":"/assets/js/chunk.trojan.js"}}},
    {"tag":"ss-ws","listen":"127.0.0.1","port":10003,"protocol":"shadowsocks","settings":{"method":"2022-blake3-aes-256-gcm","password":"$SSPASS","network":"tcp"},"streamSettings":{"network":"ws","security":"none","wsSettings":{"path":"/assets/js/chunk.ss.js"}}}
  ],
  "outbounds": [{"protocol":"freedom","tag":"direct"}]
}
EOF
xray_test "full-crimson-stack" "$WORK/full.json"

# SS cipher variants
for method in 2022-blake3-aes-256-gcm 2022-blake3-chacha20-poly1305 2022-blake3-aes-128-gcm; do
  case $method in
    *aes-128*) k=$(openssl rand -base64 16) ;;
    *) k=$(openssl rand -base64 32) ;;
  esac
  cat > "$WORK/ss-$method.json" <<EOF
{"log":{"loglevel":"error"},"inbounds":[{"listen":"127.0.0.1","port":12003,"protocol":"shadowsocks","settings":{"method":"$method","password":"$k","network":"tcp"},"streamSettings":{"network":"ws","security":"none","wsSettings":{"path":"/ss"}}}],"outbounds":[{"protocol":"freedom"}]}
EOF
  xray_test "ss-$method" "$WORK/ss-$method.json"
done

# Negative: bad UUID should fail or warn — just ensure tool runs
cat > "$WORK/bad-ss-key.json" <<EOF
{"log":{"loglevel":"error"},"inbounds":[{"listen":"127.0.0.1","port":13003,"protocol":"shadowsocks","settings":{"method":"2022-blake3-aes-256-gcm","password":"tooshort","network":"tcp"},"streamSettings":{"network":"ws","security":"none","wsSettings":{"path":"/ss"}}}],"outbounds":[{"protocol":"freedom"}]}
EOF
if [ -x "$XRAY_BIN" ]; then
  if "$XRAY_BIN" run -test -format json -config "$WORK/bad-ss-key.json" >/dev/null 2>&1; then
    wrn "xray accepted short SS2022 key (unexpected; clients may still fail)"
  else
    ok "xray rejects invalid SS2022 key length (good)"
  fi
fi

# -----------------------------------------------------------------------------
sec "3. TLS / certificate tools (offline)"
# -----------------------------------------------------------------------------
openssl req -x509 -newkey rsa:2048 -keyout "$WORK/k.pem" -out "$WORK/c.pem" \
  -days 1 -nodes -subj "/CN=lab.example.com" >/dev/null 2>&1 \
  && ok "openssl self-signed generation works" \
  || bad "openssl self-signed generation failed"

if [ -f "$WORK/c.pem" ]; then
  if openssl x509 -checkend 0 -noout -in "$WORK/c.pem" 2>/dev/null; then
    ok "cert checkend (not expired) works"
  else
    bad "cert checkend failed on fresh cert"
  fi
  # pair check
  h1=$(openssl x509 -noout -modulus -in "$WORK/c.pem" 2>/dev/null | openssl md5)
  h2=$(openssl rsa -noout -modulus -in "$WORK/k.pem" 2>/dev/null | openssl md5)
  [ "$h1" = "$h2" ] && ok "cert/key modulus match" || bad "cert/key mismatch"
fi

# -----------------------------------------------------------------------------
sec "4. SSH-WS bridge Python (syntax + import)"
# -----------------------------------------------------------------------------
if [ -f "$SCRIPT" ]; then
  # Extract embedded bridge if present between markers or file write
  if grep -q 'ssh-ws-bridge' "$SCRIPT"; then
    ok "script references ssh-ws-bridge"
  else
    wrn "ssh-ws-bridge string not found"
  fi
fi
python3 -c "import asyncio,sys; print('pyok')" >/dev/null 2>&1 \
  && ok "python3 asyncio importable" \
  || bad "python3 asyncio broken"

# Minimal raw-upgrade server syntax check (Python 3.6+ — no asyncio.run)
cat > "$WORK/bridge_min.py" <<'PY'
from __future__ import print_function
import asyncio
import sys

async def h(r, w):
    try:
        line = await asyncio.wait_for(r.readline(), timeout=5)
        if not line:
            w.close()
            return
        while True:
            hdr = await asyncio.wait_for(r.readline(), timeout=5)
            if hdr in (b"\r\n", b"\n", b""):
                break
        w.write(b"HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\n\r\n")
        await w.drain()
    finally:
        try:
            w.close()
        except Exception:
            pass

async def main():
    s = await asyncio.start_server(h, "127.0.0.1", 0)
    s.close()
    if sys.version_info >= (3, 7):
        await s.wait_closed()

if sys.version_info >= (3, 7):
    asyncio.run(main())
else:
    loop = asyncio.get_event_loop()
    loop.run_until_complete(main())
    loop.close()
print("bridge-syntax-ok")
PY
if python3 "$WORK/bridge_min.py" >"$WORK/bridge.out" 2>"$WORK/bridge.err"; then
  ok "minimal WS-upgrade bridge logic runs"
else
  bad "minimal bridge failed"
  sed 's/^/    /' "$WORK/bridge.err" | head -10
fi

# -----------------------------------------------------------------------------
sec "5. Nginx config fragment (nginx -t if available)"
# -----------------------------------------------------------------------------
if need nginx; then
  cat > "$WORK/nginx-lab.conf" <<EOF
pid $WORK/nginx.pid;
error_log $WORK/nginx-error.log;
events { worker_connections 64; }
http {
  server {
    listen 127.0.0.1:18080;
    server_name _;
    location = /lab-vless {
      if (\$http_upgrade !~* "websocket") { return 404; }
      proxy_pass http://127.0.0.1:10000;
      proxy_http_version 1.1;
      proxy_set_header Upgrade \$http_upgrade;
      proxy_set_header Connection "upgrade";
      proxy_set_header Host \$host;
    }
    location / { return 204; }
  }
}
EOF
  if nginx -t -c "$WORK/nginx-lab.conf" >"$WORK/nginx-t.out" 2>&1; then
    ok "nginx -t accepts path+Upgrade fragment"
  else
    # Some nginx builds require full absolute paths / user — treat partial fail carefully
    if grep -qi "syntax is ok\|test is successful" "$WORK/nginx-t.out"; then
      ok "nginx syntax ok (warnings possible)"
    else
      wrn "nginx -t did not fully succeed (may need root paths)"
      tail -n 6 "$WORK/nginx-t.out" | sed 's/^/    /'
    fi
  fi
else
  skp "nginx not installed"
fi

# -----------------------------------------------------------------------------
sec "6. Link format sanity (regex only)"
# -----------------------------------------------------------------------------
# Simulated links as CrimsonGate mklink would emit
fake_uuid=$UUID
fake_host="lab.example.com"
path_enc="%2Fassets%2Fjs%2Fchunk.vless.js"
vless="vless://${fake_uuid}@${fake_host}:443?encryption=none&security=tls&sni=${fake_host}&type=ws&host=${fake_host}&path=${path_enc}#lab-VLESS"
trojan="trojan://secret@${fake_host}:443?security=tls&sni=${fake_host}&type=ws&host=${fake_host}&path=${path_enc}#lab-Trojan"
[[ "$vless" =~ ^vless://[a-f0-9-]{36}@[^:]+:[0-9]+\? ]] && ok "vless link shape OK" || bad "vless link shape bad"
[[ "$trojan" =~ ^trojan://[^@]+@[^:]+:[0-9]+\? ]] && ok "trojan link shape OK" || bad "trojan link shape bad"
# ss userinfo must not be blindly assumed — structure only
ss_link="ss://method:pass@${fake_host}:443?type=ws&security=tls&path=${path_enc}#lab-SS"
[[ "$ss_link" =~ ^ss:// ]] && ok "ss link prefix OK" || bad "ss link bad"

# -----------------------------------------------------------------------------
sec "7. Source-load safety (CRIMSON_SOURCE_ONLY)"
# -----------------------------------------------------------------------------
if [ -f "$SCRIPT" ]; then
  if CRIMSON_SOURCE_ONLY=1 bash -c "source \"$SCRIPT\"; echo SOURCED_OK" >"$WORK/source.out" 2>"$WORK/source.err"; then
    if grep -q SOURCED_OK "$WORK/source.out"; then
      ok "CRIMSON_SOURCE_ONLY source loads without running install"
    else
      wrn "source ran but marker missing"
    fi
  else
    # Some scripts exit on source — not always fatal
    wrn "source with CRIMSON_SOURCE_ONLY returned non-zero"
    tail -n 5 "$WORK/source.err" | sed 's/^/    /'
  fi
fi

# -----------------------------------------------------------------------------
sec "RESULT"
# -----------------------------------------------------------------------------
printf '\n'
printf '  %sPASS%s=%s  %sFAIL%s=%s  %sWARN%s=%s  %sSKIP%s=%s\n' \
  "$G" "$N" "$PASS" "$R" "$N" "$FAIL" "$Y" "$N" "$WARN" "$C" "$N" "$SKIP"
if [ "$FAIL" -eq 0 ]; then
  printf '\n  %sALL CRITICAL LAB CHECKS PASSED.%s\n' "$G" "$N"
  printf '  This does NOT replace live install + crimson test / deep-test on a VPS.\n\n'
  exit 0
else
  printf '\n  %s%d FAILURE(S) — review above before production install.%s\n\n' "$R" "$FAIL" "$N"
  exit 1
fi

