//! Performance-regression smoke benchmarks.
//!
//! This is a deliberately tiny Criterion suite used as a CI "canary": it is
//! meant to detect obvious, large performance regressions (≥3× slower) in
//! core engine paths. It is **not** a source of truth for performance
//! numbers — see `query_implementations`, `scale`, `realistic`, and the
//! WAL/concurrency suites for that.
//!
//! Run locally with:
//!   `cargo bench -p lora-database --bench perf_smoke`
//!
//! Regression-check with:
//!   `cargo bench -p lora-database --bench perf_smoke \
//!        -- --output-format bencher \
//!    | node scripts/check-perf-smoke.mjs`
//!
//! Baseline lives at
//! `crates/lora-database/benches/perf_smoke_baseline.json`. See
//! `docs/performance/perf-smoke.md` for the refresh flow.

mod fixtures;

use criterion::{criterion_group, criterion_main, BatchSize, Criterion};
use fixtures::*;
use lora_database::{
    Database, ExecuteOptions, InMemoryGraph, ResultFormat, SyncMode, TransactionMode, WalConfig,
};
use std::hint::black_box;
use std::path::PathBuf;
use std::time::{Duration, SystemTime, UNIX_EPOCH};

fn opts() -> Option<ExecuteOptions> {
    Some(ExecuteOptions {
        format: ResultFormat::Rows,
    })
}

/// Per-iteration scratch directory for WAL-backed benches. Removed on
/// drop, so each Criterion sample starts on an empty WAL.
struct WalScratch {
    path: PathBuf,
}

impl WalScratch {
    fn new(tag: &str) -> Self {
        let mut path = std::env::temp_dir();
        path.push(format!(
            "lora-perf-smoke-{}-{}-{}",
            tag,
            std::process::id(),
            SystemTime::now()
                .duration_since(UNIX_EPOCH)
                .unwrap()
                .as_nanos()
        ));
        std::fs::create_dir_all(&path).unwrap();
        Self { path }
    }
}

impl Drop for WalScratch {
    fn drop(&mut self) {
        let _ = std::fs::remove_dir_all(&self.path);
    }
}

/// Open a WAL-backed database with `SyncMode::GroupSync`. Group sync matches
/// the durability profile most embedded callers reach for — write-only
/// on commit, background flusher fsyncs on a 50 ms interval — and keeps
/// the bench dominated by engine + WAL append cost rather than fsync
/// latency.
fn open_wal_group(tag: &str) -> (WalScratch, Database<InMemoryGraph>) {
    let dir = WalScratch::new(tag);
    let cfg = WalConfig::Enabled {
        dir: dir.path.clone(),
        sync_mode: SyncMode::GroupSync { interval_ms: 50 },
        segment_target_bytes: 8 * 1024 * 1024,
    };
    let db = Database::open_with_wal(cfg).unwrap();
    (dir, db)
}

/// CI-friendly Criterion config: short warmup + short measurement + modest
/// sample count. Total measurement budget per bench ≈ 1.8 s.
fn smoke_config() -> Criterion {
    Criterion::default()
        .warm_up_time(Duration::from_millis(300))
        .measurement_time(Duration::from_millis(1_500))
        .sample_size(30)
        // Don't fail the bench binary on small noise-driven regressions that
        // criterion flags; regression detection is handled by the external
        // check script against a checked-in baseline.
        .noise_threshold(0.10)
}

fn bench_perf_smoke(c: &mut Criterion) {
    let mut group = c.benchmark_group("perf_smoke");

    // --- 1. simple scan: MATCH + RETURN on 1 000 nodes ---------------------
    {
        let db = build_node_graph(Scale::SMALL);
        group.bench_function("scan_1k", |b| {
            b.iter(|| {
                black_box(
                    db.service
                        .execute("MATCH (n:Node) RETURN n.id", opts())
                        .unwrap(),
                );
            });
        });
    }

    // --- 2. filtered query: predicate evaluation on 1 000 nodes ------------
    {
        let db = build_node_graph(Scale::SMALL);
        group.bench_function("filter_1k", |b| {
            b.iter(|| {
                black_box(
                    db.service
                        .execute("MATCH (n:Node) WHERE n.value > 50 RETURN n.id", opts())
                        .unwrap(),
                );
            });
        });
    }

    // --- 3. single-hop traversal on a 500-node chain -----------------------
    {
        let db = build_chain(500);
        group.bench_function("traversal_chain_500", |b| {
            b.iter(|| {
                black_box(
                    db.service
                        .execute(
                            "MATCH (a:Chain)-[:NEXT]->(b:Chain) RETURN a.idx, b.idx",
                            opts(),
                        )
                        .unwrap(),
                );
            });
        });
    }

    // --- 4. batched write: UNWIND + CREATE, fresh DB per iteration ---------
    group.bench_function("write_batch_100", |b| {
        b.iter_batched(
            BenchDb::new,
            |db| {
                black_box(
                    db.service
                        .execute(
                            "UNWIND list.range(1, 100) AS i CREATE (:B {id: i, val: i * 2})",
                            opts(),
                        )
                        .unwrap(),
                );
            },
            BatchSize::SmallInput,
        );
    });

    // --- 5. streaming read: full drain via Database::stream ----------------
    //
    // Exercises the live pull cursor (`StreamInner::Live`). Same workload
    // as `scan_1k` but goes through the streaming pipeline instead of
    // `execute_compiled_rows`. A regression here means the per-operator
    // pull pipeline got slower (or the LiveCursor RwLock guard wrapper did).
    {
        let db = build_node_graph(Scale::SMALL);
        group.bench_function("stream_scan_1k", |b| {
            b.iter(|| {
                let stream = db.service.stream("MATCH (n:Node) RETURN n.id").unwrap();
                let mut count = 0usize;
                for row in stream {
                    black_box(row);
                    count += 1;
                }
                black_box(count);
            });
        });
    }

    // --- 6. streaming read: pull-and-drop --------------------------------
    //
    // Pulls a single row then drops the stream. Should be O(1)-ish
    // regardless of underlying graph size — the test guards that the
    // pull pipeline stays lazy and the LiveCursor's Drop releases
    // promptly.
    {
        let db = build_node_graph(Scale::SMALL);
        group.bench_function("stream_pull_one", |b| {
            b.iter(|| {
                let mut stream = db.service.stream("MATCH (n:Node) RETURN n.id").unwrap();
                black_box(stream.next_row().unwrap());
                drop(stream);
            });
        });
    }

    // --- 7. streaming write: auto-commit drain ---------------------------
    //
    // Auto-commit write stream — exercises classify-stream → hidden tx
    // → mutable executor → AutoCommitGuard (delegating to
    // Transaction::commit). Fresh DB per iteration since the writes
    // commit on exhaustion.
    group.bench_function("stream_write_100", |b| {
        b.iter_batched(
            BenchDb::new,
            |db| {
                let stream = db
                    .service
                    .stream("UNWIND list.range(1, 100) AS i CREATE (:B {id: i}) RETURN i")
                    .unwrap();
                let mut count = 0usize;
                for row in stream {
                    black_box(row);
                    count += 1;
                }
                black_box(count);
            },
            BatchSize::SmallInput,
        );
    });

    // --- 7b. streaming write through Sort (M4 wave-1 regression gate) -----
    //
    // `MATCH ... ORDER BY ... CREATE` exercises `SortSource` inside the
    // streaming chain: Sort buffers internally (inherent), then yields
    // sorted rows lazily into the per-row CREATE. Pre-M4 this fell back
    // to the buffered path (~2N peak memory); post-M4 the chain holds
    // ~N (Sort's buffer only). This bench locks in that regression
    // surface — a refactor that re-routes ORDER BY → CREATE through the
    // buffered path would show up as a notable slowdown here.
    group.bench_function("stream_write_sort_100", |b| {
        b.iter_batched(
            BenchDb::new,
            |db| {
                // Pre-seed 100 source nodes so the MATCH has work to do.
                db.service
                    .execute(
                        "UNWIND list.range(1, 100) AS i CREATE (:Src {i: i})",
                        opts(),
                    )
                    .unwrap();
                let stream = db
                    .service
                    .stream(
                        "MATCH (s:Src) WITH s ORDER BY s.i DESC \
                         CREATE (:Top {origin: s.i}) RETURN s.i AS i",
                    )
                    .unwrap();
                let mut count = 0usize;
                for row in stream {
                    black_box(row);
                    count += 1;
                }
                black_box(count);
            },
            BatchSize::SmallInput,
        );
    });

    // --- 8. transaction round-trip: empty begin → commit -----------------
    //
    // Measures the fixed cost of opening + committing an explicit
    // read-write transaction with zero statements. Staging is lazy, so this
    // should not clone the graph.
    group.bench_function("tx_roundtrip_empty", |b| {
        let db = build_node_graph(Scale::SMALL);
        b.iter(|| {
            let tx = db
                .service
                .begin_transaction(TransactionMode::ReadWrite)
                .unwrap();
            tx.commit().unwrap();
        });
    });

    // --- 9. transaction with one read statement --------------------------
    //
    // Single-statement read-only transaction: no staging clone, just the
    // transaction wrapper plus executor + commit.
    group.bench_function("tx_read_1k", |b| {
        let db = build_node_graph(Scale::SMALL);
        b.iter(|| {
            let mut tx = db
                .service
                .begin_transaction(TransactionMode::ReadOnly)
                .unwrap();
            black_box(tx.execute("MATCH (n:Node) RETURN n.id", opts()).unwrap());
            tx.commit().unwrap();
        });
    });

    // --- 10. transaction with one write statement ------------------------
    //
    // Single-statement read-write transaction. Fresh DB per
    // iteration because the writes get published on commit.
    group.bench_function("tx_write_100", |b| {
        b.iter_batched(
            BenchDb::new,
            |db| {
                let mut tx = db
                    .service
                    .begin_transaction(TransactionMode::ReadWrite)
                    .unwrap();
                black_box(
                    tx.execute("UNWIND list.range(1, 100) AS i CREATE (:B {id: i})", opts())
                        .unwrap(),
                );
                tx.commit().unwrap();
            },
            BatchSize::SmallInput,
        );
    });

    // --- 11. persistent single CREATE: WAL group sync --------------------
    //
    // Auto-commit `CREATE` against a WAL-backed DB. The DB is opened
    // once and shared across iterations — the WAL grows, but UNWIND-free
    // CREATE has no scan cost, so per-iteration cost stays steady-state.
    // What this bench captures is one record-encode + one segment append
    // + one `Arc::make_mut` mutation. A regression here means the
    // durable single-write path picked up per-call overhead beyond the
    // fixed WAL append cost.
    {
        let (_dir, db) = open_wal_group("write-one");
        group.bench_function("write_one_wal_group", |b| {
            b.iter(|| {
                black_box(db.execute("CREATE (:B {n: 1})", opts()).unwrap());
            });
        });
    }

    // --- 12. persistent batched write: WAL group sync --------------------
    //
    // Same shape as `write_batch_100` but committed durably via the
    // group-sync WAL path. Distance from `write_batch_100` is the
    // batched-write WAL overhead (one append, not 100). A regression
    // that affects only the persistence write path — e.g., a per-row
    // WAL fence or extra encode pass — shows up here without moving
    // the in-memory baseline.
    {
        let (_dir, db) = open_wal_group("write-batch-100");
        group.bench_function("write_batch_100_wal_group", |b| {
            b.iter(|| {
                black_box(
                    db.execute(
                        "UNWIND list.range(1, 100) AS i CREATE (:B {id: i, val: i * 2})",
                        opts(),
                    )
                    .unwrap(),
                );
            });
        });
    }

    // --- 13. persistent explicit transaction: WAL group sync -------------
    //
    // Mirror of `tx_write_100` against the WAL/group profile. Catches
    // regressions in the explicit-transaction commit path where it
    // intersects the WAL recorder (transaction commit promotes the
    // staged graph and appends one batched commit record).
    {
        let (_dir, db) = open_wal_group("tx-write-100");
        group.bench_function("tx_write_100_wal_group", |b| {
            b.iter(|| {
                let mut tx = db.begin_transaction(TransactionMode::ReadWrite).unwrap();
                black_box(
                    tx.execute("UNWIND list.range(1, 100) AS i CREATE (:B {id: i})", opts())
                        .unwrap(),
                );
                tx.commit().unwrap();
            });
        });
    }

    // --- 14. persistent single CREATE: WAL + force_fsync per commit ------
    //
    // Same shape as `write_one_wal_group` but adds an explicit
    // `Database::sync()` on each iteration, forcing the WAL through to
    // durable storage before the next commit lands. This captures the
    // fully-durable explicit-sync profile; the gap to `write_one_wal_group`
    // is one `fsync` per iteration.
    // A regression here without one in `write_one_wal_group` points at
    // the durability boundary path (`force_fsync` / `sync_dir`).
    {
        let (_dir, db) = open_wal_group("write-one-persistent");
        group.bench_function("write_one_wal_persistent", |b| {
            b.iter(|| {
                black_box(db.execute("CREATE (:B {n: 1})", opts()).unwrap());
                db.sync().unwrap();
            });
        });
    }

    // --- 15. persistent read: WAL group sync -----------------------------
    //
    // Read-heavy workload against a WAL-backed DB. Reads should not touch
    // the recorder, so this should track `scan_1k` (#1) ± noise. A
    // regression here without one in `scan_1k` points at the WAL recorder
    // leaking into the read path (e.g. an extra guard or fence taken on
    // every MATCH).
    {
        let (_dir, db) = open_wal_group("scan-wal");
        db.execute(
            "UNWIND list.range(1, 1000) AS i CREATE (:Node {id: i, value: i % 100})",
            opts(),
        )
        .unwrap();
        group.bench_function("scan_1k_wal_group", |b| {
            b.iter(|| {
                black_box(db.execute("MATCH (n:Node) RETURN n.id", opts()).unwrap());
            });
        });
    }

    // --- 16. persistent batched delete: WAL group sync -------------------
    //
    // MATCH + DELETE on 100 nodes against a freshly seeded DB. Exercises
    // the WAL node-tombstone record path, which is distinct from the
    // node-create path that #11/#12 cover. Fresh DB per iteration since
    // each batch consumes the seed.
    group.bench_function("delete_100_wal_group", |b| {
        b.iter_batched(
            || {
                let (dir, db) = open_wal_group("delete-100");
                db.execute("UNWIND list.range(1, 100) AS i CREATE (:D {id: i})", opts())
                    .unwrap();
                (dir, db)
            },
            |(_dir, db)| {
                black_box(db.execute("MATCH (n:D) DELETE n", opts()).unwrap());
            },
            BatchSize::SmallInput,
        );
    });

    // --- 17. persistent property update: WAL group sync ------------------
    //
    // MATCH + SET on 100 nodes — exercises the property-mutation WAL
    // record path (distinct from create/delete). DB is shared across
    // iterations: each SET re-stamps the same property and appends one
    // batched commit record, giving a steady-state cost per iteration.
    {
        let (_dir, db) = open_wal_group("update-100");
        db.execute("UNWIND list.range(1, 100) AS i CREATE (:U {id: i})", opts())
            .unwrap();
        group.bench_function("update_100_wal_group", |b| {
            b.iter(|| {
                black_box(
                    db.execute("MATCH (n:U) SET n.touched = true", opts())
                        .unwrap(),
                );
            });
        });
    }

    // --- 18. WAL replay on open ------------------------------------------
    //
    // Open a WAL directory that already has 100 committed records and
    // measure how long the replay-on-open path takes. Bounds startup time
    // on a crash-recovery boot. The WAL is built once outside the iter and
    // its scratch directory is `mem::forget`-ed so the files stay alive
    // across every Criterion iteration (the OS reclaims at process exit).
    {
        let dir = WalScratch::new("replay-100");
        {
            let cfg = WalConfig::Enabled {
                dir: dir.path.clone(),
                sync_mode: SyncMode::GroupSync { interval_ms: 50 },
                segment_target_bytes: 8 * 1024 * 1024,
            };
            let db = Database::open_with_wal(cfg).unwrap();
            for _ in 0..100 {
                db.execute("CREATE (:R {n: 1})", opts()).unwrap();
            }
            drop(db);
        }
        let path = dir.path.clone();
        std::mem::forget(dir);
        group.bench_function("wal_replay_100", |b| {
            b.iter(|| {
                let cfg = WalConfig::Enabled {
                    dir: path.clone(),
                    sync_mode: SyncMode::GroupSync { interval_ms: 50 },
                    segment_target_bytes: 8 * 1024 * 1024,
                };
                let db = Database::open_with_wal(cfg).unwrap();
                black_box(db.node_count());
            });
        });
    }

    group.finish();
}

criterion_group! {
    name = benches;
    config = smoke_config();
    targets = bench_perf_smoke,
}
criterion_main!(benches);
