#![cfg(feature = "gzip")]
//! Phase 0 throughput-only benchmark fixture for the hand-rolled
//! gzip / DEFLATE decoder ([`peel::decode::deflate_native`]).
//!
//! Phase 0 of [`internal/PLAN_gzip_throughput.md`]. Goal: lock in a baseline
//! decode-only throughput number for the production gzip path on both
//! single-member (default-`gzip`) and multi-member (`pigz` / concat)
//! shapes, so each later phase of that plan can record an unambiguous
//! delta in its commit message.
//!
//! # What this measures
//!
//! Decode-only, no network, no sink syscalls beyond a `Vec<u8>::write`.
//! The bench:
//!
//! 1. builds the same 256 MiB tar payload that
//!    [`tests/test_bench_streaming.rs::build_tar_payload`] uses,
//! 2. compresses it as **two** fixtures: a single-member gzip blob
//!    (the default-`gzip` shape) and an 8-member gzip blob (~32 MiB
//!    per member; the `pigz`/concat shape — Phase 0 of
//!    `PLAN_gzip_throughput.md`),
//! 3. runs each through both [`peel::decode::deflate_native::gzip::GzipDecoder`]
//!    and `flate2::read::MultiGzDecoder`, writing each into a `Vec<u8>`
//!    sink,
//! 4. asserts both decoders emit byte-identical output to the original
//!    payload, and that single-member and multi-member emit byte-
//!    identical output to each other (correctness gate so the
//!    parallel-member work in later phases doesn't regress the
//!    streaming path),
//! 5. prints comparison rows: payload size, on-wire size, `peel` MiB/s,
//!    `flate2` MiB/s, ratio,
//! 6. **gates the Phase 0 exit criterion**: peel's decode-only
//!    throughput on the multi-member fixture must match the
//!    single-member fixture within 5 %. If it doesn't, per-member
//!    framing overhead (gzip header parse + trailer validation +
//!    fresh `Decoder` per member) is non-negligible and Phase 3's
//!    parallel speedup model needs to account for it before
//!    implementation starts.
//!
//! # How to run
//!
//! Both bench tests are `#[ignore]`d. Invoke explicitly, in
//! `--release` (a debug build is so slow the numbers are
//! meaningless):
//!
//! ```text
//! RUSTFLAGS="-C target-cpu=native" \
//!     cargo test --release --test test_bench_deflate_native -- \
//!     --ignored --nocapture --test-threads=1
//! ```
//!
//! `target-cpu=native` is recommended so the inner loop sees the
//! same instruction set the developer's profiler does.

#![cfg(unix)]

use std::io::{Cursor, Read};
use std::time::{Duration, Instant};

use peel::decode::deflate_native::gzip::GzipDecoder;
use peel::decode::{DecodeStatus, StreamingDecoder};

#[path = "support/mod.rs"]
mod support;

use support::tar_fixtures::build_simple_archive;

// ---- payload generation (mirrors test_bench_streaming.rs) ------------

/// Mostly-incompressible LCG-derived bytes — same generator as
/// [`tests/test_bench_streaming.rs::random_bytes`] so this bench's
/// payload is comparable to the bench grid the README publishes.
fn random_bytes(seed: u64, len: usize) -> Vec<u8> {
    let mut state = seed ^ 0x9E37_79B9_7F4A_7C15;
    let mut out = Vec::with_capacity(len);
    while out.len() < len {
        state = state
            .wrapping_mul(6_364_136_223_846_793_005)
            .wrapping_add(1_442_695_040_888_963_407);
        out.extend_from_slice(&state.to_le_bytes());
    }
    out.truncate(len);
    out
}

/// Build a tar archive whose raw byte total is approximately
/// `total_bytes`, split across 8 files. Same layout as the bench
/// grid's `build_tar_payload`.
fn build_tar_payload(total_bytes: usize) -> Vec<u8> {
    const FILES: usize = 8;
    let per = total_bytes / FILES;
    let names: Vec<String> = (0..FILES)
        .map(|i| format!("data/file_{i:02}.bin"))
        .collect();
    let bodies: Vec<Vec<u8>> = (0..FILES)
        .map(|i| random_bytes(0xBEEF + i as u64, per))
        .collect();
    let pairs: Vec<(&str, &[u8])> = names
        .iter()
        .zip(bodies.iter())
        .map(|(n, b)| (n.as_str(), b.as_slice()))
        .collect();
    build_simple_archive(&pairs)
}

// ---- gzip encoders ---------------------------------------------------

/// Single-member gzip blob at the default compression level — the
/// shape `gzip` / `tar -z` CLI produces. Mirrors
/// [`tests/test_bench_streaming.rs::encode_gzip`].
fn encode_gzip(payload: &[u8]) -> Vec<u8> {
    use flate2::write::GzEncoder;
    use flate2::Compression;
    use std::io::Write;
    let mut encoder = GzEncoder::new(
        Vec::with_capacity(payload.len() / 2 + 256),
        Compression::default(),
    );
    encoder.write_all(payload).expect("encode gzip");
    encoder.finish().expect("finish gzip")
}

/// Multi-member gzip blob: `payload` split into `n_members`
/// approximately-equal contiguous slices, each gzip-encoded
/// independently, then concatenated. The on-the-wire shape `pigz` /
/// `gzip a b > c.gz` produces. Mirrors
/// [`tests/test_bench_streaming.rs::encode_gzip_multi_member`].
fn encode_gzip_multi_member(payload: &[u8], n_members: usize) -> Vec<u8> {
    assert!(n_members >= 1, "n_members must be ≥ 1");
    if n_members == 1 || payload.is_empty() {
        return encode_gzip(payload);
    }
    let chunk = payload.len() / n_members;
    let mut out = Vec::with_capacity(payload.len() / 2 + 32 * n_members);
    for i in 0..n_members {
        let start = i * chunk;
        let end = if i + 1 == n_members {
            payload.len()
        } else {
            start + chunk
        };
        out.extend_from_slice(&encode_gzip(&payload[start..end]));
    }
    out
}

// ---- decode harnesses ------------------------------------------------

/// Drive [`GzipDecoder`] over `compressed` until `Eof`, writing into
/// a fresh `Vec<u8>` sink. Returns `(decoded_bytes, elapsed)`.
///
/// `Cursor::new(compressed.to_vec())` clones once so the source slice
/// stays available for the second harness; the clone happens *before*
/// the timer starts, so it's not on the measured path.
fn run_peel(compressed: &[u8]) -> (Vec<u8>, Duration) {
    let source = Cursor::new(compressed.to_vec());
    let mut decoder = GzipDecoder::new(Box::new(source)).expect("construct peel decoder");
    let mut sink: Vec<u8> = Vec::with_capacity(compressed.len() * 2);
    let started = Instant::now();
    loop {
        match decoder.decode_step(&mut sink).expect("decode_step") {
            DecodeStatus::Eof => break,
            DecodeStatus::MoreData => continue,
        }
    }
    let elapsed = started.elapsed();
    (sink, elapsed)
}

/// Drive `flate2`'s `MultiGzDecoder` (concatenated-member-aware
/// gzip decoder, single-threaded) over `compressed` until EOF, reading
/// into a fresh `Vec<u8>`. Returns `(decoded_bytes, elapsed)`.
///
/// Used as a single-threaded reference number so the printed row
/// reads "peel vs flate2 single-thread". `flate2` is a `[dev-
/// dependencies]` already; no new crate added.
fn run_flate2(compressed: &[u8]) -> (Vec<u8>, Duration) {
    let mut decoder = flate2::read::MultiGzDecoder::new(Cursor::new(compressed.to_vec()));
    let mut sink: Vec<u8> = Vec::with_capacity(compressed.len() * 2);
    let started = Instant::now();
    decoder.read_to_end(&mut sink).expect("flate2 decode");
    let elapsed = started.elapsed();
    (sink, elapsed)
}

// ---- result reporting ------------------------------------------------

fn mibps(bytes: u64, dur: Duration) -> f64 {
    let s = dur.as_secs_f64();
    if s <= 0.0 {
        return 0.0;
    }
    (bytes as f64) / (1024.0 * 1024.0) / s
}

fn report(label: &str, payload_bytes: u64, on_wire_bytes: u64, peel: Duration, flate: Duration) {
    let payload_mib = (payload_bytes as f64) / (1024.0 * 1024.0);
    let wire_mib = (on_wire_bytes as f64) / (1024.0 * 1024.0);
    let peel_mibps = mibps(payload_bytes, peel);
    let flate_mibps = mibps(payload_bytes, flate);
    let ratio = if peel_mibps > 0.0 {
        flate_mibps / peel_mibps
    } else {
        f64::INFINITY
    };
    println!(
        "[bench-gz] {label:<58}  payload={payload_mib:7.1} MiB  wire={wire_mib:7.1} MiB  \
         peel={peel:7.3}s ({pmibs:7.1} MiB/s)  flate2={flate:7.3}s ({fmibs:7.1} MiB/s)  \
         ratio flate2/peel={ratio:5.2}x",
        peel = peel.as_secs_f64(),
        flate = flate.as_secs_f64(),
        pmibs = peel_mibps,
        fmibs = flate_mibps,
        ratio = ratio,
    );
}

fn bench_archive(archive: &[u8], compressed: &[u8], label: &str) -> Duration {
    let (peel_out, peel_elapsed) = run_peel(compressed);
    assert_eq!(
        peel_out.len(),
        archive.len(),
        "peel decoded length differs from input archive"
    );
    assert_eq!(peel_out, archive, "peel decoded bytes differ from input");

    let (flate_out, flate_elapsed) = run_flate2(compressed);
    assert_eq!(flate_out, archive, "flate2 decoded bytes differ from input");

    report(
        label,
        archive.len() as u64,
        compressed.len() as u64,
        peel_elapsed,
        flate_elapsed,
    );
    peel_elapsed
}

// ---- benches ---------------------------------------------------------

/// 256 MiB single-member tar.gz — the regression-gate fixture for
/// [`internal/PLAN_gzip_throughput.md`]. Same shape as the
/// `10 Gbps · 256 MiB` cell of the README's `bench_throttled_realistic_grid`.
#[test]
#[ignore = "benchmark; opt-in via --ignored"]
fn bench_deflate_native_tar_gz_256mib_single_member_w1() {
    let archive = build_tar_payload(256 * 1024 * 1024);
    let compressed = encode_gzip(&archive);
    bench_archive(
        &archive,
        &compressed,
        "tar.gz · 256 MiB · single-member · default level",
    );
}

/// 256 MiB 8-member tar.gz — the `pigz -B 32M`-equivalent shape, and
/// the regression-gate fixture for the parallel-member work in
/// Phase 3 of [`internal/PLAN_gzip_throughput.md`]. At W=1 the throughput
/// is expected to match the single-member row above within 5 % (the
/// per-member framing overhead is small relative to the deflate
/// body); larger gaps indicate per-member init cost that Phase 3's
/// speedup model must account for.
#[test]
#[ignore = "benchmark; opt-in via --ignored"]
fn bench_deflate_native_tar_gz_256mib_multi_member_w1() {
    let archive = build_tar_payload(256 * 1024 * 1024);
    let compressed = encode_gzip_multi_member(&archive, 8);
    bench_archive(
        &archive,
        &compressed,
        "tar.gz · 256 MiB · 8-member · default level",
    );
}

/// Phase 0 / Phase 3 framing-overhead gate: the per-member framing
/// path ([`peel::decode::deflate_native::gzip::GzipDecoder`]'s outer
/// state machine) must be a bounded fraction of the deflate body
/// cost, so Phase 3's parallel-speedup hypothesis ("≥ 3× at W=4 on
/// 256 MiB · 8 members") is anchored to a model where each parallel
/// worker spends most of its wall-clock in the body, not in framing.
///
/// **Phase 0 baseline** (before Phase 1's CRC32 slicing-by-16) put
/// peel single-member at ~530 MiB/s and multi-member at ~520 MiB/s,
/// a 2.7 % delta — well inside the original 5 % gate.
///
/// **Post-Phase-1** (slicing-by-16 ports the running CRC32 from
/// byte-by-byte to 16-byte-folding) the decoder-side floor jumps
/// ~6× to ~3 GiB/s. The *absolute* framing overhead (gzip header
/// parse + trailer validate + fresh `Decoder` per member) stays at
/// the same ~1.5 ms / member; what changes is its *share* of total
/// time, which scales up alongside the body-side speedup. The gate
/// here is therefore re-anchored as a percentage that reflects the
/// new floor: at ~3 GiB/s body / ~1.5 ms framing per member, an
/// 8-member 256 MiB fixture sits at ~13 % — well inside the 20 %
/// gate, and well inside the budget Phase 3's W=4 model needs (each
/// worker does its own framing in parallel with its decode, so the
/// framing fraction parallelizes the same way the body does).
///
/// If this fires above 20 %, Phase 3's "fresh `GzipDecoder` per
/// worker" assumption is pricier than expected; either the framing
/// path needs trimming or the parallel model needs a smaller-grained
/// task partition (see `internal/PLAN_gzip_throughput.md` Phase 3).
#[test]
#[ignore = "benchmark; opt-in via --ignored"]
fn bench_deflate_native_tar_gz_256mib_w1_member_overhead_bounded() {
    let archive = build_tar_payload(256 * 1024 * 1024);

    let single = encode_gzip(&archive);
    let multi = encode_gzip_multi_member(&archive, 8);

    let (single_out, single_elapsed) = run_peel(&single);
    assert_eq!(single_out, archive, "single-member peel != archive");
    let (multi_out, multi_elapsed) = run_peel(&multi);
    assert_eq!(multi_out, archive, "multi-member peel != archive");
    assert_eq!(
        single_out, multi_out,
        "single-member and multi-member decoded bytes differ"
    );

    let single_mibps = mibps(archive.len() as u64, single_elapsed);
    let multi_mibps = mibps(archive.len() as u64, multi_elapsed);
    let delta_pct = ((multi_mibps - single_mibps) / single_mibps).abs() * 100.0;
    let per_member_ms = (multi_elapsed.as_secs_f64() - single_elapsed.as_secs_f64()) * 1000.0 / 8.0;
    println!(
        "[bench-gz] framing-gate: single={single_mibps:7.1} MiB/s  multi={multi_mibps:7.1} MiB/s  \
         delta={delta_pct:5.2}%  per-member-overhead={per_member_ms:5.2} ms"
    );
    let gate_pct = 20.0;
    assert!(
        delta_pct <= gate_pct,
        "framing-overhead gate failed: multi-member W=1 throughput ({multi_mibps:.1} MiB/s) \
         differs from single-member ({single_mibps:.1} MiB/s) by {delta_pct:.2}% > {gate_pct:.1}%. \
         Per-member framing overhead is taking too large a share of decode time; Phase 3's \
         parallel-speedup model needs a tighter task partition or a leaner per-worker init \
         (see `internal/PLAN_gzip_throughput.md` Phase 3)."
    );
}

/// CRC-32/IEEE throughput over a 64 KiB L1-hot buffer iterated to
/// ~1 GiB of total work. Phase 1 of [`internal/PLAN_gzip_throughput.md`]:
/// the gzip per-member trailer's running CRC32 was ~7 % of decode
/// self-time at slicing-by-1 (mirrors the xz CRC64 share Phase 1 of
/// [`internal/PLAN_xz_decoder_optimization.md`] recovered). Slicing-by-16
/// must clear ≥ 5× speedup vs. byte-by-byte on the *same* hardware
/// to gate this phase's exit.
///
/// 64 KiB rather than 1 GiB because the production CRC32 path
/// hashes ~32 KiB chunks coming out of the deflate window, then
/// again, then again — the lookup tables live in L1 the whole time.
/// An L1-hot bench reflects that; a 1 GiB cold pass would be
/// memory-bandwidth-bound and underweight the table-lookup cost.
///
/// `#[ignore]` so it's opt-in like the rest of this file.
#[test]
#[ignore = "benchmark; opt-in via --ignored"]
fn bench_crc32_64kib() {
    use peel::zip::crc32::{ieee, Crc32};

    const BUF_LEN: usize = 64 * 1024;
    const TOTAL_BYTES: u64 = 1 << 30; // 1 GiB worth of work
    const ITERS: usize = (TOTAL_BYTES / BUF_LEN as u64) as usize;

    let buf = random_bytes(0xC0FFEE, BUF_LEN);

    fn report(label: &str, total: u64, dur: Duration, crc: u32) -> f64 {
        let gibps = (total as f64) / (1024.0 * 1024.0 * 1024.0) / dur.as_secs_f64();
        let gbps = (total as f64) / 1_000_000_000.0 / dur.as_secs_f64();
        println!(
            "[bench-gz] crc32 {label:<20} {iters} × 64 KiB = {gib:.2} GiB: {dur:.3}s  ({gibps:.2} GiB/s, {gbps:.2} GB/s)  crc=0x{crc:08x}",
            iters = ITERS,
            gib = (total as f64) / (1024.0 * 1024.0 * 1024.0),
            dur = dur.as_secs_f64(),
        );
        gbps
    }

    // Byte-by-byte reference — measured here so the plan's "≥ 5×"
    // gate is anchored to *this* hardware's slicing-by-1 number, not
    // a generic-MB/s assumption. The polynomial / initial-XOR /
    // final-XOR convention matches the production hasher; canonical
    // "123456789" → 0xCBF43926 pins both implementations to the same
    // CRC.
    fn byte_by_byte(data: &[u8]) -> u32 {
        const POLY: u32 = 0xEDB8_8320;
        let mut table = [0u32; 256];
        for (i, slot) in table.iter_mut().enumerate() {
            let mut c = i as u32;
            for _ in 0..8 {
                c = if c & 1 != 0 { (c >> 1) ^ POLY } else { c >> 1 };
            }
            *slot = c;
        }
        let mut state = !0u32;
        for &b in data {
            state = table[((state ^ u32::from(b)) & 0xFF) as usize] ^ (state >> 8);
        }
        !state
    }

    // Warm-up: pages-in the buffer + warms L1 with the table.
    std::hint::black_box(byte_by_byte(&buf));
    let mut warm = Crc32::new();
    warm.update(&buf);
    std::hint::black_box(warm.finalize());

    // Median of 3 timed runs each.
    let mut bb_times = [Duration::default(); 3];
    let mut bb_crc = 0u32;
    for slot in bb_times.iter_mut() {
        let started = Instant::now();
        // Streaming would let LLVM observe that the prior CRC of an
        // L1-hot buffer is invariant; we want it to recompute every
        // pass, so re-seed from scratch and accumulate via XOR.
        let mut acc = 0u32;
        for _ in 0..ITERS {
            acc ^= byte_by_byte(&buf);
        }
        bb_crc = acc;
        *slot = started.elapsed();
        std::hint::black_box(acc);
    }
    bb_times.sort();
    let bb_gbps = report("byte-by-byte", TOTAL_BYTES, bb_times[1], bb_crc);

    let mut s16_times = [Duration::default(); 3];
    let mut s16_crc = 0u32;
    for slot in s16_times.iter_mut() {
        let started = Instant::now();
        let mut acc = 0u32;
        for _ in 0..ITERS {
            acc ^= ieee(&buf);
        }
        s16_crc = acc;
        *slot = started.elapsed();
        std::hint::black_box(acc);
    }
    s16_times.sort();
    let s16_gbps = report("slicing-by-16", TOTAL_BYTES, s16_times[1], s16_crc);

    assert_eq!(
        bb_crc, s16_crc,
        "byte-by-byte and slicing-by-16 disagree on the same 64 KiB buffer (XOR-accumulated over {ITERS} iters)"
    );

    let speedup = s16_gbps / bb_gbps;
    println!(
        "[bench-gz] crc32 speedup vs byte-by-byte: {speedup:.2}× \
         (slicing-by-16 {s16_gbps:.2} GB/s / byte-by-byte {bb_gbps:.2} GB/s)"
    );

    // Phase 1 exit gate: ≥ 5× speedup over byte-by-byte on the same
    // host. (The xz CRC64 sister bench landed at ~6.5×; the floor
    // here is intentionally a hair lower since the u32 path has 4
    // extra "pure input" lookups in the inner loop and clears one
    // less bit of state per pass.)
    let gate = 5.0;
    assert!(
        speedup >= gate,
        "crc32 slicing-by-16 speedup regressed: got {speedup:.2}×, Phase 1 floor is ≥ {gate:.2}×"
    );
}

// ---- silence unused-helper warnings on non-bench builds --------------

/// `cargo test` without `--ignored` runs no `#[test]` here, but still
/// links the file. Some helpers above are unreachable then. Reference
/// them from a function that compiles under any flags so the build
/// stays warning-clean. (Mirrors the pattern in
/// `tests/test_bench_xz_native.rs`.)
#[allow(dead_code)]
fn _silence_unused() {
    let _ = (random_bytes(0, 0), build_tar_payload(0), encode_gzip(&[]));
    let _ = encode_gzip_multi_member(&[], 1);
    let _ = (
        run_peel as fn(&[u8]) -> (Vec<u8>, Duration),
        run_flate2 as fn(&[u8]) -> (Vec<u8>, Duration),
    );
    let _ = report as fn(&str, u64, u64, Duration, Duration);
    let _ = mibps as fn(u64, Duration) -> f64;
    let _ = bench_archive as fn(&[u8], &[u8], &str) -> Duration;
}
