#!/usr/bin/env bash
#
# O QUE O CI CONFERE E O CORREDOR LOCAL NAO -- e quanto disso da para medir aqui.
# E, desde 07/10/2026, O QUE O CORREDOR CONFERE E O CI NAO (D441).
#
#     bash scripts/conferir-lacuna-do-ci.sh
#     bash scripts/conferir-lacuna-do-ci.sh --so-declaracao
#
# Sai com 1 quando aparece, em qualquer das duas direcoes, um passo que ninguem
# declarou -- ou uma declaracao que nao se aplica mais. `--so-declaracao` confere
# so isso e sai, sem rodar portao nenhum: e o modo do `conferir-tudo.sh` e do CI.
# Saida 2 e "o leitor quebrou" (heredoc ou aspas sem fechar, ou nenhuma chamada
# lida), e nunca verde.
#
# O LEITOR E POSTO A PROVA por `scripts/ensaiar-lacuna-do-ci.sh`: cada forma de
# escrever um passo que ele diz ver (ou ignorar) e um caso ali, com a saida que
# tem de dar. Mudou o leitor, rode o ensaio.
#
# -----------------------------------------------------------------------------
# POR QUE ISTO EXISTE (D187)
#
# Desde 13/09/2026 o CI nao inicia -- cobranca da conta, nao defeito do codigo.
# Enquanto isso, o registro dizia que "os portoes que so existem no CI foram
# rodados A MAO". Disciplina ninguem sustenta, e pior: ninguem sabia QUANTOS
# eram. O registro dizia seis; sao treze.
#
# A LISTA E DERIVADA, e nao digitada. Ela sai da diferenca entre o que o
# `ci.yml` roda e o que o `conferir-tudo.sh` roda, recalculada a cada execucao.
# Passo novo no CI que ninguem declarar aqui faz este roteiro parar -- que e o
# ponto. Lista digitada envelhece em silencio (D177).
#
# E O QUE NAO RODA AQUI SAI COMO "NAO MEDIDO", nunca como verde. Ausencia de
# medicao nao vira permissao.
#
# -----------------------------------------------------------------------------
# A OUTRA DIRECAO (D441, 07/10/2026)
#
# Ate aqui so se perguntava o que o CI tem e o corredor nao. A pergunta
# contraria ninguem fazia, e a resposta era grande: em 07/10/2026 o corredor
# rodava 36 roteiros que o `ci.yml` nao rodava -- portao novo nascia no
# `conferir-tudo.sh` e ficava la, e o PR que o quebrasse passaria verde no CI.
# Agora cada um deles esta num `run:` do CI ou na tabela FORA_DO_CI, com o
# motivo. E o `rodar` do corredor que nao chama roteiro de `scripts/` -- os
# testes do front sao `npm test` -- vale pelo COMANDO: o mesmo texto tem de
# estar num `run:` do CI, ou o comando entra na FORA_DO_CI.
#
# O SHELL E LIDO COMO SHELL, e nao como linha de texto. Os dois arquivos passam
# pelo mesmo leitor: aspas, comentario (o `#` no comeco de palavra -- depois de
# espaco, e tambem de `;`, `&`, `|`), linha continuada por contrabarra, heredoc
# (o `: <<'FIM'` e o comentario em bloco do shell) e os operadores que separam
# um comando do outro. O portao textual le comentario, e a primeira versao
# desta direcao contava um `#` colado no `;`, um `echo` e um `|| true` como "o
# CI roda" -- e um `rodar` depois de `&&` como "o corredor nao roda".
#
# CADA PERGUNTA LE DO JEITO QUE ERRA PARA O LADO SEGURO, e por isso sao quatro
# leituras, e nao duas:
#
#   - "o CI RODA isto como portao?" (a direcao nova) conta A MENOS: so o
#     comando de um `run:` cuja falha derruba o passo. Nao conta o que esta
#     entre aspas, o `echo`/`printf`/`true`/`test`, o comando seguido de `||`,
#     `&&`, `|` ou `&`, a condicao de `if`/`while`/`until`, o que vem depois de
#     `set +e`, o corpo de heredoc, nem o passo ou o job com `continue-on-error`
#     ligado, ou com `if:` que diga `false`, `failure()` ou `cancelled()`.
#   - "o corredor RODA isto?" (a direcao nova) conta A MAIS: todo codigo que
#     nao e comentario, o `rodar` depois de `&&` ou dentro de `if` inclusive.
#   - "o CI CITA isto?" (a primeira direcao) conta A MAIS: todo texto do
#     `ci.yml` que nao e comentario, do YAML ou do shell. Citar a mais so pede
#     uma linha a mais na tabela DECLARADO.
#   - "o corredor CHAMA isto?" (a primeira direcao) conta A MENOS: so o
#     comando das chamadas de `rodar` e `rodar_conhecido`, sem os rotulos.
#
# A DECLARACAO VELHA SE MEDE COM A MESMA LEITURA QUE A EXIGE. Linha da
# DECLARADO cujo roteiro so sobrou num comentario do `ci.yml` e velha; linha da
# FORA_DO_CI cujo roteiro so sobrou num comentario do corredor tambem. Medir as
# duas coisas com leituras diferentes faria uma linha ser exigida e velha ao
# mesmo tempo.
#
# O QUE ELE NAO VE:
#   - o NOME do roteiro e a unidade, e nao a chamada -- o mesmo roteiro com
#     outro argumento conta como coberto (o `--conteiner` do
#     `conferir-provedores-no-loopback` e do operador, e o CI roda o modo dos
#     arquivos);
#   - na primeira direcao, so roteiro de `scripts/`: o CI roda `composer`, as
#     suites do PHPUnit e o `psql` do RLS, que o corredor nao roda, e isso nao
#     entra na conta (as suites estao no fim do relatorio completo);
#   - o shell e lido por aproximacao: o `bash -e` do runner nao derruba o passo
#     num comando do MEIO de uma lista com `&&` -- este leitor ja nao o conta --,
#     mas tambem nao o derruba num subshell em segundo plano, num `if:` com
#     outra expressao, num `shell:` trocado, nem num `run:` entre aspas do YAML
#     (este conta a menos, que e o lado seguro); bloco `>` e lido linha a
#     linha, e so o primeiro heredoc de cada linha conta.

set -u
cd "$(dirname "$0")/.."

# O MESMO RESULTADO NO GAWK DO GIT BASH E NO MAWK DO RUNNER: byte a byte, e
# a mesma ordem no `sort` e no `comm`.
export LC_ALL=C

CI=".github/workflows/ci.yml"
CORREDOR="scripts/conferir-tudo.sh"

for f in "$CI" "$CORREDOR"; do
    if [ ! -f "$f" ]; then
        echo "Nao consegui ler $f" >&2
        exit 2
    fi
done

SO_DECLARACAO=""
[ "${1:-}" = "--so-declaracao" ] && SO_DECLARACAO=1

# -----------------------------------------------------------------------------
# O LEITOR DE SHELL, comum aos dois arquivos. Escrito em awk POSIX: o runner
# usa o mawk, e o Git Bash, o gawk.
#
# Cada linha fisica sai em COD (o codigo, sem o comentario) e em NUA (o mesmo
# texto com tudo que esta entre aspas, as aspas inclusive, trocado por `_`).
# Quem procura nome de roteiro, operador ou palavra-chave na NUA nao acha o que
# esta citado; as duas tem o mesmo comprimento, e uma posicao vale nas duas.
LEITOR_DE_SHELL='
BEGIN { SQ = "\047"; DQ = "\042" }

function normaliza(s) { gsub(/[ \t]+/, " ", s); sub(/^ /, "", s); sub(/ $/, "", s); return s }

function limpa(s,    i, c, r) {
    r = ""
    for (i = 1; i <= length(s); i++) {
        c = substr(s, i, 1)
        if (c != SQ && c != DQ && c != "\\") r = r c
    }
    return r
}

# Uma linha fisica. As aspas abertas (sq, dq) passam para a linha seguinte.
# O `#` so abre comentario no comeco de uma palavra: depois de nada, de branco
# ou de operador -- `$#` e `a#b` nao sao comentario, `true;#x` e.
function escaneia(s,    i, n, c, cod, nua, esc) {
    cod = ""; nua = ""; esc = 0; CONT = 0; n = length(s)
    for (i = 1; i <= n; i++) {
        c = substr(s, i, 1)
        if (sq) { cod = cod c; nua = nua "_"; if (c == SQ) sq = 0; continue }
        if (esc) { cod = cod c; nua = nua "_"; esc = 0; continue }
        if (c == "\\") {
            if (i == n) { CONT = 1; break }
            cod = cod c; nua = nua "_"; esc = 1; continue
        }
        if (dq) { cod = cod c; nua = nua "_"; if (c == DQ) dq = 0; continue }
        if (c == SQ) { sq = 1; cod = cod c; nua = nua "_"; continue }
        if (c == DQ) { dq = 1; cod = cod c; nua = nua "_"; continue }
        if (c == "#" && (nua == "" || substr(nua, length(nua), 1) ~ /[ \t;&|()<>]/)) break
        cod = cod c; nua = nua c
    }
    COD = cod; NUA = nua
}

# Junta as linhas fisicas numa logica (LCOD/LNUA): contrabarra no fim continua,
# e aspa aberta tambem. Devolve 1 quando a linha logica esta completa.
function fisica(s) {
    escaneia(s)
    LCOD = LCOD COD; LNUA = LNUA NUA
    if (CONT) return 0
    if (sq || dq) { LCOD = LCOD " "; LNUA = LNUA "_"; return 0 }
    return 1
}

# O HEREDOC: o corpo nao e comando. O primeiro `<<` fora de aspas que nao seja
# `<<<` abre o corpo; o delimitador fica sem as aspas, e `: <<FIM` -- o
# comando nulo -- marca o corpo como comentario.
function abre_heredoc(    base, r, i, p, n, d, c) {
    base = 0; r = LNUA
    while (1) {
        i = index(r, "<<")
        if (i == 0) return
        if (substr(r, i + 2, 1) != "<") break
        base += i + 2; r = substr(r, i + 3)
    }
    p = base + i
    i = p + 2; HD_TABS = 0
    if (substr(LNUA, i, 1) == "-") { HD_TABS = 1; i++ }
    n = length(LNUA)
    while (i <= n && substr(LNUA, i, 1) ~ /[ \t]/) i++
    d = ""
    while (i <= n && substr(LNUA, i, 1) !~ /[ \t;&|()<>]/) { d = d substr(LCOD, i, 1); i++ }
    d = limpa(d)
    if (d == "") return
    HD = d
    HD_NULO = (substr(LNUA, 1, p - 1) ~ /^[ \t]*:[ \t]*$/)
}

function fecha_heredoc(s) {
    if (HD_TABS) sub(/^\t+/, "", s)
    if (s == HD) { HD = ""; return 1 }
    return 0
}

# Parte a linha logica nos operadores de lista e de pipe fora de aspas, e
# entrega cada comando simples a `comando()` com o operador que vem DEPOIS
# dele. Redirecionamento com `&` (`2>&1`, `>&2`, `&>`) nao separa nada.
function parte(    nua, i, n, c, op, ini) {
    nua = LNUA
    gsub(/>&/, "> ", nua); gsub(/<&/, "< ", nua); gsub(/&>/, " >", nua)
    n = length(nua); ini = 1
    for (i = 1; i <= n; i++) {
        c = substr(nua, i, 1)
        if (c != ";" && c != "&" && c != "|" && c != "(" && c != ")") continue
        op = substr(nua, i, 2)
        if (op != "&&" && op != "||" && op != ";;") op = c
        comando(substr(LCOD, ini, i - ini), substr(nua, ini, i - ini), op)
        i += length(op) - 1; ini = i + 1
    }
    comando(substr(LCOD, ini), substr(nua, ini), "")
}

# Tira as palavras-chave do comeco (`!`, `then`, `else`, `{`...) e entrega a
# `simples()` o comando a partir da primeira palavra, em PAL. COND fica ligado
# entre `if`/`while`/`until` e o `then`/`do`: e a condicao, e nao o corpo.
function comando(cod, nua, depois) {
    PCOD = cod; PNUA = nua; NEG = 0
    while (1) {
        match(PNUA, /^[ \t]*/)
        PCOD = substr(PCOD, RLENGTH + 1); PNUA = substr(PNUA, RLENGTH + 1)
        if (PNUA == "") return
        match(PNUA, /^[^ \t]+/); PAL = substr(PNUA, 1, RLENGTH)
        if (PAL == "!") NEG = 1
        else if (PAL == "if" || PAL == "elif" || PAL == "while" || PAL == "until") COND = 1
        else if (PAL == "then" || PAL == "do") COND = 0
        else if (PAL != "else" && PAL != "time" && PAL != "{" && PAL != "}" && PAL != "fi" && PAL != "done" && PAL != "esac") break
        PCOD = substr(PCOD, RLENGTH + 1); PNUA = substr(PNUA, RLENGTH + 1)
    }
    simples(PCOD, PNUA, depois)
}

# Corta a primeira palavra de PCOD/PNUA. Devolve 0 quando nao ha mais nada.
function corta() {
    match(PNUA, /^[ \t]*/); PCOD = substr(PCOD, RLENGTH + 1); PNUA = substr(PNUA, RLENGTH + 1)
    if (PNUA == "") return 0
    match(PNUA, /^[^ \t]+/); PCOD = substr(PCOD, RLENGTH + 1); PNUA = substr(PNUA, RLENGTH + 1)
    return 1
}
'

# O CORREDOR. Em `modo=min`, so o comando de cada chamada de `rodar` e de
# `rodar_conhecido`, sem os rotulos (M|); em `modo=max`, todo o codigo (X|), e o
# comando de cada `rodar` que nao chama roteiro de `scripts/` (K|).
LEITOR_DO_CORREDOR='
function simples(cod, nua, depois,    k, n) {
    if (PAL != "rodar" && PAL != "rodar_conhecido") return
    n = (PAL == "rodar") ? 2 : 3
    PCOD = cod; PNUA = nua
    for (k = 0; k < n; k++) if (!corta()) return
    cod = normaliza(PCOD)
    if (cod == "") return
    if (modo == "min") print "M|" cod
    else if (cod !~ /scripts\/[a-z0-9-]+\.(php|sh)/) print "K|" cod
}
{
    sub(/\r$/, "")
    if (HD != "") {
        if (fecha_heredoc($0)) next
        if (modo == "max" && !HD_NULO) print "X|" $0
        next
    }
    if (!fisica($0)) next
    if (modo == "max") print "X|" LCOD
    parte()
    abre_heredoc()
    LCOD = ""; LNUA = ""
}
END {
    if (HD != "" || sq || dq || LCOD != "") {
        print "o corredor acaba dentro de um heredoc, de aspas ou de uma linha continuada" > "/dev/stderr"
        exit 3
    }
}
'

# O CI. Todo texto que nao e comentario sai em T| na hora. O comando que derruba
# o passo sai em N| (a NUA, para os nomes) e em C| (o texto, para o comando do
# `rodar` sem roteiro) -- guardado por passo e por job, porque o `if:` e o
# `continue-on-error` podem vir depois do `run:`.
LEITOR_DO_CI='
function recuo(s) { match(s, /^ */); return RLENGTH }
function valor(l) { sub(/^[ \t]*[A-Za-z_-]+:[ \t]*/, "", l); return l }

function neutro(chave, v) {
    sub(/[ \t]+#.*$/, "", v); v = normaliza(limpa(v))
    if (chave == "if") return (v ~ /false|failure[(][)]|cancelled[(][)]/)
    return (v !~ /^(false|[$][{][{] ?false ?[}][}])$/)
}

# A primeira palavra que nao e atribuicao (`VAR=valor comando`).
function primeira(nua,    w) {
    while (1) {
        sub(/^[ \t]+/, "", nua)
        if (nua == "") return ""
        match(nua, /^[^ \t]+/); w = substr(nua, 1, RLENGTH)
        if (w !~ /^[A-Za-z_][A-Za-z0-9_]*=/) return w
        nua = substr(nua, RLENGTH + 1)
    }
}

function simples(cod, nua, depois,    w) {
    w = primeira(nua)
    if (w == "") return
    if (w == "set" && nua ~ /[ \t][+][A-Za-z]*e|[ \t][+]o[ \t]+errexit/) SEM_E = 1
    if (NEG || COND || SEM_E) return
    if (depois == "||" || depois == "&&" || depois == "|" || depois == "&") return
    if (w == "echo" || w == "printf" || w == ":" || w == "true" || w == "false" || w == "test" || w == "[" || w == "[[") return
    PASSO = PASSO "N|" nua "\n" "C|" normaliza(cod) "\n"
}

function shell(s) {
    if (HD != "") {
        if (fecha_heredoc(s)) return
        print "T|" s
        return
    }
    if (!fisica(s)) return
    print "T|" LCOD
    parte()
    abre_heredoc()
    LCOD = ""; LNUA = ""
}

function fim_do_shell() {
    if (HD != "" || sq || dq || LCOD != "") ERRO = 1
    HD = ""; sq = 0; dq = 0; LCOD = ""; LNUA = ""; COND = 0; SEM_E = 0
}

function fecha_passo() {
    if (BLOCO) { BLOCO = 0; fim_do_shell() }
    if (!PASSO_NEUTRO) JOB = JOB PASSO
    PASSO = ""; PASSO_NEUTRO = 0
}

function fecha_job() {
    fecha_passo()
    if (!JOB_NEUTRO) printf "%s", JOB
    JOB = ""; JOB_NEUTRO = 0; EM_STEPS = 0; TRACO = -1; JK = -1
}

BEGIN { TRACO = -1; JK = -1; JI = -1 }
{
    sub(/\r$/, "")
    linha = $0

    # O BLOCO `run: |` vai ate a primeira linha com recuo menor ou igual ao da
    # chave; linha em branco nao o fecha. O shell ve o bloco sem o recuo dele.
    if (BLOCO) {
        if (linha ~ /^[ \t]*$/) next
        if (recuo(linha) > CHAVE) {
            if (BRECUO < 0) BRECUO = recuo(linha)
            shell(substr(linha, BRECUO + 1))
            next
        }
        BLOCO = 0; fim_do_shell()
    }
    if (linha ~ /^[ \t]*$/ || linha ~ /^[ \t]*#/) next
    r = recuo(linha)
    if (linha !~ /^[ \t]*(- )?run:/) { t = linha; sub(/[ \t]#.*$/, "", t); print "T|" t }

    if (r == 0) { fecha_job(); EM_JOBS = (linha ~ /^jobs:/); JI = -1; next }
    if (!EM_JOBS) next
    if (JI < 0) JI = r
    if (r <= JI) { fecha_job(); next }
    if (JK < 0) JK = r
    if (r <= JK) {
        fecha_passo(); EM_STEPS = 0
        if (linha ~ /^[ \t]*steps:/) { EM_STEPS = 1; TRACO = -1 }
        else if (linha ~ /^[ \t]*if:/ && neutro("if", valor(linha))) JOB_NEUTRO = 1
        else if (linha ~ /^[ \t]*continue-on-error:/ && neutro("coe", valor(linha))) JOB_NEUTRO = 1
        next
    }
    if (!EM_STEPS) next
    if (linha ~ /^[ \t]*- /) {
        if (TRACO < 0) TRACO = r
        if (r == TRACO) { fecha_passo(); sub(/- /, "  ", linha) }
    }
    if (TRACO < 0 || recuo(linha) != TRACO + 2) next
    if (linha ~ /^[ \t]*if:/) { if (neutro("if", valor(linha))) PASSO_NEUTRO = 1; next }
    if (linha ~ /^[ \t]*continue-on-error:/) { if (neutro("coe", valor(linha))) PASSO_NEUTRO = 1; next }
    if (linha !~ /^[ \t]*run:/) next
    v = valor(linha)
    if (v ~ /^[|>][-+0-9]*([ \t]+#.*)?[ \t]*$/) { BLOCO = 1; CHAVE = TRACO + 2; BRECUO = -1; next }
    shell(v); fim_do_shell()
}
END { fecha_job(); if (ERRO) exit 3 }
'

quebrou() {
    echo "Nao consegui ler $1 ate o fim (heredoc ou aspas sem fechar) -- o leitor quebrou." >&2
    exit 2
}

LEITURA_CI=$(awk "$LEITOR_DE_SHELL$LEITOR_DO_CI" "$CI") || quebrou "$CI"
LEITURA_CHAMA=$(awk -v modo=min "$LEITOR_DE_SHELL$LEITOR_DO_CORREDOR" "$CORREDOR") || quebrou "$CORREDOR"
LEITURA_TOCA=$(awk -v modo=max "$LEITOR_DE_SHELL$LEITOR_DO_CORREDOR" "$CORREDOR") || quebrou "$CORREDOR"

# AS DUAS DIFERENCAS, numa passada so. As quatro leituras de nomes (T e N do
# CI, M e X do corredor) e os dois conjuntos de comandos (C do CI, K do corredor)
# entram juntos; sai `I|` para o que so o CI tem, `R|` para o que so o corredor
# tem, e `Q|` para dizer que cada lado leu ao menos uma chamada. Um processo, e
# nao cinquenta: no Git Bash cada um custa dezenas de milissegundos, e o ensaio
# roda isto dezenas de vezes.
CONJUNTOS=$(awk '
    { tag = substr($0, 1, 1); s = substr($0, 3) }
    tag == "C" || tag == "K" { ITEM[tag, s] = 1; if (tag == "K") K[s] = 1; next }
    {
        while (match(s, /scripts\/[a-z0-9-]+\.(php|sh)/)) {
            n = substr(s, RSTART, RLENGTH); ITEM[tag, n] = 1
            if (tag == "T") CITA[n] = 1
            if (tag == "X") TOCA[n] = 1
            if (tag == "N") QN = 1
            if (tag == "M") QM = 1
            s = substr(s, RSTART + RLENGTH)
        }
    }
    END {
        for (n in CITA) if (!(("M", n) in ITEM)) print "I|" n
        for (n in TOCA) if (!(("N", n) in ITEM)) print "R|" n
        for (k in K)    if (!(("C", k) in ITEM)) print "R|" k
        if (QN) print "Q|CI"
        if (QM) print "Q|CORREDOR"
    }
' <<< "$LEITURA_CI"$'\n'"$LEITURA_CHAMA"$'\n'"$LEITURA_TOCA" | sort)

SO_NO_CI="" SO_NO_CORREDOR="" LEU_CI="" LEU_CORREDOR=""
while IFS= read -r l; do
    case $l in
        I\|*) SO_NO_CI+="${l#I|}"$'\n' ;;
        R\|*) SO_NO_CORREDOR+="${l#R|}"$'\n' ;;
        Q\|CI) LEU_CI=1 ;;
        Q\|CORREDOR) LEU_CORREDOR=1 ;;
    esac
done <<< "$CONJUNTOS"

# `contem` E `conta` SEM PROCESSO NOVO, pelo mesmo motivo.
contem() { [[ $'\n'"$1" == *$'\n'"$2"$'\n'* ]]; }
conta()  { local s n=0; while IFS= read -r s; do [ -n "$s" ] && n=$((n+1)); done <<< "$1"; echo "$n"; }

# GUARDA CONTRA VERDE SOBRE NADA: leitura que nao achou chamada nenhuma e o
# leitor quebrado, e nao dois arquivos em dia.
if [ -z "$LEU_CI" ] || [ -z "$LEU_CORREDOR" ]; then
    echo "Nao achei chamada nenhuma no $CI ou no $CORREDOR -- o leitor quebrou." >&2
    exit 2
fi

# -----------------------------------------------------------------------------
# A DECLARACAO. Cada passo exclusivo do CI diz se roda aqui e, quando nao roda,
# POR QUE -- e o "por que" e o produto desta tabela, nao a chave.
#
# O `rodar-contrato.sh` SAIU DAQUI em 07/10/2026: o `ci.yml` so o cita num
# comentario, e o que o CI roda e a suite de contrato do PHPUnit, que esta conta
# nao ve (ver o fim do relatorio completo).
declare -A DECLARADO=(
  ["scripts/verificar-schema.php"]="RODA|divergencias entre o codigo e o schema MySQL"
  ["scripts/definir-senha-contrato.php"]="PREPARO|nao e portao: define a senha do usuario de contrato"
  ["scripts/gerar-ddl-postgres.php"]="PREPARO|nao e portao: gera o DDL a partir do mysql.sql"
  ["scripts/verificar-schema-postgres.php"]="PRECISA_PG|mapeamento Doctrine contra o schema PostgreSQL"
  ["scripts/medir-telas-no-postgres.php"]="PRECISA_PG|nenhuma tela do ERP pode responder 500"
  ["scripts/medir-cron-no-postgres.php"]="PRECISA_PG|nenhuma rotina de cron pode quebrar"
  ["scripts/medir-relatorios-no-postgres.php"]="PRECISA_PG|nenhum relatorio pode quebrar"
  ["scripts/medir-adm-api-no-postgres.php"]="PRECISA_PG|nenhum endpoint de /adm/api pode quebrar"
  ["scripts/ensaiar-rotas-publicas-do-erp.php"]="PRECISA_PG|as telas publicas do ERP, sem login (D146)"
  ["scripts/ensaiar-erro-json-da-api.php"]="PRECISA_PG|erro sob /api responde JSON"
  ["scripts/ensaiar-permissao-adm-api.php"]="PRECISA_PG|a API nova recusa quem nao tem o privilegio"
  ["scripts/ensaiar-busca-de-cliente-da-agenda.php"]="PRECISA_PG|quem marca na agenda busca cliente, nas duas telas (D226)"
  ["scripts/ensaiar-sessao-do-erp.php"]="PRECISA_PG|a sessao do ERP comeca e termina onde deve (D222, D198, D223)"
  ["scripts/ensaiar-recusas-adm-api.php"]="PRECISA_PG|as 22 recusas de escrita da comanda (D170)"
  ["scripts/ensaiar-cupons-adm-api.php"]="PRECISA_PG|as campanhas de cupom pela API e pelo ERP (a tela nova)"
  ["scripts/ensaiar-fidelidade-adm-api.php"]="PRECISA_PG|o credito, os pontos e os pacotes do cliente (a tela nova)"
  ["scripts/ensaiar-usuarios-adm-api.php"]="PRECISA_PG|a lista de usuarios, os privilegios lidos e a barreira de origem (a tela nova)"
  ["scripts/ensaiar-etiquetas-adm-api.php"]="PRECISA_PG|as etiquetas dos servicos pela API (a tela nova)"
  ["scripts/ensaiar-banners-adm-api.php"]="PRECISA_PG|os banners do aplicativo pela API, com o envio de arquivo (a tela nova)"
  ["scripts/ensaiar-sorteios-adm-api.php"]="PRECISA_PG|sortear, informar o ganhador, cancelar e a planilha pela API (a tela nova)"
  ["scripts/ensaiar-saidas-adm-api.php"]="PRECISA_PG|Contas a Pagar pela API: a leitura e o dar por quitada (a tela nova)"
  ["scripts/ensaiar-vipclass-adm-api.php"]="PRECISA_PG|a VIP Class do aluno pela API: as guardas, o cadeado, o checkpoint e o certificado (a tela nova)"
  ["scripts/ensaiar-cursos-adm-api.php"]="PRECISA_PG|a administracao da VIP Class pela API: o administrador, o estado, o ciclo, a aula, a resposta correta e os arquivos em multipart (a tela nova)"
  ["scripts/ensaiar-arquivos-adm-api.php"]="PRECISA_PG|a biblioteca de arquivos: o /arquivos/ fechado e a entrega com sessao (D355)"
  ["scripts/ensaiar-whatsapp-adm-api.php"]="PRECISA_PG|o WhatsApp da unidade: as recusas da escrita, sem sair da maquina (o W10)"
  ["scripts/ensaiar-pesquisas-adm-api.php"]="PRECISA_PG|a pesquisa da adesao pela API: o aninhamento, os estados e quem escreve (a tela nova)"
  ["scripts/ensaiar-mensagens-adm-api.php"]="PRECISA_PG|os avisos do mural da agenda pela API: so o administrador escreve, o recorte de quem so le e o momento (a tela nova)"
  ["scripts/ensaiar-whatsapp-provedor-de-mentira.php"]="PRECISA_PG|o WhatsApp da unidade: o fluxo inteiro contra o provedor de mentira, no loopback"
  ["scripts/ensaiar-notificacoes-adm-api.php"]="PRECISA_PG|as campanhas: as escritas e o teste contra o provedor de mentira, no loopback (a tela nova)"
  ["scripts/ensaiar-escritas-do-aplicativo.php"]="PRECISA_PG|as seis escritas do aplicativo instalado"
  ["scripts/ensaiar-fluxo-adm-api.php"]="PRECISA_PG|um dia da barbearia pela API nova, encadeado"
  ["scripts/ensaiar-fechamento-da-agenda.php"]="NUNCA|ESCREVE na agenda; so em banco descartavel, nunca aqui"
)

# -----------------------------------------------------------------------------
# O QUE O CORREDOR RODA E O CI NAO, E POR QUE (D441). Cada linha diz o que o
# runner nao tem: a COPIA anonimizada (nas fixtures o roteiro nao tem o que
# medir), o docker do OPERADOR, o historico do GIT (o checkout e raso), o DISCO
# de quem programa (arquivo que nao e versionado), ou e o vermelho CONHECIDO.
# A chave e o roteiro, ou -- para o `rodar` que nao chama roteiro de `scripts/`
# -- o comando inteiro, com os brancos juntados, como esta no corredor.
# Medido num checkout limpo, no PHP 8.1 do conteiner, em 07/10/2026.
declare -A FORA_DO_CI=(
  ["scripts/conferir-completude-da-copia.php"]="COPIA|o maior id de cada tabela contra o AUTO_INCREMENT de producao; nas fixtures ele se declara sem o que conferir e sai 0"
  ["scripts/conferir-unicidade.php"]="COPIA|as duplicatas nas chaves que o codigo trata como unicas; sem a copia ele sai 0 sem medir"
  ["scripts/conferir-limpeza-da-unicidade.php"]="COPIA|a duplicata na origem da carga sem bloco de limpeza; nas fixtures ele sai 2"
  ["scripts/conferir-caminhos-postgres.php"]="OPERADOR|le o local.postgres.php.dist direto: o host 'postgres' do compose, que no runner nao resolve"
  ["scripts/conferir-schema-tool.sh"]="OPERADOR|chama 'docker compose exec app' e mede o esquema do banco de desenvolvimento"
  ["scripts/conferir-passo-zero-do-corte.php"]="GIT|compara com o mysql.sql do commit 3b1f05c; o checkout do CI e raso e ele sai 2"
  ["scripts/conferir-referencias-docs.php"]="DISCO|confere existencia no disco, e os docs citam o local.php e os diretorios de upload: 32 ausencias num checkout limpo"
  ["scripts/ensaiar-permissoes-com-o-nome-da-tela.php"]="OPERADOR|carrega as fixtures nos conteineres do MySQL e do PostgreSQL pelo docker exec, e nenhum job do CI tem os dois servicos (a DE3, 04/11/2026)"
  ["scripts/ensaiar-trava-do-deploy-teste.sh"]="OPERADOR|roda o deploy do teste como root e como outro usuario num conteiner descartavel da imagem do app daqui (docker run --pull=never), que o runner nao tem (a fatia 129, D444)"
  ["scripts/ensaiar-publicar-producao.sh"]="OPERADOR|o montar copia o vendor/ daqui e o resto roda num conteiner descartavel da imagem do app (docker run --pull=never); e le o commit da importacao, que o checkout raso nao tem (a fatia 129, D37)"
  ["scripts/conferir-agendamento.php"]="CONHECIDO|o D50, que espera a producao; desde 25/10/2026 o corredor o tira pelo nome (--conhecidas=), e o CI ainda nao o roda"
)

echo "=============================================================================="
echo "A LACUNA DO CI  (D187, e a outra direcao no D441)"
echo "=============================================================================="
echo

# -----------------------------------------------------------------------------
# A DECLARACAO NAS DUAS DIRECOES, antes de rodar qualquer coisa. Declaracao
# quebrada invalida o resto: nao ha o que medir sobre uma lista que mente.
problemas=0

nao_declarados=()
while IFS= read -r s; do
    [ -n "$s" ] || continue
    [ -n "${DECLARADO[$s]+x}" ] || nao_declarados+=("$s")
done <<< "$SO_NO_CI"
if [ "${#nao_declarados[@]}" -gt 0 ]; then
    echo "  PASSO NOVO NO CI, E NINGUEM DECLAROU O QUE ELE CONFERE:"
    printf '    %s\n' "${nao_declarados[@]}"
    echo
    echo "  Acrescente-o a tabela DECLARADO deste roteiro, dizendo se roda aqui"
    echo "  e, se nao roda, por que. Quem nao souber escrever o que ele defende,"
    echo "  essa e a resposta."
    echo
    problemas=$((problemas+1))
fi

sem_ci=()
while IFS= read -r s; do
    [ -n "$s" ] || continue
    [ -n "${FORA_DO_CI[$s]+x}" ] || sem_ci+=("$s")
done <<< "$SO_NO_CORREDOR"
if [ "${#sem_ci[@]}" -gt 0 ]; then
    echo "  O CORREDOR RODA, E O CI NAO -- e ninguem declarou por que (D441):"
    printf '    %s\n' "${sem_ci[@]}"
    echo
    echo "  Ponha-o no job do ci.yml que ja tem o que ele pede (os textuais no"
    echo "  'verificacao'; os de MySQL no 'contrato'; os de PostgreSQL no"
    echo "  'caracterizacao-postgres'; o front no 'front') -- ou, se o runner nao"
    echo "  tem o que ele pede, acrescente-o a tabela FORA_DO_CI com o motivo."
    echo "  Conta so o comando cuja falha derruba o passo: comentario, echo,"
    echo "  '|| true' e passo com continue-on-error nao rodam o portao."
    echo
    problemas=$((problemas+1))
fi

# A DECLARACAO QUE NAO SE APLICA MAIS tambem para o roteiro. Linha que ficou
# depois de o passo mudar de lugar e folclore, e lista que nao encolhe deixa de
# descrever (a mesma regra do `conferir-referencias-docs.php`).
velhas=()
for s in "${!DECLARADO[@]}"; do
    contem "$SO_NO_CI" "$s" || velhas+=("$s  (DECLARADO)")
done
for s in "${!FORA_DO_CI[@]}"; do
    contem "$SO_NO_CORREDOR" "$s" || velhas+=("$s  (FORA_DO_CI)")
done
if [ "${#velhas[@]}" -gt 0 ]; then
    echo "  DECLARACAO QUE NAO SE APLICA MAIS -- o passo mudou de lugar; tire a linha:"
    printf '    %s\n' "${velhas[@]}" | sort
    echo
    problemas=$((problemas+1))
fi

if [ "$problemas" -gt 0 ]; then
    exit 1
fi

echo "  O CORREDOR RODA E O CI NAO ($(conta "$SO_NO_CORREDOR"), todos declarados):"
while IFS= read -r s; do
    [ -n "$s" ] || continue
    printf '    %-42s %s\n' "${s#scripts/}" "${FORA_DO_CI[$s]%%|*} -- ${FORA_DO_CI[$s]#*|}"
done <<< "$SO_NO_CORREDOR"
echo

if [ -n "$SO_DECLARACAO" ]; then
    echo "  O CI RODA E O CORREDOR NAO: $(conta "$SO_NO_CI"), todos declarados."
    echo "  Declaracao em dia nas duas direcoes. Isto le dois arquivos: NAO diz"
    echo "  que algum passo do CI rodou, nem que passou -- isso so o log do CI"
    echo "  mostra (gh run list)."
    exit 0
fi

# O CONTEINER DA APLICACAO, e nao o shell do operador.
#
# Os portoes do PostgreSQL resolvem o host `postgres`, que e o nome do servico
# no compose. Do shell do Windows esse nome nao existe, e ate 21/09/2026 isso
# bastou para todos eles ficarem anos como 'so roda no CI'. Rodam aqui -- de
# dentro da rede, que e onde o nome resolve.
#
# O APP DESTE PROJETO, pelo compose daqui -- e nao o primeiro conteiner com
# "app" no nome. Com a pilha do ensaio do deploy de teste de pe
# (franquiabv-teste-app-1, outro worktree, outro codigo), o `name=app` a
# escolhia, e o ensaio novo saia VERMELHO por "arquivo nao existe" la dentro
# (25/09/2026). O medidor apontava defeito no codigo que nem tinha rodado.
APP=$(docker compose ps -q app 2>/dev/null | head -1)

# Teto por portao. Generoso de proposito -- a ideia e separar 'quebrou' de
# 'esta maquina nao da conta', e nao apressar ninguem.
LIMITE="${LIMITE_POR_PORTAO:-240}"

rodaram=0 verdes=0 vermelhos=0 nao_medidos=0

for s in $SO_NO_CI; do
    tipo="${DECLARADO[$s]%%|*}"
    motivo="${DECLARADO[$s]#*|}"

    case "$tipo" in
        RODA)
            printf '  %-46s ' "$(basename "$s")"
            if php "$s" >/dev/null 2>&1; then
                echo "ok"; verdes=$((verdes+1))
            else
                echo "VERMELHO"; vermelhos=$((vermelhos+1))
            fi
            rodaram=$((rodaram+1))
            ;;
        PREPARO)
            printf '  %-46s %s\n' "$(basename "$s")" "preparo, nao e portao"
            ;;
        NUNCA)
            printf '  %-46s %s\n' "$(basename "$s")" "NAO RODADO DE PROPOSITO -- $motivo"
            nao_medidos=$((nao_medidos+1))
            ;;
        PRECISA_PG)
            printf '  %-46s ' "$(basename "$s")"
            if [ -z "$APP" ]; then
                echo "NAO MEDIDO -- o conteiner da aplicacao nao esta de pe"
                nao_medidos=$((nao_medidos+1))
            # MSYS_NO_PATHCONV=1 NAO E ENFEITE: sem ele o Git Bash converte
            # `/var/www/html/...` num caminho do Windows antes de o docker ver,
            # e TODOS os portoes saem vermelhos por um defeito do medidor.
            # Custou uma rodada inteira de 11 falsos vermelhos em 21/09/2026.
            else
                # TEMPO ESTOURADO NAO E VERMELHO, e a diferenca importa.
                #
                # `medir-cron-no-postgres` pede rotinas que levam MINUTOS
                # nesta maquina, e o nginx corta em 504. Contar isso como
                # portao vermelho seria o medidor mentindo: o defeito seria
                # do ambiente, nao do codigo. Sai como NAO MEDIDO.
                MSYS_NO_PATHCONV=1 timeout "$LIMITE" docker exec "$APP" php "/var/www/html/$s" >/dev/null 2>&1
                caso=$?
                rodaram=$((rodaram+1))
                case $caso in
                    0)   echo "ok"; verdes=$((verdes+1)) ;;
                    124) echo "NAO MEDIDO -- estourou ${LIMITE}s nesta maquina"
                         nao_medidos=$((nao_medidos+1)); rodaram=$((rodaram-1)) ;;
                    2)   # SAIDA 2 E A CONVENCAO DESTA CASA PARA 'nao consegui
                         # medir' -- e nao para 'achei defeito'. Estes ensaios
                         # pedem o banco de CONTRATO e a senha que o
                         # `definir-senha-contrato.php` grava nele. Chegar la
                         # exige repontar o `local.php`, que e a disputa que ja
                         # deu falso vermelho duas vezes -- por isso este
                         # roteiro NAO reponta nada sozinho.
                         echo "NAO MEDIDO -- pede o banco de contrato (rode montar-banco-contrato.sh antes)"
                         nao_medidos=$((nao_medidos+1)); rodaram=$((rodaram-1)) ;;
                    *)   echo "VERMELHO (saida $caso)"; vermelhos=$((vermelhos+1)) ;;
                esac
            fi
            ;;
        *)
            printf '  %-46s %s\n' "$(basename "$s")" "NAO MEDIDO -- tipo nao tratado: $tipo"
            nao_medidos=$((nao_medidos+1))
            ;;
    esac
done

echo
echo "  rodaram aqui ...... $rodaram  ($verdes ok, $vermelhos vermelho)"
echo "  NAO MEDIDOS ....... $nao_medidos"
echo
echo "  Para medir os que faltam, suba o PostgreSQL e monte o cenario:"
echo "    docker compose --profile fase1 up -d postgres"
echo "    bash scripts/montar-banco-postgres.sh   # se existir"
echo
# AS SUITES DO PHPUNIT NAO SAO ROTEIROS DE `scripts/`, e esta conta nao as ve.
# O CI roda a de contrato e a de caracterizacao nos tres bancos. Aqui, cada uma
# SOZINHA: as duas trocam o `local.php`, e junto com o `conferir-tudo.sh` dao
# falso vermelho no schema-tool -- ja custou duas vezes.
echo "  E AS SUITES DO PHPUNIT, que o CI roda e esta conta nao ve -- cada uma"
echo "  SOZINHA, nunca junto com o conferir-tudo.sh (disputam o local.php):"
echo "    bash scripts/rodar-contrato.sh"
echo "    sh scripts/rodar-caracterizacao-como-ci.sh"
echo
echo "  ENQUANTO O CI NAO VOLTA, os $nao_medidos acima NAO ESTAO CONFERIDOS."
echo "  Verde aqui nao cobre o que nao rodou. Ver D187."

[ "$vermelhos" -gt 0 ] && exit 1
exit 0
