#!/usr/bin/env bash
#
# recreer_staging.sh — ouvre une campagne de recette en recréant `staging`
# depuis `development`, en refusant les pièges connus.
#
# À QUOI ÇA SERT
#   Le modèle de branches retenu le 25/08 (décision Enguerran, issue #658) :
#   UNE branche de recette à NOM FIXE, `staging`, sur laquelle CI et CD jouent
#   en permanence — « il n'y a plus de doute : la seule branche sur laquelle CI
#   et CD jouent est staging ». Cette branche est RECRÉÉE depuis `development` à
#   l'ouverture de chaque campagne, jamais rattrapée par des merges successifs :
#   une branche permanente qui rate un report garde sa divergence en silence
#   (la maladie chronique du dépôt — #137, #603, .htaccess).
#
# QUAND ON LE JOUE
#   À l'OUVERTURE d'une campagne de recette, une fois, et JAMAIS pendant.
#   Pendant une campagne, `staging` ne bouge que par les correctifs de recette
#   (branche de recette PUIS `development`, jamais une seule des deux).
#   Le jouer en cours de campagne renvoie les testeurs sur un écran qui n'est
#   plus celui qu'ils recettent.
#
# LES DEUX VERROUS, ET POURQUOI ILS EXISTENT
#   1. REFUS SI `staging` PORTE DU CODE ABSENT DE `development`.
#      C'est le contrôle qui manquait à FRONT#154 : un correctif appliqué sur la
#      branche de recette et jamais reporté dans `development` disparaît au
#      premier reset — sans bruit, sans trace, et il revient en régression trois
#      semaines plus tard. Ici il BLOQUE la cérémonie : on reporte, puis on
#      relance. Le script ne choisit jamais quoi effacer.
#   2. TAG ANNOTÉ `recette-<campagne>-fin` SUR L'ANCIENNE POINTE, POUSSÉ AVANT
#      TOUT RESET. Perte impossible par construction : même en cas d'erreur
#      d'appréciation humaine, l'état recetté reste atteignable. Un tag est
#      immuable ; une branche d'archive dérive. L'ordre (tag PUIS reset) fait
#      partie du contrat et il est vérifié par `--autotest`.
#
#   Ordre de risque, tel que consigné sur #658 :
#     `staging` fixe AVEC cérémonie scriptée  <  `release/**` + garde
#                                             <  `staging` fixe SANS script.
#   Sans script, un force-push permanent à main levée est le pire des mondes.
#
# CE QUE CE SCRIPT NE FAIT PAS — ET LE POINT D'ENTRÉE D'UNE CAMPAGNE
#   Ce script est PUR GIT : tag, verrous, reset. Il ne règle RIEN côté GitHub
#   et ne bascule AUCUN réglage d'API. La protection de `staging` refuse le
#   force-push EN PERMANENCE — y compris pour nous, et surtout pour les comptes
#   externes qui ont `push` sur les dépôts : une fenêtre ouverte en continu
#   couvrirait une réécriture silencieuse de la branche de recette.
#
#   ➡️ LE POINT D'ENTRÉE DE TOUTE CAMPAGNE EST L'ENVELOPPE
#   `scripts/ouvrir_campagne_staging.sh` (conversation structure, #659) :
#   elle OUVRE la fenêtre de force-push (API), appelle CETTE cérémonie, et
#   REFERME dans tous les cas (trap), état relu. Jouée NUE, cette cérémonie
#   échoue au force-push hors fenêtre : c'est le comportement VOULU (charte §6
#   — un garde-fou se VOIT échouer avant d'être cru), pas une panne.
#   La création de la règle de protection, le contrôle requis et la
#   documentation des environnements appartiennent aussi à la structure
#   (#657, #658) — PAS à ce script.
#
# LA PREMIÈRE CÉRÉMONIE RÉELLE
#   `staging` n'existe pas encore : la première cérémonie sera une CRÉATION
#   SIMPLE — pas de force-push, pas de tag, pas de fenêtre. C'est exactement le
#   scénario S1 de `--autotest`. Elle se joue VIA LE SCRIPT (jamais à la main),
#   sur GO du chapeau, et chaque écart entre ce que le script annonce et ce qui
#   se passe réellement se consigne. La VRAIE épreuve des verrous — dérive non
#   reportée, tag, fenêtre de force-push — viendra à la DEUXIÈME cérémonie :
#   elle se jouera comme une épreuve, pas comme une routine.
#
#   Il ne touche AUCUN fichier et ne bascule AUCUNE branche locale : il lit
#   `origin/*`, pose un tag, et pousse. Un worktree sale n'est pas un problème.
#
# USAGE
#   scripts/recreer_staging.sh --campagne 2026-09
#   scripts/recreer_staging.sh --campagne 2026-09-bis --dry-run   # répétition
#   scripts/recreer_staging.sh --autotest      # épreuve d'échec, dépôt jetable
#
# CODES DE SORTIE
#   0 cérémonie faite (ou répétition, ou autotest vert)
#   1 usage
#   2 REFUS d'un pré-contrôle (dérive non reportée, PR ouverte, gh absent…)
#   3 REFUS du verrou d'archive (le tag de cette campagne existe déjà)
#   4 POST-CONTRÔLE en échec (l'état poussé n'est pas celui attendu)
#   5 autotest rouge
#
set -euo pipefail

BRANCHE_RECETTE="staging"
BRANCHE_SOURCE="development"
REMOTE="origin"

CAMPAGNE=""
DRY_RUN=0
AUTOTEST=0

# Crochet de testabilité (utilisé par --autotest uniquement) : si la variable
# CEREMONIE_TRACE_FICHIER est définie, chaque action irréversible y écrit une
# ligne. C'est ce qui permet de PROUVER que le tag est posé AVANT le reset,
# plutôt que de le supposer. Non défini en usage normal : aucun effet.
tracer() {
    [[ -n "${CEREMONIE_TRACE_FICHIER:-}" ]] && echo "$*" >>"$CEREMONIE_TRACE_FICHIER"
    return 0
}

rouge() { printf '\033[31m%s\033[0m\n' "$*"; }
vert()  { printf '\033[32m%s\033[0m\n' "$*"; }
gris()  { printf '\033[90m%s\033[0m\n' "$*"; }

usage() {
    sed -n '2,66p' "$0" | sed 's/^# \{0,1\}//'
    exit 1
}

while [[ $# -gt 0 ]]; do
    case "$1" in
        --campagne) CAMPAGNE="${2:-}"; shift 2 ;;
        --dry-run)  DRY_RUN=1; shift ;;
        --autotest) AUTOTEST=1; shift ;;
        -h|--help)  usage ;;
        *) rouge "Argument inconnu : $1"; echo; usage ;;
    esac
done

# ═════════════════════════════════════════════════════════════════════════════
#  LA CÉRÉMONIE
# ═════════════════════════════════════════════════════════════════════════════
ceremonie() {
    local campagne="$1"
    local tag="recette-${campagne}-fin"

    echo "── Ouverture de la campagne de recette « $campagne » ──"
    gris "   $BRANCHE_SOURCE → $BRANCHE_RECETTE   ·   tag d'archive : $tag"
    [[ $DRY_RUN -eq 1 ]] && gris "   (répétition : rien ne sera poussé)"
    echo

    # ── 1. Pré-contrôle (a) : fetch frais ────────────────────────────────────
    echo "── Pré-contrôles ──"
    if ! git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
        rouge "REFUS — hors dépôt git."
        return 2
    fi
    if ! git remote get-url "$REMOTE" >/dev/null 2>&1; then
        rouge "REFUS — pas de remote '$REMOTE'."
        return 2
    fi
    if ! git fetch "$REMOTE" --prune --tags --quiet; then
        rouge "REFUS — le fetch de '$REMOTE' a échoué : on ne travaille pas sur une vue périmée."
        return 2
    fi
    vert "  (a) fetch frais de '$REMOTE'"

    local sha_source
    if ! sha_source=$(git rev-parse --verify --quiet "refs/remotes/$REMOTE/$BRANCHE_SOURCE"); then
        rouge "REFUS — '$REMOTE/$BRANCHE_SOURCE' est introuvable."
        return 2
    fi

    local sha_recette=""
    local staging_existe=0
    if sha_recette=$(git rev-parse --verify --quiet "refs/remotes/$REMOTE/$BRANCHE_RECETTE"); then
        staging_existe=1
    fi

    # ── 2. Pré-contrôle (b) : aucune dérive non reportée ─────────────────────
    #    LE contrôle qui manquait à FRONT#154.
    if [[ $staging_existe -eq 1 ]]; then
        local derive
        derive=$(git log --oneline --no-decorate \
                     "refs/remotes/$REMOTE/$BRANCHE_SOURCE..refs/remotes/$REMOTE/$BRANCHE_RECETTE" || true)
        if [[ -n "$derive" ]]; then
            rouge "  (b) REFUS — '$BRANCHE_RECETTE' porte du code ABSENT de '$BRANCHE_SOURCE' :"
            echo "$derive" | sed 's/^/        /'
            echo
            rouge "Ces commits seraient EFFACÉS par la recréation. Un correctif de recette"
            rouge "non reporté dans '$BRANCHE_SOURCE' bloque la cérémonie — c'est exactement"
            rouge "l'incident FRONT#154, où la disparition avait été silencieuse."
            gris  "  → reporte-les dans '$BRANCHE_SOURCE' (PR normale), puis relance."
            gris  "  → un correctif de recette va TOUJOURS dans les deux branches, jamais une seule."
            return 2
        fi
        vert "  (b) aucune dérive : '$BRANCHE_RECETTE' n'a rien que '$BRANCHE_SOURCE' n'ait"
    else
        gris "  (b) '$REMOTE/$BRANCHE_RECETTE' n'existe pas encore — rien à comparer (premier lancement)"
    fi

    # ── 3. Pré-contrôle (c) : aucune PR ouverte vers staging ─────────────────
    if [[ "${CEREMONIE_SANS_GH:-0}" == "1" ]]; then
        gris "  (c) contrôle des PR ouvertes vers '$BRANCHE_RECETTE' COURT-CIRCUITÉ"
        gris "      (CEREMONIE_SANS_GH=1 — réservé à --autotest, qui n'a ni réseau ni dépôt GitHub)"
    else
        if ! command -v gh >/dev/null 2>&1; then
            rouge "  (c) REFUS — 'gh' est absent : impossible de vérifier les PR ouvertes vers '$BRANCHE_RECETTE'."
            gris  "      Un contrôle qu'on ne peut pas jouer n'est pas un contrôle passé."
            return 2
        fi
        if ! gh auth status >/dev/null 2>&1; then
            rouge "  (c) REFUS — 'gh' n'est pas authentifié."
            return 2
        fi
        local prs
        if ! prs=$(gh pr list --base "$BRANCHE_RECETTE" --state open \
                       --json number,title,headRefName \
                       --jq '.[] | "#\(.number) \(.headRefName) — \(.title)"' 2>/dev/null); then
            rouge "  (c) REFUS — 'gh pr list' a échoué."
            return 2
        fi
        if [[ -n "$prs" ]]; then
            rouge "  (c) REFUS — des PR ouvertes ciblent '$BRANCHE_RECETTE' :"
            echo "$prs" | sed 's/^/        /'
            echo
            rouge "Recréer la branche sous ces PR les rendrait incohérentes (base réécrite)."
            gris  "  → ferme-les ou re-cible-les vers '$BRANCHE_SOURCE', puis relance."
            return 2
        fi
        vert "  (c) aucune PR ouverte vers '$BRANCHE_RECETTE'"
    fi

    echo
    echo "── Verrou d'archive ──"

    # ── 4. Verrou : tag annoté sur l'ancienne pointe, POUSSÉ AVANT tout reset ─
    if [[ $staging_existe -eq 0 ]]; then
        gris "  Premier lancement : '$REMOTE/$BRANCHE_RECETTE' n'existe pas, il n'y a aucune"
        gris "  pointe à archiver. Étape sautée — aucun tag ne sera posé."
    else
        if git rev-parse --verify --quiet "refs/tags/$tag" >/dev/null; then
            rouge "REFUS — le tag '$tag' existe déjà (en local)."
            rouge "Une campagne ne se clôt pas deux fois : soit la cérémonie a déjà été jouée,"
            rouge "soit le nom de campagne est erroné."
            gris  "  → nomme la nouvelle campagne autrement (--campagne 2026-09-bis)."
            gris  "  → ou, si ce tag local est un résidu : git tag -d $tag"
            return 3
        fi
        if [[ -n "$(git ls-remote --tags "$REMOTE" "refs/tags/$tag" 2>/dev/null)" ]]; then
            rouge "REFUS — le tag '$tag' existe déjà sur '$REMOTE'."
            rouge "Une campagne ne se clôt pas deux fois."
            gris  "  → nomme la nouvelle campagne autrement (--campagne ${campagne}-bis)."
            return 3
        fi

        if [[ "$sha_recette" == "$sha_source" ]]; then
            gris "  no-op : '$BRANCHE_RECETTE' est déjà à la pointe de '$BRANCHE_SOURCE'."
            gris "  Le tag de clôture est tout de même posé (l'état recetté reste nommé),"
            gris "  et la recréation ci-dessous ne déplacera rien."
        fi

        if [[ $DRY_RUN -eq 1 ]]; then
            gris "  [répétition] git tag -a $tag ${sha_recette:0:8} && git push $REMOTE refs/tags/$tag"
        else
            git tag -a "$tag" "$sha_recette" \
                -m "Fin de la campagne de recette $campagne — pointe de $BRANCHE_RECETTE archivée avant recréation depuis $BRANCHE_SOURCE"
            if ! git push "$REMOTE" "refs/tags/$tag" --quiet; then
                git tag -d "$tag" >/dev/null 2>&1 || true
                rouge "REFUS — le tag n'a pas pu être poussé : AUCUN reset ne sera tenté."
                rouge "Le verrou de perte-impossible n'est pas en place, donc la cérémonie s'arrête ici."
                return 2
            fi
            tracer "tag $tag $sha_recette"
            vert "  tag '$tag' posé sur ${sha_recette:0:8} et poussé — l'état recetté est atteignable pour toujours"
        fi
    fi

    # ── 5. Recréation : force-push, jamais delete/recreate ───────────────────
    echo
    echo "── Recréation de '$BRANCHE_RECETTE' ──"
    if [[ $DRY_RUN -eq 1 ]]; then
        gris "  [répétition] git push --force-with-lease $REMOTE ${sha_source:0:8}:refs/heads/$BRANCHE_RECETTE"
        echo
        vert "Répétition terminée : la cérémonie passerait."
        gris "  ancienne pointe : ${sha_recette:-(aucune)}"
        gris "  nouvelle pointe : $sha_source"
        if [[ $staging_existe -eq 1 ]]; then
            gris "  tag qui serait posé : $tag"
        else
            gris "  tag qui serait posé : (aucun — premier lancement)"
        fi
        return 0
    fi

    if [[ $staging_existe -eq 1 ]]; then
        # --force-with-lease avec la valeur attendue explicite : si quelqu'un a
        # poussé sur staging entre le fetch et maintenant, le push échoue au lieu
        # d'écraser. La dérive contrôlée en (b) reste vraie au moment du push.
        if ! git push --force-with-lease="$BRANCHE_RECETTE:$sha_recette" \
                 "$REMOTE" "$sha_source:refs/heads/$BRANCHE_RECETTE" --quiet; then
            rouge "REFUS — le force-push a échoué."
            gris  "  Deux causes probables :"
            gris  "   · la protection de branche de '$BRANCHE_RECETTE' n'autorise pas le force-push"
            gris  "     → réglage GitHub, porté par la conversation structure (#657/#658), pas par ce script ;"
            gris  "   · quelqu'un a poussé sur '$BRANCHE_RECETTE' depuis le fetch (le bail a sauté)."
            gris  "  Le tag '$tag' est déjà poussé : rien n'est perdu, relance après correction."
            return 2
        fi
    else
        if ! git push "$REMOTE" "$sha_source:refs/heads/$BRANCHE_RECETTE" --quiet; then
            rouge "REFUS — la création de '$BRANCHE_RECETTE' a échoué."
            return 2
        fi
    fi
    tracer "reset $BRANCHE_RECETTE $sha_source"
    vert "  '$BRANCHE_RECETTE' pointe désormais sur ${sha_source:0:8}"

    # ── 6. Post-contrôles ────────────────────────────────────────────────────
    echo
    echo "── Post-contrôles ──"
    git fetch "$REMOTE" --prune --tags --quiet

    local sha_verif
    sha_verif=$(git rev-parse --verify --quiet "refs/remotes/$REMOTE/$BRANCHE_RECETTE" || echo "")
    if [[ "$sha_verif" != "$sha_source" ]]; then
        rouge "  POST-CONTRÔLE ROUGE — '$REMOTE/$BRANCHE_RECETTE' vaut ${sha_verif:-(rien)}"
        rouge "  au lieu de $sha_source. Ne lance PAS la campagne."
        return 4
    fi
    vert "  '$REMOTE/$BRANCHE_RECETTE' == la pointe poussée de '$BRANCHE_SOURCE' ($sha_source)"

    if [[ $staging_existe -eq 1 ]]; then
        if [[ -z "$(git ls-remote --tags "$REMOTE" "refs/tags/$tag" 2>/dev/null)" ]]; then
            rouge "  POST-CONTRÔLE ROUGE — le tag '$tag' est absent de '$REMOTE'."
            return 4
        fi
        vert "  tag '$tag' présent sur '$REMOTE'"
    fi

    local sha_source_apres
    sha_source_apres=$(git rev-parse --verify --quiet "refs/remotes/$REMOTE/$BRANCHE_SOURCE" || echo "")
    if [[ "$sha_source_apres" != "$sha_source" ]]; then
        gris "  note : '$BRANCHE_SOURCE' a avancé pendant la cérémonie (${sha_source_apres:0:8})."
        gris "  '$BRANCHE_RECETTE' porte l'état figé ${sha_source:0:8} — c'est voulu : une campagne"
        gris "  recette un instantané, pas une branche mouvante."
    fi

    # ── Récapitulatif ────────────────────────────────────────────────────────
    echo
    echo "── Campagne « $campagne » ouverte ──"
    if [[ $staging_existe -eq 1 ]]; then
        printf '  ancienne pointe (archivée) : %s  → tag %s\n' "$sha_recette" "$tag"
    else
        printf '  ancienne pointe (archivée) : (aucune — premier lancement, pas de tag)\n'
    fi
    printf '  nouvelle pointe            : %s\n' "$sha_source"
    echo
    gris "  Pendant la campagne : '$BRANCHE_RECETTE' ne bouge QUE par correctif de recette,"
    gris "  et tout correctif de recette part aussi dans '$BRANCHE_SOURCE' — sinon la prochaine"
    gris "  cérémonie refusera (verrou b), ce qui est le comportement voulu."
    return 0
}

# ═════════════════════════════════════════════════════════════════════════════
#  L'ÉPREUVE D'ÉCHEC — --autotest
#
#  Exigée par la décision du 25/08 : « un garde-fou qui n'a jamais rougi ne
#  prouve rien » (les 5 dispositifs faussement actifs). L'autotest monte un
#  dépôt git JETABLE (bare local en guise d'origin + clone), y rejoue la
#  cérémonie, et vérifie que chaque REFUS tombe — avec le bon message, et sans
#  effet de bord. Aucun réseau, aucun contact avec le vrai origin : le contrôle
#  des PR est court-circuité par CEREMONIE_SANS_GH=1, et le script le DIT.
# ═════════════════════════════════════════════════════════════════════════════
SOI="$(cd "$(dirname "$0")" && pwd)/$(basename "$0")"
TMP_AUTOTEST=""
SCN_TOTAL=0
SCN_VERTS=0
SCN_ECHEC=0

nettoyer_autotest() {
    [[ -n "$TMP_AUTOTEST" && -d "$TMP_AUTOTEST" ]] && rm -rf "$TMP_AUTOTEST"
    return 0
}

gitc() { git -C "$TMP_AUTOTEST/clone" "$@"; }

commiter() {
    echo "$2" >>"$TMP_AUTOTEST/clone/$1"
    gitc add "$1"
    gitc -c user.name=Autotest -c user.email=autotest@local commit -q -m "$2"
}

scenario() {
    SCN_TOTAL=$((SCN_TOTAL + 1))
    SCN_ECHEC=0
    echo
    echo "═══ S$SCN_TOTAL — $1"
}

verifier() {
    if [[ "$2" == "0" ]]; then
        vert "    ✓ $1"
    else
        rouge "    ✗ $1"
        SCN_ECHEC=1
    fi
}

clore_scenario() {
    if [[ $SCN_ECHEC -eq 0 ]]; then
        SCN_VERTS=$((SCN_VERTS + 1))
        vert "    S$SCN_TOTAL VERT"
    else
        rouge "    S$SCN_TOTAL ROUGE"
    fi
}

# Joue la cérémonie dans le dépôt jetable. Renseigne SORTIE et CODE.
SORTIE=""
CODE=0
jouer() {
    set +e
    SORTIE=$(cd "$TMP_AUTOTEST/clone" && \
             CEREMONIE_SANS_GH=1 \
             CEREMONIE_TRACE_FICHIER="${CEREMONIE_TRACE_FICHIER:-}" \
             bash "$SOI" "$@" 2>&1)
    CODE=$?
    set -e
}

contient() { grep -qi -- "$1" <<<"$SORTIE"; }

sha_distant() {
    git -C "$TMP_AUTOTEST/origin.git" rev-parse --verify --quiet "$1" || echo ""
}

autotest() {
    echo "══ Épreuve d'échec de recreer_staging.sh ══"
    gris "  dépôt jetable, aucun réseau, aucun contact avec le vrai origin."
    gris "  Le contrôle (c) « PR ouvertes » y est court-circuité (CEREMONIE_SANS_GH=1) :"
    gris "  il n'y a pas de dépôt GitHub derrière un bare local, et c'est dit à chaque passe."

    TMP_AUTOTEST=$(mktemp -d "${TMPDIR:-/tmp}/ceremonie-autotest.XXXXXX")
    trap nettoyer_autotest EXIT

    git init --bare --quiet "$TMP_AUTOTEST/origin.git"
    git clone --quiet "$TMP_AUTOTEST/origin.git" "$TMP_AUTOTEST/clone" 2>/dev/null
    gitc config user.name Autotest
    gitc config user.email autotest@local
    gitc checkout -q -b "$BRANCHE_SOURCE" 2>/dev/null || gitc checkout -q "$BRANCHE_SOURCE"
    commiter socle.txt "socle initial"
    gitc push -q -u origin "$BRANCHE_SOURCE"

    # ── S1 : premier lancement, staging n'existe pas ─────────────────────────
    scenario "premier lancement (staging inexistante) — création propre, aucun tag"
    jouer --campagne 2026-09
    verifier "sortie 0 (obtenu : $CODE)" "$([[ $CODE -eq 0 ]] && echo 0 || echo 1)"
    verifier "l'étape d'archive est sautée avec un message explicite" \
        "$(contient "Premier lancement" && echo 0 || echo 1)"
    verifier "le court-circuit du contrôle PR est annoncé" \
        "$(contient "COURT-CIRCUITÉ" && echo 0 || echo 1)"
    local dev1 stg1
    dev1=$(sha_distant "refs/heads/$BRANCHE_SOURCE")
    stg1=$(sha_distant "refs/heads/$BRANCHE_RECETTE")
    verifier "staging créée à la pointe de development" \
        "$([[ -n "$stg1" && "$stg1" == "$dev1" ]] && echo 0 || echo 1)"
    verifier "aucun tag posé (il n'y avait rien à archiver)" \
        "$([[ -z "$(git -C "$TMP_AUTOTEST/origin.git" tag -l)" ]] && echo 0 || echo 1)"
    clore_scenario

    # ── S2 : dérive non reportée → REFUS, sans effet de bord ─────────────────
    scenario "staging porte un commit absent de development → REFUS, rien touché"
    gitc checkout -q -B "$BRANCHE_RECETTE" "origin/$BRANCHE_RECETTE"
    commiter correctif.txt "correctif de recette jamais reporte"
    gitc push -q origin "$BRANCHE_RECETTE"
    gitc checkout -q "$BRANCHE_SOURCE"
    local stg_derive
    stg_derive=$(sha_distant "refs/heads/$BRANCHE_RECETTE")

    jouer --campagne 2026-10
    verifier "sortie 2 (refus de pré-contrôle) (obtenu : $CODE)" "$([[ $CODE -eq 2 ]] && echo 0 || echo 1)"
    verifier "le message nomme la dérive et FRONT#154" \
        "$(contient "ABSENT de" && contient "FRONT#154" && echo 0 || echo 1)"
    verifier "le commit menacé est listé" \
        "$(contient "correctif de recette jamais reporte" && echo 0 || echo 1)"
    verifier "AUCUN tag n'a été posé sur origin" \
        "$([[ -z "$(git -C "$TMP_AUTOTEST/origin.git" tag -l)" ]] && echo 0 || echo 1)"
    verifier "staging N'A PAS bougé" \
        "$([[ "$(sha_distant "refs/heads/$BRANCHE_RECETTE")" == "$stg_derive" ]] && echo 0 || echo 1)"
    clore_scenario

    # ── S3 : chemin nominal — tag AVANT reset ────────────────────────────────
    scenario "chemin nominal après report — tag posé AVANT le reset, staging == development"
    gitc checkout -q "$BRANCHE_SOURCE"
    gitc -c user.name=Autotest -c user.email=autotest@local \
        merge -q --no-edit "origin/$BRANCHE_RECETTE"     # le correctif est reporté
    commiter socle.txt "une fonctionnalite de plus dans development"
    gitc push -q origin "$BRANCHE_SOURCE"
    local ancien_stg dev3
    ancien_stg=$(sha_distant "refs/heads/$BRANCHE_RECETTE")
    dev3=$(sha_distant "refs/heads/$BRANCHE_SOURCE")

    CEREMONIE_TRACE_FICHIER="$TMP_AUTOTEST/trace.txt"
    : >"$CEREMONIE_TRACE_FICHIER"
    jouer --campagne 2026-10
    local ordre premiere_ligne seconde_ligne
    premiere_ligne=$(sed -n '1p' "$CEREMONIE_TRACE_FICHIER" || echo "")
    seconde_ligne=$(sed -n '2p' "$CEREMONIE_TRACE_FICHIER" || echo "")
    ordre=$([[ "$premiere_ligne" == tag\ * && "$seconde_ligne" == reset\ * ]] && echo 0 || echo 1)
    CEREMONIE_TRACE_FICHIER=""

    verifier "sortie 0 (obtenu : $CODE)" "$([[ $CODE -eq 0 ]] && echo 0 || echo 1)"
    verifier "ORDRE : le tag est poussé AVANT le reset (trace : « $premiere_ligne » puis « $seconde_ligne »)" "$ordre"
    verifier "staging == development ($dev3)" \
        "$([[ "$(sha_distant "refs/heads/$BRANCHE_RECETTE")" == "$dev3" ]] && echo 0 || echo 1)"
    verifier "le tag recette-2026-10-fin est sur origin" \
        "$([[ -n "$(git -C "$TMP_AUTOTEST/origin.git" tag -l recette-2026-10-fin)" ]] && echo 0 || echo 1)"
    verifier "le tag pointe sur l'ANCIENNE pointe de staging (${ancien_stg:0:8})" \
        "$([[ "$(git -C "$TMP_AUTOTEST/origin.git" rev-parse -q --verify 'recette-2026-10-fin^{commit}' 2>/dev/null)" == "$ancien_stg" ]] && echo 0 || echo 1)"
    verifier "le post-contrôle a confirmé la pointe" \
        "$(contient "Post-contrôles" && contient "Campagne « 2026-10 » ouverte" && echo 0 || echo 1)"
    clore_scenario

    # ── S4 : tag déjà existant → REFUS ───────────────────────────────────────
    scenario "cérémonie rejouée avec la MÊME campagne (tag existant) → REFUS"
    local stg_avant4
    stg_avant4=$(sha_distant "refs/heads/$BRANCHE_RECETTE")
    jouer --campagne 2026-10
    verifier "sortie 3 (refus du verrou d'archive) (obtenu : $CODE)" "$([[ $CODE -eq 3 ]] && echo 0 || echo 1)"
    verifier "le message dit qu'une campagne ne se clôt pas deux fois" \
        "$(contient "ne se clôt pas deux fois" && echo 0 || echo 1)"
    verifier "staging inchangée" \
        "$([[ "$(sha_distant "refs/heads/$BRANCHE_RECETTE")" == "$stg_avant4" ]] && echo 0 || echo 1)"
    clore_scenario

    # ── S5 : rejeu sur une staging déjà propre, campagne neuve → no-op ───────
    #    Comportement DOCUMENTÉ (choix de conception) : la cérémonie passe et
    #    n'est qu'un no-op ; le tag de clôture est tout de même posé, parce que
    #    « la campagne est close » est une information qui vaut d'être nommée.
    scenario "rejeu sur staging déjà recréée et propre, campagne neuve → no-op propre"
    jouer --campagne 2026-11
    verifier "sortie 0 (obtenu : $CODE)" "$([[ $CODE -eq 0 ]] && echo 0 || echo 1)"
    verifier "le no-op est ANNONCÉ, pas subi" "$(contient "no-op" && echo 0 || echo 1)"
    verifier "staging toujours == development" \
        "$([[ "$(sha_distant "refs/heads/$BRANCHE_RECETTE")" == "$(sha_distant "refs/heads/$BRANCHE_SOURCE")" ]] && echo 0 || echo 1)"
    verifier "le tag recette-2026-11-fin est posé" \
        "$([[ -n "$(git -C "$TMP_AUTOTEST/origin.git" tag -l recette-2026-11-fin)" ]] && echo 0 || echo 1)"
    clore_scenario

    # ── Récapitulatif ────────────────────────────────────────────────────────
    echo
    echo "══ Récapitulatif ══"
    if [[ $SCN_VERTS -eq $SCN_TOTAL ]]; then
        vert "  $SCN_TOTAL scénarios, $SCN_VERTS verts, 0 rouge."
        return 0
    fi
    rouge "  $SCN_TOTAL scénarios, $SCN_VERTS verts, $((SCN_TOTAL - SCN_VERTS)) ROUGE(S)."
    rouge "  Un verrou de la cérémonie ne tient pas : ne joue pas ce script sur le vrai dépôt."
    return 5
}

# ═════════════════════════════════════════════════════════════════════════════
#  Point d'entrée
# ═════════════════════════════════════════════════════════════════════════════
if [[ $AUTOTEST -eq 1 ]]; then
    [[ -n "$CAMPAGNE" ]] && gris "(--campagne ignoré en mode --autotest)"
    autotest
    exit $?
fi

if [[ -z "$CAMPAGNE" ]]; then
    rouge "--campagne <AAAA-MM[-suffixe]> est obligatoire : il nomme le tag d'archive."
    echo
    usage
fi

if ! [[ "$CAMPAGNE" =~ ^[0-9]{4}-(0[1-9]|1[0-2])(-[A-Za-z0-9._-]+)?$ ]]; then
    rouge "Campagne '$CAMPAGNE' mal formée."
    gris  "Attendu : AAAA-MM, éventuellement suffixée — 2026-09, 2026-09-bis, 2026-11-hotfix."
    gris  "Le nom sert au tag 'recette-<campagne>-fin' : il doit rester lisible dans dix mois."
    exit 1
fi

ceremonie "$CAMPAGNE"
exit $?
