/* Copyright 2026 Rhett Creighton - Apache License 2.0
 *
 * test_reducer_forward_progress_gate — the MULTI-BLOCK forward-progress +
 * REORG gate (the hermetic live-wedge repro harness).
 *
 * WHY THIS GATE EXISTS (the live-wedge axis)
 * ------------------------------------------
 * test_reducer_block_ingest_gate proves ONE mined regtest block finalizes
 * through the reducer front door to tip+1. The #1 v1 blocker, though, is the
 * LIVE WEDGE: on the mainnet datadir the tip HOLDS at some height without
 * finalizing forward — tip_finalize oscillates, finalized_total stalls. A
 * single-block gate cannot catch a wedge that only appears after several
 * sequential finalizations. This gate extends the proven it-works pattern to
 * MANY blocks and asserts MONOTONIC forward progress, so if the reducer ever
 * stalls or oscillates mid-run, it is reproduced HERMETICALLY (a deterministic,
 * in-process repro of the v1 blocker — far cheaper to debug than the live node).
 *
 * WHAT THIS PROVES
 * ----------------
 *   PART 1 (forward progress): mine + ingest N = 32 sequential regtest blocks
 *     through reducer_ingest_block (the SAME front door live intake uses),
 *     looping reducer_kick to convergence after each. Assert the authoritative
 *     active_chain_height reaches exactly 32 with NO stall and NO oscillation:
 *     finalized_total strictly increases each step and never retreats, and the
 *     tip never goes backward. If the tip stalls at H<32 or oscillates, the
 *     live-wedge failure mode is reproduced and the stall height + the exact
 *     eight stage cursors are captured in the output.
 *
 *   PART 2 (reorg): from a fork point near the tip (within ZCL_FINALITY_DEPTH),
 *     mine a competing branch that is HEAVIER (one block longer above the fork),
 *     install it on the active chain, and drive the reducer to convergence.
 *     Assert the reducer REORGS: the authoritative tip switches to the heavier
 *     branch, the displaced (losing-branch) coinbase UTXOs are removed, the new
 *     branch's coinbases are present, and the UTXO commitment is BYTE-EXACT to a
 *     from-scratch recompute of the winning chain (the consensus stake — a wrong
 *     inverse silently corrupts the UTXO set with no crash).
 *
 * THE ONE-BLOCK-LOOKAHEAD CONVENTION (carried from the it-works gate)
 * ------------------------------------------------------------------
 * tip_finalize finalizes height H by reading active_chain_at(H+1) and stamping
 * that successor as the tip; it records the row for height H in the log row at
 * key H-1. So to finalize up to height N you must have block N+1 data'd. PART 1
 * mines N+1 blocks (1..33) and ingests blocks 1..32; block 33 is the lookahead
 * successor that lets block 32 finalize.
 *
 * THE FINALITY FLOOR (why PART 2 forks NEAR the tip, not at height 16)
 * -------------------------------------------------------------------
 * utxo_apply's reorg unwind is gated by reorg_is_allowed(tip, fork): a reorg
 * deeper than ZCL_FINALITY_DEPTH (=10) is CORRECTLY REFUSED (a consensus safety
 * feature, not a wedge). With the tip at 32, a fork at height 16 (depth 16)
 * would be refused. PART 2 therefore forks within the finality window so the
 * unwind actually proceeds — exactly the reorg a live node performs.
 *
 * No stubs, no injected readers: the eight stages run on their PRODUCTION
 * defaults (bodies persisted to disk by the front door / helper and read back
 * by stage_default_block_reader; coinbase-only blocks have no transparent inputs
 * and no shielded proofs, so script_validate / proof_validate pass trivially).
 */

#include "test/test_helpers.h"

#include "bloom/merkle.h"
#include "chain/chainparams.h"
#include "chain/chainparamsbase.h"
#include "chain/pow.h"
#include "chain/subsidy.h"
#include "consensus/validation.h"
#include "core/arith_uint256.h"
#include "core/uint256.h"
#include "domain/consensus/coinbase.h"
#include "mining/miner.h"
#include "primitives/block.h"
#include "primitives/transaction.h"
#include "script/script.h"
#include "services/chain_activation_service.h"
#include "services/header_admit_inbox.h"
#include "storage/coins_kv.h"
#include "storage/disk_block_io.h"
#include "storage/event_log.h"
#include "storage/progress_store.h"
#include "storage/utxo_projection.h"
#include "jobs/header_admit_stage.h"
#include "jobs/validate_headers_stage.h"
#include "jobs/body_fetch_stage.h"
#include "jobs/body_persist_stage.h"
#include "jobs/script_validate_stage.h"
#include "jobs/proof_validate_stage.h"
#include "jobs/utxo_apply_stage.h"
#include "jobs/tip_finalize_stage.h"
#include "jobs/stage_helpers.h"
#include "util/blocker.h"
#include "util/safe_alloc.h"
#include "util/util.h"
#include "validation/accept_block_header.h"
#include "validation/chainstate.h"
#include "validation/check_block.h"
#include "validation/main_constants.h"
#include "validation/main_state.h"

#include <errno.h>
#include <inttypes.h>
#include <sqlite3.h>
#include <stdio.h>
#include <string.h>
#include <sys/stat.h>
#include <unistd.h>

#define RFP_CHECK(name, expr) do {                            \
    printf("reducer_forward_progress_gate: %s... ", (name));  \
    if ((expr)) printf("OK\n");                               \
    else { printf("FAIL\n"); failures++; }                    \
} while (0)

/* How many sequential blocks PART 1 drives the reducer forward. A wedge that
 * only surfaces after a handful of finalizations is the whole point — 32 is
 * deep enough to surface the live-wedge oscillation while staying fast. */
#define RFP_N 32

static int rfp_mkdir_p(const char *p)
{
    if (mkdir(p, 0700) == 0) return 0;
    if (errno == EEXIST) return 0;
    return -1;
}

/* ── Block builder — identical shape to the it-works gate's
 * rbi_build_regtest_block (one coinbase tx, real merkle root, regtest powLimit
 * compact nBits). `script_salt` perturbs the miner pubkey-hash so a competing
 * branch's coinbase gets a DISTINCT txid at the same height (salt 0 = the
 * canonical chain, salt != 0 = a fork). Returns false on any build failure. */
static bool rfp_build_regtest_block(struct block *blk, int height,
                                    const struct uint256 *prev_hash,
                                    const struct chain_params *cp,
                                    uint8_t script_salt,
                                    int64_t *out_coinbase_value)
{
    block_init(blk);
    blk->vtx = zcl_calloc(1, sizeof(struct transaction), "rfp_vtx");
    if (!blk->vtx)
        return false;
    blk->num_vtx = 1;

    struct transaction *coinbase = &blk->vtx[0];
    transaction_init(coinbase);
    if (!transaction_alloc(coinbase, 1, 1))
        return false;

    struct script miner_script;
    script_init(&miner_script);
    miner_script.data[0] = 0x76; /* OP_DUP        */
    miner_script.data[1] = 0xa9; /* OP_HASH160    */
    miner_script.data[2] = 0x14; /* push 20       */
    for (int i = 0; i < 20; i++)
        miner_script.data[3 + i] = (unsigned char)(0x10 + i + script_salt);
    miner_script.data[23] = 0x88; /* OP_EQUALVERIFY */
    miner_script.data[24] = 0xac; /* OP_CHECKSIG    */
    miner_script.size = 25;

    int64_t subsidy = get_block_subsidy(height, &cp->consensus);
    struct domain_consensus_coinbase_inputs cb_in = {
        .n_height     = height,
        .subsidy      = subsidy,
        .total_fees   = 0,
        .miner_script = &miner_script,
        .params       = &cp->consensus,
    };
    struct zcl_result r = domain_consensus_coinbase_build(&cb_in, coinbase);
    if (!r.ok)
        return false;

    if (out_coinbase_value)
        *out_coinbase_value = subsidy;

    struct uint256 txid = blk->vtx[0].hash;
    blk->header.hashMerkleRoot = compute_merkle_root(&txid, 1);

    blk->header.hashPrevBlock = *prev_hash;
    uint256_set_null(&blk->header.hashFinalSaplingRoot);
    blk->header.nTime = 1600000000u + (uint32_t)height;

    struct arith_uint256 pow_limit;
    uint256_to_arith(&pow_limit, &cp->consensus.powLimit);
    blk->header.nBits = arith_uint256_get_compact(&pow_limit, false);

    return true;
}

/* Seed a genesis (height 0) utxo_apply_log row marking it already-applied with
 * a zero coin delta, so the cold-start anchor finalizes cleanly into block 1
 * (identical to the it-works gate's rbi_seed_genesis_utxo_apply_row). */
static bool rfp_seed_genesis_utxo_apply_row(sqlite3 *db)
{
    if (!db) return false;
    sqlite3_stmt *st = NULL;
    if (sqlite3_prepare_v2(db,
        "INSERT OR REPLACE INTO utxo_apply_log "
        "(height,status,ok,spent_count,added_count,total_value_delta,applied_at) "
        "VALUES(0,'verified',1,0,0,0,1)", -1, &st, NULL) != SQLITE_OK)
        return false;
    bool ok = (sqlite3_step(st) == SQLITE_DONE);  // raw-sql-ok:test-seed
    sqlite3_finalize(st);
    return ok;
}

struct rfp_ingest_ctx {
    const struct chain_params *cp;
    struct main_state *ms;
    struct chain_activation_controller *ctl;
    const char *datadir;
};

/* Mine a regtest block at `height` on `prev_hash`, admit its header (creating
 * the block_index + linking pprev), write its body to disk, and mark the index
 * BLOCK_HAVE_DATA — the "headers + bodies synced but not connected" state.
 * On success fills the out-params (caller block_free's *out_blk). Mirrors the
 * it-works gate's rbi_mine_admit_persist. The window/best-header anchor is
 * forward-extended to this block so the reducer can finalize down to it. */
static bool rfp_mine_admit_persist(struct rfp_ingest_ctx *c, int height,
                                   const struct uint256 *prev_hash,
                                   struct block *out_blk,
                                   struct uint256 *out_hash,
                                   int64_t *out_cb_value,
                                   struct block_index **out_bi)
{
    if (!rfp_build_regtest_block(out_blk, height, prev_hash, c->cp,
                                 0 /*canonical chain salt*/, out_cb_value))
        return false;
    if (!mine_block_pow(out_blk, height, c->cp, 0))
        return false;

    block_get_hash(out_blk, out_hash);

    struct header_admit_msg hmsg;
    memset(&hmsg, 0, sizeof(hmsg));
    hmsg.height = height;
    hmsg.hash = *out_hash;
    hmsg.has_header = true;
    hmsg.header = out_blk->header;
    if (!mailbox_header_admit_push(&hmsg))
        return false;
    (void)header_admit_stage_drain(64);

    struct block_index *bi = block_map_find(&c->ms->map_block_index, out_hash);
    if (!bi)
        return false;

    struct disk_block_pos pos;
    disk_block_pos_init(&pos);
    if (!write_block_to_disk(out_blk, &pos, c->datadir,
                             c->cp->pchMessageStart))
        return false;
    if (!block_index_set_have_data_verified(bi, &pos, c->datadir))
        return false;

    c->ms->pindex_best_header = bi;
    (void)active_chain_extend_window(&c->ms->chain_active, bi);

    if (out_bi) *out_bi = bi;
    return true;
}

/* Read the eight stage cursors into a fixed-order array so a stall snapshot can
 * pinpoint WHICH stage stopped advancing. Index order matches pipeline order. */
static void rfp_snapshot_cursors(sqlite3 *db, uint64_t out[8])
{
    static const char *names[8] = {
        "header_admit", "validate_headers", "body_fetch", "body_persist",
        "script_validate", "proof_validate", "utxo_apply", "tip_finalize"
    };
    for (int i = 0; i < 8; i++)
        out[i] = stage_cursor_persisted(db, names[i], names[i]);
}

static void rfp_print_cursors(const char *tag, const uint64_t cur[8])
{
    printf("reducer_forward_progress_gate: [%s cursors] header_admit=%llu "
           "validate_headers=%llu body_fetch=%llu body_persist=%llu "
           "script_validate=%llu proof_validate=%llu utxo_apply=%llu "
           "tip_finalize=%llu\n", tag,
           (unsigned long long)cur[0], (unsigned long long)cur[1],
           (unsigned long long)cur[2], (unsigned long long)cur[3],
           (unsigned long long)cur[4], (unsigned long long)cur[5],
           (unsigned long long)cur[6], (unsigned long long)cur[7]);
}

int test_reducer_forward_progress_gate(void);
int test_reducer_forward_progress_gate(void)
{
    printf("\n=== reducer forward-progress + reorg gate "
           "(live-wedge repro: %d sequential blocks -> reducer -> tip+N; "
           "then a heavier-fork reorg) ===\n", RFP_N);
    int failures = 0;

    /* OPT-IN gate (same rationale as the it-works gate): drives reducer
     * process-globals, deterministic only in a FRESH process. Run isolated via
     * `make mvp-forward-progress` (a dedicated --only fresh process). */
    if (!getenv("ZCL_STRESS_TESTS")) {
        printf("reducer_forward_progress_gate: SKIP "
               "(set ZCL_STRESS_TESTS=1 and run isolated via "
               "`make mvp-forward-progress`)\n");
        return 0;
    }

    blocker_module_init();
    chain_params_select(CHAIN_REGTEST);
    const struct chain_params *cp = chain_params_get();

    {
        unsigned int n = chain_params_equihash_n(cp, 1);
        unsigned int k = chain_params_equihash_k(cp, 1);
        RFP_CHECK("regtest is Equihash (48,5)", n == 48 && k == 5);
    }

    /* ── (1) Fresh node-db / progress.kv at genesis, hermetic datadir ────── */
    char dir[256];
    rfp_mkdir_p("./test-tmp");
    test_fmt_tmpdir(dir, sizeof(dir), "reducer_forward_progress_gate", "main");
    rfp_mkdir_p(dir);

    SetDataDir(dir);
    ClearDataDirCache();
    char netdir[512];
    GetDataDir(true /*net-specific (regtest subdir)*/, netdir, sizeof(netdir));
    rfp_mkdir_p(netdir);
    char blocksdir[640];
    snprintf(blocksdir, sizeof(blocksdir), "%s/blocks", netdir);
    rfp_mkdir_p(blocksdir);

    char log_path[512], proj_path[512];
    snprintf(log_path, sizeof(log_path), "%s/events.log", dir);
    snprintf(proj_path, sizeof(proj_path), "%s/utxo.db", dir);

    progress_store_close();
    bool store_ok = progress_store_open(dir);
    RFP_CHECK("progress_store opens at fresh datadir", store_ok);

    event_log_t *lg = store_ok ? event_log_open(log_path) : NULL;
    RFP_CHECK("event log opens", lg != NULL);

    utxo_projection_t *proj = lg ? utxo_projection_open(proj_path, lg) : NULL;
    RFP_CHECK("UTXO projection opens", proj != NULL);

    if (!store_ok || !lg || !proj) {
        if (proj) utxo_projection_close(proj);
        if (lg) event_log_close(lg);
        progress_store_close();
        test_cleanup_tmpdir(blocksdir);
        test_cleanup_tmpdir(netdir);
        test_cleanup_tmpdir(dir);
        SetDataDir("");
        ClearDataDirCache();
        chain_params_select(CHAIN_MAIN);
        printf("=== reducer forward-progress gate: %d failures (setup) ===\n",
               failures);
        return failures;
    }

    utxo_projection_set_event_log(lg);
    utxo_projection_test_set_author(UTXO_AUTHOR_STAGE);

    struct main_state ms;
    main_state_init(&ms);

    /* Seed genesis exactly the way boot.c does. */
    struct uint256 genesis_hash = cp->consensus.hashGenesisBlock;
    struct block_index *genesis = chainstate_insert_block_index(
        (struct chainstate *)&ms, &genesis_hash);
    RFP_CHECK("genesis block_index inserted", genesis != NULL);
    if (genesis) {
        genesis->nHeight = 0;
        genesis->nStatus = BLOCK_HAVE_DATA | BLOCK_VALID_SCRIPTS;
        genesis->nTx = 1;
        genesis->nChainTx = 1;
        genesis->nChainWork = GetBlockProof(genesis);
        active_chain_move_window_tip(&ms.chain_active, genesis);
        ms.pindex_best_header = genesis;
    }

    RFP_CHECK("tip_finalize anchor seed at genesis",
              tip_finalize_stage_seed_anchor(0, genesis_hash.data, false));

    bool stages_ok =
        header_admit_stage_init(&ms) &&
        validate_headers_stage_init(&ms) &&
        body_fetch_stage_init(&ms) &&
        body_persist_stage_init(&ms) &&
        script_validate_stage_init(&ms) &&
        proof_validate_stage_init(&ms) &&
        utxo_apply_stage_init(&ms) &&
        tip_finalize_stage_init(&ms);
    RFP_CHECK("all eight reducer stages init", stages_ok);

    if (stages_ok)
        RFP_CHECK("seed genesis utxo_apply_log row",
                  rfp_seed_genesis_utxo_apply_row(progress_store_db()));

    struct chain_activation_controller ctl;
    activation_controller_init(&ctl, &ms, NULL, cp, netdir);

    int height_genesis = active_chain_height(&ms.chain_active);
    RFP_CHECK("genesis tip height is 0", height_genesis == 0);

    /* coins_kv is the authoritative UTXO set the stages author (the projection
     * dual-write was removed); read the genesis baseline from it. */
    uint64_t count_genesis = (uint64_t)coins_kv_count(progress_store_db());

    struct rfp_ingest_ctx ictx = { .cp = cp, .ms = &ms, .ctl = &ctl,
                                   .datadir = netdir };

    /* ── (2) PART 1 — mine N+1 blocks (1..N+1) in the "headers+bodies synced"
     * state. Block N+1 is the one-block-lookahead successor that lets block N
     * finalize. Keep all blocks + their hashes/block_index pointers alive (the
     * reorg in PART 2 reuses the fork-point hash). ──────────────────────── */
    struct block      *blocks = zcl_calloc(RFP_N + 2, sizeof(struct block),
                                           "rfp_blocks");
    struct uint256    *hashes = zcl_calloc(RFP_N + 2, sizeof(struct uint256),
                                           "rfp_hashes");
    struct uint256    *cbids  = zcl_calloc(RFP_N + 2, sizeof(struct uint256),
                                           "rfp_cbids");
    int64_t           *cbvals = zcl_calloc(RFP_N + 2, sizeof(int64_t),
                                           "rfp_cbvals");
    struct block_index **bidx = zcl_calloc(RFP_N + 2,
                                           sizeof(struct block_index *),
                                           "rfp_bidx");
    bool allocs_ok = blocks && hashes && cbids && cbvals && bidx;
    RFP_CHECK("PART1 chain arrays allocated", allocs_ok);
    if (blocks) for (int i = 0; i <= RFP_N + 1; i++) block_init(&blocks[i]);

    /* The live-wedge witnesses we capture if PART 1 stalls. */
    bool wedge_reproduced = false;
    int  wedge_height = -1;
    char wedge_detail[256] = {0};
    uint64_t wedge_cursors[8] = {0};

    bool part1_ready = stages_ok && allocs_ok;

    if (part1_ready) {
        /* hashes[0] == genesis. Mine heights 1..N+1 on top. */
        hashes[0] = genesis_hash;
        bidx[0] = genesis;
        bool mined_all = true;
        for (int h = 1; h <= RFP_N + 1 && mined_all; h++) {
            mined_all = rfp_mine_admit_persist(&ictx, h, &hashes[h - 1],
                                               &blocks[h], &hashes[h],
                                               &cbvals[h], &bidx[h]);
            if (mined_all) cbids[h] = blocks[h].vtx[0].hash;
        }
        RFP_CHECK("all N+1 blocks mined + header-admitted + body-on-disk",
                  mined_all);
        part1_ready = mined_all;
    }

    /* ── (3) PART 1 drive: ingest blocks 1..N sequentially through the FRONT
     * DOOR, looping reducer_kick to convergence after each, asserting the
     * authoritative tip and finalized_total advance MONOTONICALLY and never
     * retreat. A stall/oscillation here is the LIVE WEDGE reproduced. ─────── */
    int      reached_height = height_genesis;
    uint64_t prev_finalized = tip_finalize_stage_finalized_total();
    if (part1_ready) {
        for (int h = 1; h <= RFP_N; h++) {
            /* Lookahead successor (block h+1) must be the visible window tip so
             * tip_finalize can finalize block h. */
            ms.pindex_best_header = bidx[h + 1];
            (void)active_chain_extend_window(&ms.chain_active, bidx[h + 1]);

            struct validation_state out;
            validation_state_init(&out);
            bool fd = reducer_ingest_block(&ctl, &blocks[h], REDUCER_SRC_MINED,
                                           true /*force*/, &out);
            (void)fd;
            /* Loop to TRUE convergence — never assert after one bounded kick. */
            for (int it = 0; it < 1000 && reducer_kick(&ctl) > 0; it++) {
                /* keep draining */
            }

            int      hh = active_chain_height(&ms.chain_active);
            uint64_t fin = tip_finalize_stage_finalized_total();

            /* Forward-progress invariant: the tip is exactly h, finalized_total
             * strictly increased, and neither retreated (no oscillation). */
            bool advanced  = (hh == h);
            bool monotone  = (hh >= reached_height) && (fin >= prev_finalized);
            bool fin_up    = (fin > prev_finalized);
            if (!advanced || !monotone || !fin_up) {
                wedge_reproduced = true;
                wedge_height = h;
                rfp_snapshot_cursors(progress_store_db(), wedge_cursors);
                snprintf(wedge_detail, sizeof(wedge_detail),
                    "stalled at block %d: tip=%d (want %d) finalized_total "
                    "%llu->%llu reorg_detected=%llu precond_failed_via_logs "
                    "reject=\"%s\"",
                    h, hh, h,
                    (unsigned long long)prev_finalized,
                    (unsigned long long)fin,
                    (unsigned long long)tip_finalize_stage_reorg_detected_total(),
                    out.reject_reason[0] ? out.reject_reason : "(none)");
                printf("reducer_forward_progress_gate: *** LIVE WEDGE "
                       "REPRODUCED *** %s\n", wedge_detail);
                rfp_print_cursors("wedge", wedge_cursors);
                break;
            }
            reached_height = hh;
            prev_finalized = fin;
        }

        printf("reducer_forward_progress_gate: PART1 reached tip height %d/%d "
               "(finalized_total=%llu)\n", reached_height, RFP_N,
               (unsigned long long)tip_finalize_stage_finalized_total());

        RFP_CHECK("PART1: tip advanced monotonically to N with no stall/"
                  "oscillation (live-wedge NOT reproduced)",
                  reached_height == RFP_N && !wedge_reproduced);
        RFP_CHECK("PART1: tip_finalize finalized >= N blocks (reducer engine)",
                  tip_finalize_stage_finalized_total() >= (uint64_t)RFP_N);
        RFP_CHECK("PART1: utxo_apply succeeded at tip height N",
                  utxo_apply_stage_succeeded_at(RFP_N));
        RFP_CHECK("PART1: no spend-unknown across the run",
                  utxo_apply_stage_spend_unknown_total() == 0);

        /* Each block added exactly one coinbase UTXO over the genesis baseline.
         * With tip at N finalized and block N+1 also data'd as the pending
         * successor, utxo_apply has applied N+1 coinbases. */
        sqlite3 *pdb = progress_store_db();
        uint64_t count_after = (uint64_t)coins_kv_count(pdb);
        printf("reducer_forward_progress_gate: utxo count %llu -> %llu\n",
               (unsigned long long)count_genesis,
               (unsigned long long)count_after);
        bool applied_succ = utxo_apply_stage_succeeded_at(RFP_N + 1);
        RFP_CHECK("PART1: N coinbases live over genesis (+1 pending successor)",
                  count_after == count_genesis + (uint64_t)RFP_N +
                                 (applied_succ ? 1u : 0u));

        /* Each finalized block's coinbase output is live with the right value. */
        bool every_cb_live = !wedge_reproduced;
        for (int h = 1; h <= RFP_N && every_cb_live; h++) {
            int64_t v = 0;
            bool live = coins_kv_get(pdb, cbids[h].data, 0, &v, NULL,
                                     0, NULL);
            if (!live || v != cbvals[h] || v <= 0)
                every_cb_live = false;
        }
        RFP_CHECK("PART1: every finalized block's coinbase is live at subsidy",
                  every_cb_live);
    }

    /* ── (4) PART 2 — REORG ─────────────────────────────────────────────────
     * Fork NEAR the tip (within ZCL_FINALITY_DEPTH so the unwind is allowed),
     * mine a HEAVIER competing branch (one block longer above the fork), install
     * it on the active chain, and drive the reducer to convergence. Assert the
     * reducer reorgs and the resulting UTXO set is byte-exact to a from-scratch
     * build of the winning chain. Skipped if PART 1 wedged (no clean base). */
    bool part2_ran = false;
    bool reorg_ok = false;
    if (part1_ready && !wedge_reproduced) {
        /* Fork at F = N - 4 (depth N-F = 4 <= ZCL_FINALITY_DEPTH=10). Branch L
         * (the losing branch) holds heights F+1..N (4 blocks above the fork);
         * Branch W (winner) is HEAVIER: F+1..N+1 (5 blocks above the fork). */
        const int F = RFP_N - 4;
        RFP_CHECK("PART2: fork depth within finality floor",
                  (RFP_N - F) <= ZCL_FINALITY_DEPTH);

        /* Mine W: heights F+1 .. N+1 on top of the SHARED fork-point hash
         * hashes[F]. W's tip is at height N+1 — strictly longer (heavier) than
         * L's tip at N, so it must win. */
        const int W_LEN = RFP_N + 1 - F;   /* number of W blocks above fork */
        struct block      *wblk  = zcl_calloc((size_t)W_LEN, sizeof(struct block),
                                              "rfp_wblk");
        struct uint256    *whash = zcl_calloc((size_t)W_LEN, sizeof(struct uint256),
                                              "rfp_whash");
        struct uint256    *wcbid = zcl_calloc((size_t)W_LEN, sizeof(struct uint256),
                                              "rfp_wcbid");
        int64_t           *wcbv  = zcl_calloc((size_t)W_LEN, sizeof(int64_t),
                                              "rfp_wcbv");
        struct block_index **wbi = zcl_calloc((size_t)W_LEN,
                                              sizeof(struct block_index *),
                                              "rfp_wbi");
        bool w_alloc = wblk && whash && wcbid && wcbv && wbi;
        RFP_CHECK("PART2: W branch arrays allocated", w_alloc);
        if (wblk) for (int i = 0; i < W_LEN; i++) block_init(&wblk[i]);

        bool w_mined = w_alloc;
        for (int i = 0; i < W_LEN && w_mined; i++) {
            int h = F + 1 + i;
            const struct uint256 *prev = (i == 0) ? &hashes[F] : &whash[i - 1];
            /* Make W's coinbase genuinely DISTINCT from L's at the same height:
             * salt=1 perturbs the miner pubkey-hash so W's coinbase TXID differs
             * from L's, and a bumped nTime diverges the block header hash too.
             * (Without a distinct coinbase txid, L and W coinbases would be the
             * identical UTXO at each height and the "displaced L removed" check
             * would be vacuous.) */
            if (!rfp_build_regtest_block(&wblk[i], h, prev, cp,
                                         1 /*fork salt*/, &wcbv[i])) {
                w_mined = false; break;
            }
            wblk[i].header.nTime += 7777u;  /* diverge from L at this height */
            if (!mine_block_pow(&wblk[i], h, cp, 0)) { w_mined = false; break; }
            block_get_hash(&wblk[i], &whash[i]);
            wcbid[i] = wblk[i].vtx[0].hash;

            /* The header_admit producer is FORWARD-only: its cursor is already
             * past the fork heights (which hold L's blocks), so it would not
             * create W's index. Create the W block_index via the SAME canonical
             * path the reducer producer uses (add_to_block_index) so every field
             * — nBits, nVersion, and the CUMULATIVE nChainWork (pprev + this
             * block's proof) — is populated correctly. W's header points at the
             * shared fork-point parent (hashes[F]) then the prior W block, so
             * add_to_block_index links pprev and accumulates work; with W one
             * block longer than L above the fork, W's TIP outweighs L's tip and
             * the reducer's chainwork-greater gate passes. */
            struct block_index *bi = add_to_block_index(&ms, &wblk[i].header);
            if (!bi) { w_mined = false; break; }
            bi->nStatus |= BLOCK_HAVE_DATA | BLOCK_VALID_SCRIPTS;
            bi->nTx = 1;
            bi->nChainTx = (bi->pprev ? bi->pprev->nChainTx : 0) + 1;

            struct disk_block_pos pos;
            disk_block_pos_init(&pos);
            if (!write_block_to_disk(&wblk[i], &pos, netdir,
                                     cp->pchMessageStart)) { w_mined = false; break; }
            if (!block_index_set_have_data_verified(bi, &pos, netdir)) {
                w_mined = false; break;
            }
            wbi[i] = bi;
        }
        RFP_CHECK("PART2: heavier W branch mined + admitted + body-on-disk",
                  w_mined);

        if (w_mined) {
            part2_ran = true;
            /* Pre-reorg witnesses. L's coinbases at heights F+1..N must be live
             * now; after the reorg they must be GONE and W's must be present. */
            struct block_index *w_tip = wbi[W_LEN - 1]; /* height N+1 */

            /* Install W on the active chain (the live driver's most-work tip
             * swap) and point best-header at W's tip so the stages extend
             * toward W. active_chain_move_window_tip reassembles chain[] along
             * W's pprev path — the structural reorg the reducer then OBSERVES
             * via branch_hash / pprev divergence. */
            ms.pindex_best_header = w_tip;
            bool installed = active_chain_move_window_tip(&ms.chain_active,
                                                          w_tip);
            RFP_CHECK("PART2: heavier W branch installed on active chain",
                      installed && active_chain_at(&ms.chain_active,
                                                   RFP_N + 1) == w_tip);

            uint64_t reorg_before = utxo_apply_stage_reorg_unwound_total();

            /* Drive the reducer to convergence: tip_finalize detects the pprev
             * divergence + rewinds its cursor, utxo_apply detects the
             * branch_hash divergence + emits the inverse deltas and re-advances
             * over W, then tip_finalize re-finalizes W. */
            for (int it = 0; it < 4000 && reducer_kick(&ctl) > 0; it++) {
                /* keep draining */
            }

            uint64_t reorg_after = utxo_apply_stage_reorg_unwound_total();
            int reorg_tip = active_chain_height(&ms.chain_active);
            printf("reducer_forward_progress_gate: PART2 post-reorg tip=%d "
                   "(want %d) reorg_unwound %llu->%llu reorg_detected=%llu\n",
                   reorg_tip, RFP_N + 1,
                   (unsigned long long)reorg_before,
                   (unsigned long long)reorg_after,
                   (unsigned long long)tip_finalize_stage_reorg_detected_total());

            RFP_CHECK("PART2: utxo_apply performed a reorg unwind",
                      reorg_after > reorg_before);
            /* One-block-lookahead: with W's tip at N+1, the finalized authority
             * height lands at N (block N+1 is the pending lookahead successor),
             * and the finalized block at height N is a W-branch block (not L). */
            struct block_index *fin_at_n =
                active_chain_at(&ms.chain_active, RFP_N);
            bool tip_is_w = fin_at_n && fin_at_n->phashBlock &&
                            uint256_eq(fin_at_n->phashBlock,
                                       &whash[W_LEN - 2]); /* W height N */
            RFP_CHECK("PART2: finalized tip switched to the heavier W branch "
                      "(authority at N is a W block; N+1 is its pending "
                      "lookahead)",
                      reorg_tip == RFP_N && tip_is_w);

            /* coins_kv is the authoritative store after the reorg unwind. */
            sqlite3 *pdb = progress_store_db();

            /* Displaced L coinbases (heights F+1..N+1, all unwound + replaced by
             * W) are GONE; W coinbases (F+1..N+1) are present. */
            int l_absent = 0, l_total = 0;
            for (int h = F + 1; h <= RFP_N + 1; h++) {
                l_total++;
                if (!coins_kv_exists(pdb, cbids[h].data, 0))
                    l_absent++;
            }
            RFP_CHECK("PART2: all displaced L-branch coinbases removed",
                      l_total > 0 && l_absent == l_total);

            int w_present = 0;
            for (int i = 0; i < W_LEN; i++) {
                if (coins_kv_exists(pdb, wcbid[i].data, 0))
                    w_present++;
            }
            RFP_CHECK("PART2: all W-branch coinbases present",
                      w_present == W_LEN);

            /* coins_kv_commitment uses the SAME SHA3 encoder as
             * utxo_projection_commitment (coins_kv.h), so the reorged coins_kv
             * commitment is byte-comparable to the from-scratch projection
             * reference (proj2) built below. */
            uint8_t commit_reorg[32];
            bool have_reorg_commit =
                (coins_kv_commitment(pdb, commit_reorg) == 0);
            uint64_t count_reorg = (uint64_t)coins_kv_count(pdb);
            RFP_CHECK("PART2: reorg-path commitment computed", have_reorg_commit);

            /* ── From-scratch recompute of the WINNING chain ───────────────
             * Build a SEPARATE projection that directly applies the canonical
             * winning chain: blocks 1..F (shared) then W's F+1..N+1. Compare the
             * SHA3 commitment byte-for-byte. A mismatch means the reorg inverse
             * silently corrupted the UTXO set. We emit the same coinbase ADDs
             * the stage would, in canonical order, over the same genesis seed. */
            char dir2[300], proj2_path[600], log2_path[600];
            snprintf(dir2, sizeof(dir2), "%s-direct", dir);
            rfp_mkdir_p(dir2);
            snprintf(proj2_path, sizeof(proj2_path), "%s/utxo.db", dir2);
            snprintf(log2_path, sizeof(log2_path), "%s/events.log", dir2);
            event_log_t *lg2 = event_log_open(log2_path);
            utxo_projection_t *proj2 = lg2 ? utxo_projection_open(proj2_path,
                                                                  lg2) : NULL;
            bool direct_ok = (lg2 && proj2);
            RFP_CHECK("PART2: from-scratch projection opens", direct_ok);
            if (direct_ok) {
                /* emit_add writes to the GLOBAL event log; point it at lg2 so
                 * proj2 (opened over lg2) folds the canonical coinbases. Restore
                 * the live log (lg) afterward. */
                utxo_projection_set_event_log(lg2);
                /* Genesis baseline carries no coinbase in this harness's model
                 * (the genesis utxo_apply row is a zero delta), so the direct
                 * build starts empty and adds exactly the canonical coinbases.
                 * CRITICAL: the commitment hashes the coinbase SCRIPT too, so we
                 * must emit each coinbase's real script_pub_key (the same bytes
                 * utxo_apply stored from the block body) — not an empty script. */
                /* Shared blocks 1..F (L == W below the fork). */
                for (int h = 1; h <= F; h++) {
                    const struct script *spk =
                        &blocks[h].vtx[0].vout[0].script_pub_key;
                    utxo_projection_emit_add(cbids[h].data, 0,
                                             cbvals[h], (uint32_t)h, true,
                                             spk->size ? spk->data : NULL,
                                             (uint32_t)spk->size);
                }
                /* Winning branch W: heights F+1..N+1. */
                for (int i = 0; i < W_LEN; i++) {
                    int h = F + 1 + i;
                    const struct script *spk =
                        &wblk[i].vtx[0].vout[0].script_pub_key;
                    utxo_projection_emit_add(wcbid[i].data, 0,
                                             wcbv[i], (uint32_t)h, true,
                                             spk->size ? spk->data : NULL,
                                             (uint32_t)spk->size);
                }
                (void)utxo_projection_catch_up(proj2);
                uint8_t commit_direct[32];
                bool have_direct =
                    (utxo_projection_commitment(proj2, commit_direct) == 0);
                uint64_t count_direct = utxo_projection_count(proj2);
                printf("reducer_forward_progress_gate: PART2 reorg count=%llu "
                       "direct count=%llu commitment_match=%d\n",
                       (unsigned long long)count_reorg,
                       (unsigned long long)count_direct,
                       (have_reorg_commit && have_direct &&
                        memcmp(commit_reorg, commit_direct, 32) == 0) ? 1 : 0);
                RFP_CHECK("PART2: reorg UTXO count == from-scratch count",
                          count_reorg == count_direct);
                reorg_ok =
                    have_reorg_commit && have_direct &&
                    memcmp(commit_reorg, commit_direct, 32) == 0 &&
                    count_reorg == count_direct;
                RFP_CHECK("PART2: reorg UTXO commitment == from-scratch "
                          "(byte-exact SHA3 — reorg did not corrupt the set)",
                          reorg_ok);
            }
            if (proj2) utxo_projection_close(proj2);
            if (lg2) event_log_close(lg2);
            test_cleanup_tmpdir(dir2);

            /* Restore the live projection's event log: the from-scratch build
             * temporarily pointed the GLOBAL emit target at lg2; put it back to
             * the live log so any later emission stays on the real projection. */
            utxo_projection_set_event_log(lg);
        }

        if (wblk) for (int i = 0; i < W_LEN; i++) block_free(&wblk[i]);
        free(wblk); free(whash); free(wcbid); free(wcbv); free(wbi);
    }
    (void)part2_ran;
    (void)reorg_ok;

    /* ── (5) Teardown ───────────────────────────────────────────────────── */
    if (blocks) for (int i = 0; i <= RFP_N + 1; i++) block_free(&blocks[i]);
    free(blocks); free(hashes); free(cbids); free(cbvals); free(bidx);

    tip_finalize_stage_shutdown();
    utxo_apply_stage_shutdown();
    proof_validate_stage_shutdown();
    script_validate_stage_shutdown();
    body_persist_stage_shutdown();
    body_fetch_stage_shutdown();
    validate_headers_stage_shutdown();
    header_admit_stage_shutdown();

    activation_controller_destroy(&ctl);

    utxo_projection_test_set_author(UTXO_AUTHOR_STAGE);
    utxo_projection_set_event_log(NULL);

    main_state_free(&ms);
    utxo_projection_close(proj);
    event_log_close(lg);
    progress_store_close();
    test_cleanup_tmpdir(blocksdir);
    test_cleanup_tmpdir(netdir);
    test_cleanup_tmpdir(dir);

    SetDataDir("");
    ClearDataDirCache();
    chain_params_select(CHAIN_MAIN);

    if (wedge_reproduced) {
        printf("=== reducer forward-progress gate: LIVE WEDGE REPRODUCED at "
               "height %d — %s ===\n", wedge_height, wedge_detail);
    }
    printf("=== reducer forward-progress + reorg gate: %d failure(s) ===\n",
           failures);
    return failures;
}
