#!/usr/bin/env bash
#
# gates-selftest — prova que os gates falham em fixtures inválidas e passam nas válidas
# (plano 05 §5). Também roda os gates na árvore real.
#
# Uso: tools/gates-selftest.sh
set -Eeuo pipefail

ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
FIX="$ROOT/tools/tests/fixtures"
PHP_BIN="${PHP_BIN:-php}"
rc=0

run_case() { # <nome> <comando...>
    local name="$1"; shift
    if "$@" >/tmp/gate.out 2>&1; then
        echo "  ok   $name"
    else
        echo "  FALHOU $name"
        sed 's/^/      /' /tmp/gate.out
        rc=1
    fi
}

expect_fail() { # <nome> <comando...>
    local name="$1"; shift
    if "$@" >/tmp/gate.out 2>&1; then
        echo "  FALHOU (deveria falhar) $name"
        rc=1
    else
        echo "  ok   $name (falha esperada)"
    fi
}

echo "== arch-check =="
run_case "valid" "$PHP_BIN" "$ROOT/tools/arch-check" "$FIX/arch/valid"
expect_fail "invalid" "$PHP_BIN" "$ROOT/tools/arch-check" "$FIX/arch/invalid"
run_case "modules valid" "$PHP_BIN" "$ROOT/tools/arch-check" "$FIX/arch/modules-valid"
expect_fail "modules invalid (import cruzado)" "$PHP_BIN" "$ROOT/tools/arch-check" "$FIX/arch/modules-invalid"
run_case "service contract valid" "$PHP_BIN" "$ROOT/tools/arch-check" "$FIX/arch/service-contract-valid"
expect_fail "service impl invalid" "$PHP_BIN" "$ROOT/tools/arch-check" "$FIX/arch/service-impl-invalid"
run_case "services contract valid" "$PHP_BIN" "$ROOT/tools/arch-check" "$FIX/arch/services-contract-valid"
expect_fail "services impl invalid" "$PHP_BIN" "$ROOT/tools/arch-check" "$FIX/arch/services-impl-invalid"
run_case "storage owner valid" "$PHP_BIN" "$ROOT/tools/arch-check" "$FIX/arch/storage-owner-valid"
run_case "storage contract valid" "$PHP_BIN" "$ROOT/tools/arch-check" "$FIX/arch/storage-valid"
expect_fail "storage invalid (module toca Storage)" "$PHP_BIN" "$ROOT/tools/arch-check" "$FIX/arch/storage-invalid"

echo "== name-check =="
run_case "valid" "$ROOT/tools/name-check.sh" "$FIX/name/valid"
expect_fail "invalid" "$ROOT/tools/name-check.sh" "$FIX/name/invalid"

echo "== sniff-gate (parser offline) =="
# Fail-closed: sem a linha `rules: N pass | N warn | N fail` o gate **não** pode aprovar.
run_case "resumo válido" bash -c 'printf "rules: 3 pass | 1 warn | 0 fail\n" | "$1" --verify-summary' _ "$ROOT/tools/sniff-gate.sh"
expect_fail "saída sem resumo" bash -c 'printf "sniff check: algo inesperado\n" | "$1" --verify-summary' _ "$ROOT/tools/sniff-gate.sh"
expect_fail "saída vazia" bash -c 'printf "" | "$1" --verify-summary' _ "$ROOT/tools/sniff-gate.sh"
expect_fail "resumo zerado" bash -c 'printf "rules: 0 pass | 0 warn | 0 fail\n" | "$1" --verify-summary' _ "$ROOT/tools/sniff-gate.sh"

echo "== migration-check =="
run_case "valid" "$PHP_BIN" "$ROOT/tools/migration-check" "$FIX/migration/valid"
expect_fail "invalid" "$PHP_BIN" "$ROOT/tools/migration-check" "$FIX/migration/invalid"

echo "== security-check =="
run_case "real" "$ROOT/tools/security-check.sh" "$ROOT"
expect_fail "invalid (sem throttleWithRedis)" "$ROOT/tools/security-check.sh" "$FIX/security/invalid"
expect_fail "invalid (borda antiga sem Trusted Types)" \
    env PATH="$FIX/security/old-edge:$PATH" SECURITY_EDGE_URL='http://fixture' \
    SECURITY_PORTAL_HOST=localhost SECURITY_CMS_HOST=cms.localhost \
    "$ROOT/tools/security-check.sh" "$ROOT"
run_case "effective valid" \
    env PATH="$FIX/security/edge-valid:$PATH" SECURITY_EDGE_URL='http://fixture' \
    SECURITY_PORTAL_HOST=localhost SECURITY_CMS_HOST=cms.localhost \
    "$ROOT/tools/security-check.sh" "$ROOT"
expect_fail "effective sem immutable" \
    env PATH="$FIX/security/edge-no-immutable:$PATH" SECURITY_EDGE_URL='http://fixture' \
    SECURITY_PORTAL_HOST=localhost SECURITY_CMS_HOST=cms.localhost \
    "$ROOT/tools/security-check.sh" "$ROOT"
expect_fail "effective sem XFO" \
    env PATH="$FIX/security/edge-no-frame:$PATH" SECURITY_EDGE_URL='http://fixture' \
    SECURITY_PORTAL_HOST=localhost SECURITY_CMS_HOST=cms.localhost \
    "$ROOT/tools/security-check.sh" "$ROOT"
expect_fail "effective sem terceiros na CSP" \
    env PATH="$FIX/security/edge-no-third-party:$PATH" SECURITY_EDGE_URL='http://fixture' \
    SECURITY_PORTAL_HOST=localhost SECURITY_CMS_HOST=cms.localhost \
    "$ROOT/tools/security-check.sh" "$ROOT"

# Regressões do contrato Apache (I3): injeta a falha numa cópia do template real. Duplicar
# o arquivo inteiro num fixture criaria drift silencioso — a cópia é derivada na hora.
apache_tmp="$(mktemp -d)"
trap 'rm -f "$apache_tmp/app.conf" "$apache_tmp/app-ssl.conf"; rmdir "$apache_tmp"' EXIT

sed -E 's#^([[:space:]]*ProxyPass[[:space:]]*)/\.well-known/acme-challenge !.*#\1/.well-known !#' \
    "$ROOT/docker/web/app.conf" > "$apache_tmp/app.conf"
expect_fail "app.conf com ProxyPass /.well-known amplo" \
    env SECURITY_APACHE_CONF="$apache_tmp/app.conf" \
    "$ROOT/tools/security-check.sh" "$ROOT"

sed -E 's#^[[:space:]]*Header always set Strict-Transport-Security.*$##' \
    "$ROOT/docker/web/app-ssl.conf" > "$apache_tmp/app-ssl.conf"
expect_fail "app-ssl.conf sem HSTS" \
    env SECURITY_APACHE_SSL_CONF="$apache_tmp/app-ssl.conf" \
    "$ROOT/tools/security-check.sh" "$ROOT"

sed -E 's#^[[:space:]]*Require all denied.*$##' \
    "$ROOT/docker/web/app.conf" > "$apache_tmp/app.conf"
expect_fail "app.conf sem deny-by-default" \
    env SECURITY_APACHE_CONF="$apache_tmp/app.conf" \
    "$ROOT/tools/security-check.sh" "$ROOT"

sed -E 's@^(TraceEnable Off)@\1 # inline@' \
    "$ROOT/docker/web/app.conf" > "$apache_tmp/app.conf"
expect_fail "app.conf com comentário inline" \
    env SECURITY_APACHE_CONF="$apache_tmp/app.conf" \
    "$ROOT/tools/security-check.sh" "$ROOT"

# Regressões da cadeia de timeout do upload (D0). O defeito era uma janela **presente e
# errada**: subir um prazo sem os outros troca o sintoma (504 → HTTP 100) sem resolver.
# Por isso o gate confere a **ordem** — cada fixture abaixo quebra um invariante e tem de
# fazer o gate falhar. Sem isto, o gate é um snapshot.
chain_tmp="$(mktemp -d)"
trap 'rm -f "$apache_tmp/app.conf" "$apache_tmp/app-ssl.conf" "$chain_tmp"/*; rmdir "$apache_tmp" "$chain_tmp"' EXIT

# (1) read_body abaixo do max_input_time do PHP ⇒ o corpo passa no edge e morre no PHP.
sed -E 's/^([[:space:]]*)read_body[[:space:]]+[0-9]+s/\1read_body 10s/' \
    "$ROOT/docker/web/Caddyfile" > "$chain_tmp/caddy-read-body-baixo.conf"
expect_fail "Caddyfile com read_body abaixo do PHP" \
    env SECURITY_CADDYFILE="$chain_tmp/caddy-read-body-baixo.conf" \
    "$ROOT/tools/security-check.sh" "$ROOT"

# (2) write abaixo do read_body ⇒ o corpo entra, a resposta não sai.
sed -E 's/^([[:space:]]*)write[[:space:]]+[0-9]+s/\1write 30s/' \
    "$ROOT/docker/web/Caddyfile" > "$chain_tmp/caddy-write-baixo.conf"
expect_fail "Caddyfile com write abaixo do read_body" \
    env SECURITY_CADDYFILE="$chain_tmp/caddy-write-baixo.conf" \
    "$ROOT/tools/security-check.sh" "$ROOT"

# (3) read_timeout do fastcgi abaixo do request_terminate_timeout do FPM ⇒ o edge desiste
#     (504) antes de o FPM responder (500).
sed -E 's/^([[:space:]]*)read_timeout[[:space:]]+[0-9]+s/\1read_timeout 30s/' \
    "$ROOT/docker/web/Caddyfile" > "$chain_tmp/caddy-fastcgi-baixo.conf"
expect_fail "Caddyfile com read_timeout abaixo do FPM" \
    env SECURITY_CADDYFILE="$chain_tmp/caddy-fastcgi-baixo.conf" \
    "$ROOT/tools/security-check.sh" "$ROOT"

# (4) request_terminate_timeout abaixo de max_input_time + max_execution_time ⇒ o worker
#     morre antes de o PHP terminar.
sed -E 's/^([[:space:]]*)request_terminate_timeout[[:space:]]*=[[:space:]]*[0-9]+s/\1request_terminate_timeout = 30s/' \
    "$ROOT/docker/php-fpm/zz-custom.conf" > "$chain_tmp/fpm-terminate-baixo.conf"
expect_fail "FPM com request_terminate_timeout abaixo do PHP" \
    env SECURITY_FPM_CONF="$chain_tmp/fpm-terminate-baixo.conf" \
    "$ROOT/tools/security-check.sh" "$ROOT"

# (5) Ordem coerente, mas janela insuficiente: read_body 100 s com PHP 60 s passa nas
#     comparações de ordem e ainda assim não comporta 110 MiB a 500 KiB/s (≈225 s).
sed -E 's/^([[:space:]]*)read_body[[:space:]]+[0-9]+s/\1read_body 100s/' \
    "$ROOT/docker/web/Caddyfile" > "$chain_tmp/caddy-janela-estreita.conf"
sed -E 's/^max_input_time[[:space:]]*=[[:space:]]*[0-9]+/max_input_time = 60/' \
    "$ROOT/docker/php-fpm/php.ini" > "$chain_tmp/php-janela-estreita.ini"
expect_fail "janela de upload estreita (ordem ok, piso de 300s violado)" \
    env SECURITY_CADDYFILE="$chain_tmp/caddy-janela-estreita.conf" \
        SECURITY_PHP_INI="$chain_tmp/php-janela-estreita.ini" \
    "$ROOT/tools/security-check.sh" "$ROOT"

# (6) Gate **negativo**: o status do pool do FPM não pode existir (vaza o estado interno).
cp "$ROOT/docker/php-fpm/zz-custom.conf" "$chain_tmp/fpm-status.conf"
printf '\npm.status_path = /fpm-status\n' >> "$chain_tmp/fpm-status.conf"
expect_fail "FPM com pm.status_path exposto" \
    env SECURITY_FPM_CONF="$chain_tmp/fpm-status.conf" \
    "$ROOT/tools/security-check.sh" "$ROOT"

# (7) CVE-2026-109619: religar o upload progress no php.ini tem de falhar o gate.
sed -E 's/^session\.upload_progress\.enabled[[:space:]]*=[[:space:]]*Off/session.upload_progress.enabled = On/' \
    "$ROOT/docker/php-fpm/php.ini" > "$chain_tmp/php-upload-progress.ini"
expect_fail "php.ini com session.upload_progress religado" \
    env SECURITY_PHP_INI="$chain_tmp/php-upload-progress.ini" \
    "$ROOT/tools/security-check.sh" "$ROOT"

# Regressões do storage (D1): o log precisa do sinal útil, e a armadilha é justamente a
# variável que **silencia** o stdout. O healthcheck tem que ser o readiness.
sed -E 's/RUSTFS_OBS_LOGGER_LEVEL: "info"/RUSTFS_OBS_LOGGER_LEVEL: "error"/' \
    "$ROOT/docker-compose.yml" > "$chain_tmp/compose-storage-mudo.yml"
expect_fail "storage com log em error" \
    env SECURITY_COMPOSE="$chain_tmp/compose-storage-mudo.yml" \
    "$ROOT/tools/security-check.sh" "$ROOT"

sed -E 's/^([[:space:]]*)RUSTFS_CHECK_UPDATE: "false"/\1RUSTFS_OBS_USE_STDOUT: "true"\n\1RUSTFS_CHECK_UPDATE: "false"/' \
    "$ROOT/docker-compose.yml" > "$chain_tmp/compose-storage-silenciado.yml"
expect_fail "storage com RUSTFS_OBS_USE_STDOUT (armadilha de versão)" \
    env SECURITY_COMPOSE="$chain_tmp/compose-storage-silenciado.yml" \
    "$ROOT/tools/security-check.sh" "$ROOT"

sed -E 's#http://127.0.0.1:9000/health/ready#http://127.0.0.1:9000/health#' \
    "$ROOT/docker-compose.yml" > "$chain_tmp/compose-storage-liveness.yml"
expect_fail "storage com healthcheck de liveness" \
    env SECURITY_COMPOSE="$chain_tmp/compose-storage-liveness.yml" \
    "$ROOT/tools/security-check.sh" "$ROOT"

# Regressões do ambiente (D2): toda variável do Compose precisa ter default
# funcional (`:-`) ou ser obrigatória por natureza (`:?` com mensagem) — o `.env`
# fica curto de propósito. E o artefato por ambiente (`docker-compose.prod.yml`)
# é proibido (ADR-032).
sed -E 's/\$\{AWS_DEFAULT_REGION:-us-east-1\}/\${AWS_DEFAULT_REGION}/' \
    "$ROOT/docker-compose.yml" > "$chain_tmp/compose-var-sem-default.yml"
expect_fail "Compose com variável sem default" \
    env SECURITY_COMPOSE="$chain_tmp/compose-var-sem-default.yml" \
    "$ROOT/tools/security-check.sh" "$ROOT"

# Regressões do Postgres (D4): o ADR-007 exige `wal_compression=on` (e os outros
# parâmetros de WAL) **com o valor certo** — presença não basta, `wal_compression = off`
# é exatamente o defeito. E o `/dev/shm` do container tem de ser ≥ o `shared_buffers`.
sed -E 's/^wal_compression = on$/wal_compression = off/' \
    "$ROOT/docker/postgres/postgresql.conf" > "$chain_tmp/postgresql-wal-off.conf"
expect_fail "Postgres com wal_compression = off" \
    env SECURITY_PG_CONF="$chain_tmp/postgresql-wal-off.conf" \
    "$ROOT/tools/security-check.sh" "$ROOT"

sed -E 's/^max_wal_size = 1GB$/max_wal_size = 64MB/' \
    "$ROOT/docker/postgres/postgresql.conf" > "$chain_tmp/postgresql-wal-pequeno.conf"
expect_fail "Postgres com max_wal_size pequeno" \
    env SECURITY_PG_CONF="$chain_tmp/postgresql-wal-pequeno.conf" \
    "$ROOT/tools/security-check.sh" "$ROOT"

sed -E 's/^log_lock_waits = on$/log_lock_waits = off/' \
    "$ROOT/docker/postgres/postgresql.conf" > "$chain_tmp/postgresql-sem-lockwait.conf"
expect_fail "Postgres sem log_lock_waits" \
    env SECURITY_PG_CONF="$chain_tmp/postgresql-sem-lockwait.conf" \
    "$ROOT/tools/security-check.sh" "$ROOT"

sed -E 's/^    shm_size: 256mb$/    shm_size: 64mb/' \
    "$ROOT/docker-compose.yml" > "$chain_tmp/compose-shm-pequeno.yml"
expect_fail "Compose com shm_size menor que shared_buffers" \
    env SECURITY_COMPOSE="$chain_tmp/compose-shm-pequeno.yml" \
    "$ROOT/tools/security-check.sh" "$ROOT"

sed -E 's/^log_checkpoints = on$/log_checkpoints = on\nlog_connections = on/' \
    "$ROOT/docker/postgres/postgresql.conf" > "$chain_tmp/postgresql-com-conexoes.conf"
expect_fail "Postgres com log_connections (ruído)" \
    env SECURITY_PG_CONF="$chain_tmp/postgresql-com-conexoes.conf" \
    "$ROOT/tools/security-check.sh" "$ROOT"

# Regressões da observabilidade (D6): o access log do Caddy existe e **apaga** a
# credencial (o cookie de sessão é o bearer do CMS); o path do `filter` não pode
# começar com `>` (o delete vira no-op e o Cookie volta ao log — medido); o Valkey
# loga em logfmt com PII mascarada; e a senha do Valkey não pode voltar para o argv.
sed -E 's/request>headers>Cookie delete/request>headers>Cookie replace "x"/' \
    "$ROOT/docker/web/Caddyfile" > "$chain_tmp/caddy-sem-delete-cookie.yml"
expect_fail "Caddy sem o delete do Cookie" \
    env SECURITY_CADDYFILE="$chain_tmp/caddy-sem-delete-cookie.yml" \
    "$ROOT/tools/security-check.sh" "$ROOT"

sed -E 's/request>headers>Cookie delete/>request>headers>Cookie delete/' \
    "$ROOT/docker/web/Caddyfile" > "$chain_tmp/caddy-path-com-gt.yml"
expect_fail "Caddy com o path do filter com '>' (delete vira no-op)" \
    env SECURITY_CADDYFILE="$chain_tmp/caddy-path-com-gt.yml" \
    "$ROOT/tools/security-check.sh" "$ROOT"

sed -E 's/resp_headers>Set-Cookie delete/resp_headers>Set-Cookie replace "x"/' \
    "$ROOT/docker/web/Caddyfile" > "$chain_tmp/caddy-sem-setcookie.yml"
expect_fail "Caddy sem o delete do Set-Cookie" \
    env SECURITY_CADDYFILE="$chain_tmp/caddy-sem-setcookie.yml" \
    "$ROOT/tools/security-check.sh" "$ROOT"

sed -E 's/^hide-user-data-from-log yes$/hide-user-data-from-log no/' \
    "$ROOT/docker/valkey/valkey.conf" > "$chain_tmp/valkey-com-pii.yml"
expect_fail "Valkey sem o hide-user-data-from-log" \
    env SECURITY_VALKEY_CONF="$chain_tmp/valkey-com-pii.yml" \
    "$ROOT/tools/security-check.sh" "$ROOT"

sed -E 's/^log-format logfmt$/log-format syslog/' \
    "$ROOT/docker/valkey/valkey.conf" > "$chain_tmp/valkey-sem-logfmt.yml"
expect_fail "Valkey sem log em logfmt" \
    env SECURITY_VALKEY_CONF="$chain_tmp/valkey-sem-logfmt.yml" \
    "$ROOT/tools/security-check.sh" "$ROOT"

sed -E 's|"VALKEYCLI_AUTH=\$\$REDIS_PASSWORD valkey-cli ping|"valkey-cli -a \\"$$REDIS_PASSWORD\\" ping|' \
    "$ROOT/docker-compose.yml" > "$chain_tmp/compose-valkey-argv.yml"
expect_fail "Compose com a senha do Valkey no argv do healthcheck" \
    env SECURITY_COMPOSE="$chain_tmp/compose-valkey-argv.yml" \
    "$ROOT/tools/security-check.sh" "$ROOT"

sed -E 's|^allow_url_fopen = Off$|allow_url_fopen = On|' \
    "$ROOT/docker/php-fpm/php.ini" > "$chain_tmp/php-url-fopen-on.ini"
expect_fail "php.ini com allow_url_fopen ligado (SSRF)" \
    env SECURITY_PHP_INI="$chain_tmp/php-url-fopen-on.ini" \
    "$ROOT/tools/security-check.sh" "$ROOT"

# Regressões do hardening (D5): `no-new-privileges` em todos, `cap_drop: [ALL]` nos que
# rodam non-root, o Postgres **sem** cap_drop (o entrypoint precisa de CAP_CHOWN), a
# exceção medida do Caddy (capability de arquivo), os tetos de processo/descritor, a
# janela de parada **maior** que o timeout do worker e o healthcheck do worker.
sed -E 's/^    security_opt: \["no-new-privileges:true"\]$//' \
    "$ROOT/docker-compose.yml" > "$chain_tmp/compose-sem-nnp.yml"
expect_fail "Compose sem no-new-privileges" \
    env SECURITY_COMPOSE="$chain_tmp/compose-sem-nnp.yml" \
    "$ROOT/tools/security-check.sh" "$ROOT"

sed -E '0,/^    cap_drop: \[ALL\]$/s///' \
    "$ROOT/docker-compose.yml" > "$chain_tmp/compose-sem-capdrop.yml"
expect_fail "Compose sem cap_drop: [ALL]" \
    env SECURITY_COMPOSE="$chain_tmp/compose-sem-capdrop.yml" \
    "$ROOT/tools/security-check.sh" "$ROOT"

sed -E 's/^(    security_opt: \["no-new-privileges:true"\]$)/    security_opt: ["no-new-privileges:true"]\n    cap_drop: [ALL]/' \
    "$ROOT/docker-compose.yml" > "$chain_tmp/compose-postgres-capdrop.yml"
expect_fail "Compose com cap_drop no postgres" \
    env SECURITY_COMPOSE="$chain_tmp/compose-postgres-capdrop.yml" \
    "$ROOT/tools/security-check.sh" "$ROOT"

sed -E '/^    cap_add: \[NET_BIND_SERVICE\]$/d' \
    "$ROOT/docker-compose.yml" > "$chain_tmp/compose-web-sem-capadd.yml"
expect_fail "Compose sem a exceção de capability do Caddy" \
    env SECURITY_COMPOSE="$chain_tmp/compose-web-sem-capadd.yml" \
    "$ROOT/tools/security-check.sh" "$ROOT"

sed -E 's/^    pids_limit: 128$//' \
    "$ROOT/docker-compose.yml" > "$chain_tmp/compose-sem-pids.yml"
expect_fail "Compose sem pids_limit" \
    env SECURITY_COMPOSE="$chain_tmp/compose-sem-pids.yml" \
    "$ROOT/tools/security-check.sh" "$ROOT"

sed -E 's/^    stop_grace_period: 150s$/    stop_grace_period: 60s/' \
    "$ROOT/docker-compose.yml" > "$chain_tmp/compose-grace-menor.yml"
expect_fail "Compose com a janela de parada menor que o timeout do worker" \
    env SECURITY_COMPOSE="$chain_tmp/compose-grace-menor.yml" \
    "$ROOT/tools/security-check.sh" "$ROOT"

sed -E 's/--timeout=120//' \
    "$ROOT/docker-compose.yml" > "$chain_tmp/compose-sem-timeout.yml"
expect_fail "Compose com o worker sem --timeout" \
    env SECURITY_COMPOSE="$chain_tmp/compose-sem-timeout.yml" \
    "$ROOT/tools/security-check.sh" "$ROOT"

sed -E 's/--backoff=3//' \
    "$ROOT/docker-compose.yml" > "$chain_tmp/compose-sem-backoff.yml"
expect_fail "Compose com o worker sem --backoff" \
    env SECURITY_COMPOSE="$chain_tmp/compose-sem-backoff.yml" \
    "$ROOT/tools/security-check.sh" "$ROOT"

sed -E 's/^    healthcheck:$//' \
    "$ROOT/docker-compose.yml" > "$chain_tmp/compose-queue-sem-health.yml"
expect_fail "Compose sem healthcheck no worker" \
    env SECURITY_COMPOSE="$chain_tmp/compose-queue-sem-health.yml" \
    "$ROOT/tools/security-check.sh" "$ROOT"

sed -E 's/^Schedule::command\('"'"'queue:prune-failed --hours=48'"'"'\)->daily\(\);$//' \
    "$ROOT/routes/console.php" > "$chain_tmp/console-sem-poda.php"
expect_fail "Console sem a poda de failed_jobs agendada" \
    env SECURITY_CONSOLE="$chain_tmp/console-sem-poda.php" \
    "$ROOT/tools/security-check.sh" "$ROOT"

# O isolamento de rede (topologia): o `web` na rede de dados é o defeito.
sed -E 's/^    networks: \[edge\]$/    networks: [edge, data]/' \
    "$ROOT/docker-compose.yml" > "$chain_tmp/compose-web-na-data.yml"
expect_fail "Compose com o web na rede de dados" \
    env SECURITY_COMPOSE="$chain_tmp/compose-web-na-data.yml" \
    "$ROOT/tools/network-check.sh" --static-only

sed -E 's/^    internal: true$//' \
    "$ROOT/docker-compose.yml" > "$chain_tmp/compose-data-sem-internal.yml"
expect_fail "Compose com a rede de dados sem internal: true" \
    env SECURITY_COMPOSE="$chain_tmp/compose-data-sem-internal.yml" \
    "$ROOT/tools/network-check.sh" --static-only

{ echo 'docker-compose.prod.yml'; cat "$ROOT/scripts/update/05-build.sh"; } > "$chain_tmp/deploy-compose-prod.sh"
expect_fail "deploy com docker-compose.prod.yml" \
    env SECURITY_DEPLOY_SCRIPT="$chain_tmp/deploy-compose-prod.sh" \
    "$ROOT/tools/security-check.sh" "$ROOT"

# Regressões da imagem (D3): uma imagem só (ADR-031), PECL pinado, download com checksum
# e digest na matriz do ADR.
# Só a **primeira** ocorrência (o `php-fpm`): se as duas mudassem, continuariam iguais.
awk 'BEGIN { done = 0 } { if (!done && /image: "\$\{PHP_IMAGE/) { sub(/image: .*/, "image: \"concordia\/php-fpm:outra\""); done = 1 } print }' \
    "$ROOT/docker-compose.yml" > "$chain_tmp/compose-duas-imagens.yml"
expect_fail "Compose com duas imagens PHP" \
    env SECURITY_COMPOSE="$chain_tmp/compose-duas-imagens.yml" \
    "$ROOT/tools/security-check.sh" "$ROOT"

sed -E 's/^([[:space:]]*)pull_policy: never$/\1build:\n\1  context: .\n\1  dockerfile: docker\/php-fpm\/Dockerfile\n\1  target: runtime\n\1pull_policy: never/' \
    "$ROOT/docker-compose.yml" > "$chain_tmp/compose-queue-build.yml"
expect_fail "Compose com build: no queue" \
    env SECURITY_COMPOSE="$chain_tmp/compose-queue-build.yml" \
    "$ROOT/tools/security-check.sh" "$ROOT"

# Sem a versão no ARG: o build passaria a pegar "a última" do PECL.
sed -E 's/^(ARG PECL_REDIS=).*/\1/' \
    "$ROOT/docker/php-fpm/Dockerfile" > "$chain_tmp/Dockerfile-sem-pinagem"
expect_fail "Dockerfile com pecl install sem versao" \
    env SECURITY_DOCKERFILE="$chain_tmp/Dockerfile-sem-pinagem" \
    "$ROOT/tools/security-check.sh" "$ROOT"

# Remove **todas** as verificações de checksum (o BLAKE3 e os 4 pacotes PECL).
sed -E 's/sha256sum -c -/true/g' \
    "$ROOT/docker/php-fpm/Dockerfile" > "$chain_tmp/Dockerfile-sem-checksum"
expect_fail "Dockerfile sem checksum do download" \
    env SECURITY_DOCKERFILE="$chain_tmp/Dockerfile-sem-checksum" \
    "$ROOT/tools/security-check.sh" "$ROOT"

sed -E 's/@sha256:[0-9a-f]{64}/@sha256:0000000000000000000000000000000000000000000000000000000000000000/' \
    "$ROOT/docker-compose.yml" > "$chain_tmp/compose-digest-desconhecido.yml"
expect_fail "Compose com digest fora da matriz do ADR-031" \
    env SECURITY_COMPOSE="$chain_tmp/compose-digest-desconhecido.yml" \
    "$ROOT/tools/security-check.sh" "$ROOT"

echo "== whm-canary =="
if command -v httpd >/dev/null 2>&1; then
    run_case "self-test (render + configtest + controle negativo)" "$ROOT/tools/whm-canary.sh" --self-test
else
    echo "  SKIP self-test (sem httpd; rode no host do WHM)"
fi

echo "== wal-retention =="
wal_tmp="$(mktemp -d)"
touch -d '30 days ago' "$wal_tmp/old.wal"
touch "$wal_tmp/new.wal"
run_case "poda por idade" "$ROOT/tools/wal-retention.sh" --dir "$wal_tmp" --days 14 --json
[ -f "$wal_tmp/old.wal" ] && { echo "  FALHOU WAL antigo não removido"; rc=1; } || echo "  ok   WAL antigo removido"
run_case "dry-run" "$ROOT/tools/wal-retention.sh" --dir "$wal_tmp" --days 1 --dry-run
[ -f "$wal_tmp/new.wal" ] && echo "  ok   dry-run preserva o arquivo" || { echo "  FALHOU dry-run apagou"; rc=1; }
expect_fail "--days 0" "$ROOT/tools/wal-retention.sh" --dir "$wal_tmp" --days 0
rm -f "$wal_tmp/old.wal" "$wal_tmp/new.wal"
rmdir "$wal_tmp"

echo "== go-live-check =="
run_case "valid" \
    env PATH="$FIX/golive:$PATH" FAKE_BREAK='' \
    "$ROOT/tools/go-live-check.sh" --url http://fixture --json
for rule in compression 304 immutable cookie forbidden trace; do
    expect_fail "invalid ($rule)" \
        env PATH="$FIX/golive:$PATH" FAKE_BREAK="$rule" \
        "$ROOT/tools/go-live-check.sh" --url http://fixture
done
run_case "modo produção (HSTS + deny 403)" \
    env PATH="$FIX/golive:$PATH" FAKE_BREAK=deny \
    "$ROOT/tools/go-live-check.sh" --url https://fixture --require-hsts --require-deny
expect_fail "sem HSTS com --require-hsts" \
    env PATH="$FIX/golive:$PATH" FAKE_BREAK=hsts \
    "$ROOT/tools/go-live-check.sh" --url https://fixture --require-hsts
expect_fail "404 não satisfaz --require-deny" \
    env PATH="$FIX/golive:$PATH" FAKE_BREAK='' \
    "$ROOT/tools/go-live-check.sh" --url http://fixture --require-deny

echo "== vendor-check =="
run_case "valid" "$ROOT/tools/vendor-check.sh" "$FIX/vendor/valid"
run_case "allowlist (VLibras/GA)" "$ROOT/tools/vendor-check.sh" "$FIX/vendor/allowed"
expect_fail "invalid" "$ROOT/tools/vendor-check.sh" "$FIX/vendor/invalid"

echo "== data-dirs (o DATA_PATH continua ignorado) =="
mkdir -p "$ROOT/.pi/tmp"
DD_TMP="$(mktemp -d "$ROOT/.pi/tmp/gate-data-dirs.XXXXXX")"
run_case "cria pgdata/rustfs/data + guard" bash -c '
    umask 077
    bash "$1/tools/data-dirs.sh" "$2/data" >/dev/null || exit 1
    test -d "$2/data/pgdata" && test -d "$2/data/rustfs/data" || exit 1
    test "$(cat "$2/data/.gitignore")" = "$(printf "*\n!.gitignore")"
' _ "$ROOT" "$DD_TMP"
run_case "pgdata montado é 0755 mesmo com umask 077 (o uid postgres precisa de x)" bash -c '
    umask 077
    out="$(mktemp -d)"
    bash "$1/tools/data-dirs.sh" "$out/data" >/dev/null || exit 1
    test "$(stat -c %a "$out/data/pgdata")" = 755
' _ "$ROOT"
run_case "não sobrescreve um guard existente" bash -c '
    printf "sentinel\n" > "$2/data/.gitignore"
    bash "$1/tools/data-dirs.sh" "$2/data" >/dev/null || exit 1
    test "$(cat "$2/data/.gitignore")" = "sentinel"
' _ "$ROOT" "$DD_TMP"
run_case "fora do repositório só cria os diretórios" bash -c '
    out="$(mktemp -d)"
    bash "$1/tools/data-dirs.sh" "$out/data" >/dev/null || exit 1
    test -d "$out/data/pgdata" && test ! -e "$out/data/.gitignore"
' _ "$ROOT"

echo "== árvore real =="
run_case "arch-check" "$PHP_BIN" "$ROOT/tools/arch-check" "$ROOT"
run_case "name-check" "$ROOT/tools/name-check.sh" "$ROOT"
run_case "migration-check" "$PHP_BIN" "$ROOT/tools/migration-check" "$ROOT"
run_case "vendor-check" "$ROOT/tools/vendor-check.sh" "$ROOT"
run_case "assets-licenses-check" "$PHP_BIN" "$ROOT/tools/assets-licenses-check.php" "$ROOT"

if [ "$rc" -ne 0 ]; then
    echo "gates-selftest: FALHOU" >&2
    exit 1
fi
echo "gates-selftest: OK"
exit 0
