#!/usr/bin/env bash
# test-suite.sh — Suite de pruebas automáticas para la migración SF5.4
#
# Módulos:
#   1. lint     — Twig + YAML + mapeos Doctrine
#   2. ajax     — Endpoints GET que devuelven JSON
#   3. post     — Formularios Symfony (GET form → extraer CSRF → POST vacío → esperar no-500)
#   4. smoke    — GET de todas las rutas (delega a smoke-test.sh)
#
# Uso:
#   ./scripts/test-suite.sh                  # todos los módulos
#   ./scripts/test-suite.sh --only lint      # solo lint
#   ./scripts/test-suite.sh --only ajax      # solo AJAX
#   ./scripts/test-suite.sh --only post      # solo POST
#   ./scripts/test-suite.sh --only smoke     # solo smoke GET
#   ./scripts/test-suite.sh --skip smoke     # todos menos smoke
#   ./scripts/test-suite.sh --verbose        # muestra rutas OK también
#
# Salida: exit 0 = todo OK, exit 1 = hay errores

set -euo pipefail

BASE_URL="${MBINV_URL:-http://127.0.0.1:8099}"
PHP="${MBINV_PHP:-/usr/local/opt/php@8.2/bin/php}"
PROJECT_DIR="$(cd "$(dirname "$0")/.." && pwd)"
VERBOSE=0
ONLY=""
SKIP=""

for arg in "$@"; do
  case $arg in
    --verbose)    VERBOSE=1 ;;
    --only=*)     ONLY="${arg#--only=}" ;;
    --skip=*)     SKIP="${arg#--skip=}" ;;
    --url=*)      BASE_URL="${arg#--url=}" ;;
  esac
done

should_run() {
  local mod=$1
  [[ -n "$ONLY" ]] && [[ "$ONLY" != "$mod" ]] && return 1
  [[ -n "$SKIP" ]] && [[ "$SKIP" == "$mod" ]] && return 1
  return 0
}

RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'
CYAN='\033[0;36m'; BOLD='\033[1m'; RESET='\033[0m'

log()  { echo -e "${CYAN}[suite]${RESET} $*"; }
pass() { echo -e "  ${GREEN}✓${RESET} $*"; }
fail() { echo -e "  ${RED}✗${RESET} $*"; }
warn() { echo -e "  ${YELLOW}!${RESET} $*"; }

ERRORS=0

echo ""
echo -e "${BOLD}╔══════════════════════════════════════════════╗${RESET}"
echo -e "${BOLD}║      MBInv — Test Suite  (SF 5.4 / PHP 8.2)  ║${RESET}"
echo -e "${BOLD}╚══════════════════════════════════════════════╝${RESET}"
echo -e "  URL: ${CYAN}${BASE_URL}${RESET}"
echo ""

# ─────────────────────────────────────────────────────────────────────────────
# MÓDULO 1: LINT
# ─────────────────────────────────────────────────────────────────────────────
if should_run lint; then
  echo -e "${BOLD}── 1. Análisis estático ─────────────────────────────────────────${RESET}"

  # 1a. Twig
  twig_out=$("$PHP" "${PROJECT_DIR}/bin/console" lint:twig \
    src/MB/ProductoBundle/Resources/views/ \
    app/Resources/views/ 2>&1) || true
  twig_errors=$(echo "$twig_out"    | awk '/ERROR/{c++} END{print c+0}')
  twig_ok=$(echo "$twig_out"        | awk 'match($0,/[0-9]+ Twig/){n=substr($0,RSTART,RLENGTH); sub(/ Twig/,"",n); print n; exit}')
  twig_ok=${twig_ok:-?}
  # reportes.html.twig usa sintaxis custom {%ui_tabs%} y no tiene controller que la renderice
  reportes_errors=$(echo "$twig_out" | awk '/ERROR.*reportes\.html\.twig/{c++} END{print c+0}')
  real_errors=$(( twig_errors - reportes_errors ))

  if [[ $real_errors -eq 0 ]]; then
    pass "Twig: ${twig_ok} archivos OK (1 ignorado: código muerto)"
  else
    fail "Twig: ${real_errors} errores reales"
    echo "$twig_out" | grep "ERROR" | grep -v "reportes.html.twig" | sed 's/^/    /'
    ERRORS=$(( ERRORS + real_errors ))
  fi

  # 1b. YAML
  yaml_out=$("$PHP" "${PROJECT_DIR}/bin/console" lint:yaml config/ 2>&1) || true
  if echo "$yaml_out" | grep -q "valid syntax"; then
    yaml_ok=$(echo "$yaml_out" | awk 'match($0,/[0-9]+ YAML/){n=substr($0,RSTART,RLENGTH); sub(/ YAML/,"",n); print n; exit}')
  yaml_ok=${yaml_ok:-?}
    pass "YAML: ${yaml_ok} archivos OK"
  else
    fail "YAML: errores de sintaxis"
    echo "$yaml_out" | grep -v "OK" | sed 's/^/    /'
    ERRORS=$(( ERRORS + 1 ))
  fi

  # 1c. Mapeos Doctrine (solo mapping, no schema sync)
  mapping_out=$("$PHP" "${PROJECT_DIR}/bin/console" doctrine:mapping:info 2>&1) || true
  mapping_errors=$(echo "$mapping_out" | awk 'tolower($0) ~ /\[fail\]/{c++} END{print c+0}')
  entity_count=$(echo "$mapping_out"  | awk 'match($0,/Found [0-9]+/){n=substr($0,RSTART,RLENGTH); sub(/Found /,"",n); print n; exit}')
  entity_count=${entity_count:-?}
  if [[ "$mapping_errors" -eq 0 ]]; then
    pass "Doctrine: mapeos OK (${entity_count} entidades)"
  else
    fail "Doctrine: ${mapping_errors} errores de mapeo"
    echo "$mapping_out" | grep -iE "\[FAIL\]|error" | sed 's/^/    /'
    ERRORS=$(( ERRORS + mapping_errors ))
  fi

  echo ""
fi

# ─────────────────────────────────────────────────────────────────────────────
# LOGIN (requerido para módulos ajax, post, smoke)
# ─────────────────────────────────────────────────────────────────────────────
COOKIE_JAR=""
if should_run ajax || should_run post || should_run smoke; then
  COOKIE_JAR="$(mktemp /tmp/suite-cookies.XXXXXX)"
  trap 'rm -f "$COOKIE_JAR"' EXIT

  log "Autenticando en ${BASE_URL}..."
  hora=$("$PHP" -r "date_default_timezone_set('America/Guatemala'); echo (new DateTime())->format('H');")

  login_ok=0
  for attempt in 0 -1; do
    h=$(( (10#$hora + attempt + 24) % 24 ))
    password="monse${h}"
    csrf=$(curl -s -c "$COOKIE_JAR" "${BASE_URL}/login" \
      | grep -o 'name="_csrf_token" value="[^"]*"' \
      | sed 's/name="_csrf_token" value="//;s/"//')
    [[ -z "$csrf" ]] && continue
    loc=$(curl -s -c "$COOKIE_JAR" -b "$COOKIE_JAR" \
      -X POST "${BASE_URL}/login_check" \
      --data-urlencode "_username=mb" \
      --data-urlencode "_password=${password}" \
      --data-urlencode "_csrf_token=${csrf}" \
      -D - 2>/dev/null | grep -i "^location:" | tr -d '\r' | awk '{print $2}')
    echo "$loc" | grep -q "/login" || { login_ok=1; break; }
  done

  if [[ $login_ok -eq 0 ]]; then
    echo -e "${RED}ERROR: Login fallido. Módulos ajax/post/smoke desactivados.${RESET}" >&2
    ERRORS=$(( ERRORS + 1 ))
    # limpiar should_run para los módulos siguientes
    ONLY="lint"
  else
    log "Autenticado (pwd=${password})"
    # curl escribe #HttpOnly_hostname; Python's MozillaCookieJar lo trata como comentario
    sed -i '' 's/^#HttpOnly_//' "$COOKIE_JAR"
    echo ""
  fi
fi

# ─────────────────────────────────────────────────────────────────────────────
# MÓDULO 2: AJAX — GET endpoints que deben retornar JSON válido
# ─────────────────────────────────────────────────────────────────────────────
if should_run ajax; then
  echo -e "${BOLD}── 2. Endpoints AJAX (JSON) ─────────────────────────────────────${RESET}"

  ajax_exit=0
  python3 - "$BASE_URL" "$COOKIE_JAR" "$VERBOSE" << 'PYEOF' || ajax_exit=$?
import sys, http.cookiejar, urllib.request, urllib.error, json, time

base_url, cookie_file, verbose = sys.argv[1:]
verbose = verbose == "1"

AJAX_ROUTES = [
    ("/procesos/generar/costopromedioestadoajax",   "estado"),
    ("/procesos/generar/costopromediobitacoraajax", None),
    ("/procesos/generar/costopromediovalidarajax",  None),
    ("/procesos/generar/costopromedioajax",         None),
    ("/procesos/revertir/costopromedioajax",        None),
    ("/procesos/revisar/libroventasajax",           None),
    ("/procesos/contabilidad/revisionajax",         None),
    ("/procesos/api/sinfirma",                      None),
    ("/procesos/api/sinactividad",                  None),
    ("/procesos/generar/promediosplusapi",           None),
    ("/procesos/cuadre/lotesnegativosapi",           None),
    ("/procesos/api/hayExistenciasNegativas",        None),
]

jar = http.cookiejar.MozillaCookieJar(cookie_file)
try:
    jar.load(ignore_discard=True, ignore_expires=True)
except Exception:
    pass

opener = urllib.request.build_opener(urllib.request.HTTPCookieProcessor(jar))
opener.addheaders = [("User-Agent", "MBInv-TestSuite/1.0"),
                     ("Accept", "application/json, text/plain, */*")]

GREEN  = "\033[0;32m"; RED = "\033[0;31m"; YELLOW = "\033[1;33m"; RESET = "\033[0m"

ok = 0; errors = 0

for route, expected_key in AJAX_ROUTES:
    url = base_url + route
    t0  = time.time()
    try:
        resp    = opener.open(url, timeout=15)
        elapsed = time.time() - t0
        code    = resp.getcode()
        body    = resp.read(4096).decode("utf-8", errors="replace")

        # Detectar login page (form con _username, no solo presencia de "/login" en nav)
        if 'name="_username"' in body or 'action="/login_check"' in body:
            print(f"  {RED}x{RESET} {route}  -- redirigido a login (sesion invalida)")
            errors += 1
            continue

        is_json = False
        parsed  = None
        ctype   = resp.headers.get("Content-Type", "")
        if "json" in ctype or body.strip().startswith(("{", "[")):
            try:
                parsed  = json.loads(body)
                is_json = True
            except json.JSONDecodeError:
                pass

        if code == 500:
            print(f"  {RED}x{RESET} {route}  -- HTTP 500")
            errors += 1
        elif not is_json:
            snippet = body[:60].replace("\n", " ")
            print(f"  {YELLOW}!{RESET} {route}  -- no es JSON ({code}) [{snippet}]")
        elif expected_key and isinstance(parsed, dict) and expected_key not in parsed:
            print(f"  {YELLOW}!{RESET} {route}  -- JSON OK pero sin clave '{expected_key}' (keys: {list(parsed.keys())[:4]})")
        else:
            ok += 1
            if verbose:
                print(f"  {GREEN}v{RESET} {route}  ({elapsed:.1f}s)")

    except urllib.error.HTTPError as e:
        elapsed = time.time() - t0
        if e.code in (400, 405):
            print(f"  {YELLOW}!{RESET} {route}  -- {e.code} (requiere parametros/POST)")
        else:
            print(f"  {RED}x{RESET} {route}  -- HTTP {e.code}")
            errors += 1
    except Exception as e:
        elapsed = time.time() - t0
        msg = str(e)[:60]
        if "timed out" in msg.lower():
            print(f"  {YELLOW}!{RESET} {route}  -- timeout 15s")
        else:
            print(f"  {RED}x{RESET} {route}  -- {msg}")
            errors += 1

total = len(AJAX_ROUTES)
print(f"\n  Endpoints probados : {total}")
print(f"  {GREEN}OK             : {ok}{RESET}")
if errors:
    print(f"  {RED}Errores        : {errors}{RESET}")
sys.exit(1 if errors else 0)
PYEOF
  [[ $ajax_exit -ne 0 ]] && ERRORS=$(( ERRORS + 1 ))
  echo ""
fi

# ─────────────────────────────────────────────────────────────────────────────
# MÓDULO 3: POST — formularios Symfony (CSRF + campos vacíos → no-500)
# ─────────────────────────────────────────────────────────────────────────────
if should_run post; then
  echo -e "${BOLD}── 3. Formularios POST (CSRF → envío vacío) ─────────────────────${RESET}"

  post_exit=0
  python3 - "$BASE_URL" "$COOKIE_JAR" "$VERBOSE" << 'PYEOF' || post_exit=$?
import sys, http.cookiejar, urllib.request, urllib.error, urllib.parse, re, time

base_url, cookie_file, verbose = sys.argv[1:]
verbose = verbose == "1"

FORMS = [
    # (GET url,        POST url,        campo CSRF,                          label)
    ("/producto/new",   "/producto/",    "producto[_token]",                  "Producto nuevo"),
    ("/documento/new",  "/documento/",   "maedoc[_token]",                    "Documento nuevo"),
    ("/movimiento/new", "/movimiento/",  "mb_productobundle_maemov[_token]",  "Movimiento nuevo"),
    ("/traslado/new",   "/traslado/",    "traslado[_token]",                  "Traslado nuevo"),
    ("/usuario/new",    "/usuario/",     "usuario[_token]",                   "Usuario nuevo"),
    ("/marca/new",      "/marca/",       "marca[_token]",                     "Marca nueva"),
    ("/maecaj/new",     "/maecaj/",      "maecaj[_token]",                    "Caja nueva"),
    ("/proveedor/new",  "/proveedor/",   "proveedor[_token]",                 "Proveedor nuevo"),
]

jar = http.cookiejar.MozillaCookieJar(cookie_file)
try:
    jar.load(ignore_discard=True, ignore_expires=True)
except Exception:
    pass

opener = urllib.request.build_opener(urllib.request.HTTPCookieProcessor(jar))
opener.addheaders = [("User-Agent", "MBInv-TestSuite/1.0")]

GREEN  = "\033[0;32m"; RED = "\033[0;31m"; YELLOW = "\033[1;33m"; RESET = "\033[0m"

ok = 0; errors = 0; warnings = 0

def extract_csrf(html, field_name):
    escaped = re.escape(field_name)
    pat = re.compile(
        r"name=" + r'["\']' + escaped + r'["\']' + r"[^>]*value=" + r'["\']([^"\']+)["\']'
        r"|value=" + r'["\']([^"\']+)["\']' + r"[^>]*name=" + r'["\']' + escaped + r'["\']',
        re.IGNORECASE
    )
    m = pat.search(html)
    if m:
        return m.group(1) or m.group(2)
    m2 = re.search(r"<input[^>]+_token[^>]+value=" + r'["\']([^"\']+)["\']', html)
    return m2.group(1) if m2 else None

for get_url, post_url, csrf_field, label in FORMS:
    t0 = time.time()
    try:
        resp_get = opener.open(base_url + get_url, timeout=60)
        html     = resp_get.read(524288).decode("utf-8", errors="replace")  # 512KB — _token aparece ~108KB en páginas grandes

        if resp_get.getcode() == 500 or ("Exception" in html and '"class":"' in html):
            print(f"  {RED}x{RESET} {label}  -- GET retorno 500")
            errors += 1
            continue

        # Detectar login page: buscar el tag HTML real, no strings JS que lo mencionan
        import re as _re
        is_login = bool(_re.search(r'<input[^>]+name=["\']_username["\']', html)) or \
                   bool(_re.search(r'<form[^>]+action=["\'][^"\']*login_check["\']', html))
        if is_login:
            print(f"  {RED}x{RESET} {label}  -- GET redirigido a login")
            errors += 1
            continue

        token = extract_csrf(html, csrf_field)
        if not token:
            print(f"  {YELLOW}!{RESET} {label}  -- sin CSRF '{csrf_field}'")
            warnings += 1
            continue

        data = urllib.parse.urlencode({csrf_field: token}).encode()
        req  = urllib.request.Request(
            base_url + post_url,
            data=data,
            headers={"Content-Type": "application/x-www-form-urlencoded"},
            method="POST"
        )
        try:
            resp_post = opener.open(req, timeout=15)
            code = resp_post.getcode()
            body = resp_post.read(4096).decode("utf-8", errors="replace")
        except urllib.error.HTTPError as he:
            code = he.code
            body = he.read(4096).decode("utf-8", errors="replace") if hasattr(he, "read") else ""

        elapsed = time.time() - t0

        if code == 500 or ("Exception" in body and '"class":"' in body):
            print(f"  {RED}x{RESET} {label}  -- POST retorno 500 ({elapsed:.1f}s)")
            errors += 1
        elif code in (200, 302, 303, 422):
            ok += 1
            if verbose:
                print(f"  {GREEN}v{RESET} {label}  -- {code} ({elapsed:.1f}s)")
        else:
            print(f"  {YELLOW}!{RESET} {label}  -- HTTP {code} ({elapsed:.1f}s)")
            warnings += 1

    except Exception as e:
        elapsed = time.time() - t0
        print(f"  {RED}x{RESET} {label}  -- {str(e)[:70]}")
        errors += 1

total = len(FORMS)
print(f"\n  Formularios probados: {total}")
print(f"  {GREEN}OK (no-500)    : {ok}{RESET}")
if warnings:
    print(f"  {YELLOW}Advertencias   : {warnings}{RESET}")
if errors:
    print(f"  {RED}Errores (500)  : {errors}{RESET}")
sys.exit(1 if errors else 0)
PYEOF
  [[ $post_exit -ne 0 ]] && ERRORS=$(( ERRORS + 1 ))
  echo ""
fi

# ─────────────────────────────────────────────────────────────────────────────
# MÓDULO 4: SMOKE — GET de todas las rutas (delega a smoke-test.sh)
# ─────────────────────────────────────────────────────────────────────────────
if should_run smoke; then
  echo -e "${BOLD}── 4. Smoke test GET (81 rutas) ─────────────────────────────────${RESET}"
  smoke_flags="--no-warmup"
  [[ $VERBOSE -eq 1 ]] && smoke_flags="$smoke_flags --verbose"
  "${PROJECT_DIR}/scripts/smoke-test.sh" $smoke_flags || ERRORS=$(( ERRORS + 1 ))
  echo ""
fi

# ─────────────────────────────────────────────────────────────────────────────
# RESUMEN FINAL
# ─────────────────────────────────────────────────────────────────────────────
echo -e "${BOLD}══════════════════════════════════════════════════════════${RESET}"
if [[ $ERRORS -eq 0 ]]; then
  echo -e "${GREEN}${BOLD}  RESULTADO: PASS — todos los módulos OK${RESET}"
else
  echo -e "${RED}${BOLD}  RESULTADO: FAIL — ${ERRORS} módulo(s) con errores${RESET}"
fi
echo -e "${BOLD}══════════════════════════════════════════════════════════${RESET}"
echo ""

exit $(( ERRORS > 0 ? 1 : 0 ))
