//! Latency-envelope tripwire, run in CI on every push (release mode,
//! `--ignored`): builds a mid-size fixture and asserts generous absolute
//! bounds on the hot paths. This is NOT the precision benchmark — criterion
//! (`cargo bench -p grit-core`) is — it exists to catch order-of-magnitude
//! regressions (the class that has actually happened: a missing index made
//! search 100x slower) on noisy shared runners without false alarms.

use std::sync::Arc;
use std::time::Instant;

use grit_core::{Budget, GraphOp, Grit, ManualClock, Options, Query, Traversal};
use uuid::Uuid;

struct Rng(u64);

impl Rng {
    fn next(&mut self) -> u64 {
        self.0 ^= self.0 << 13;
        self.0 ^= self.0 >> 7;
        self.0 ^= self.0 << 17;
        self.0
    }

    fn below(&mut self, n: usize) -> usize {
        (self.next() % n as u64) as usize
    }
}

const WORDS: &[&str] = &[
    "yoneda",
    "functor",
    "kernel",
    "image",
    "exact",
    "sequence",
    "lemma",
    "sheaf",
    "topos",
    "adjoint",
    "limit",
    "colimit",
    "monad",
    "algebra",
    "module",
    "tensor",
    "homology",
    "cohomology",
    "spectral",
    "fibration",
];

fn p95(mut samples: Vec<f64>) -> f64 {
    samples.sort_by(f64::total_cmp);
    samples[(samples.len() - 1) * 95 / 100]
}

#[test]
#[ignore = "several-second fixture build; CI runs it via --release -- --ignored"]
fn latency_envelope_tripwire() {
    const N_NODES: usize = 30_000;
    const N_EDGES: usize = 90_000;
    // Envelope targets are 10ms / 50ms on a desktop baseline at 100k nodes;
    // these bounds are ~3x headroom on a smaller graph, so only an
    // order-of-magnitude regression trips them — by design.
    const TRAVERSE_P95_MS: f64 = 30.0;
    const SEARCH_P95_MS: f64 = 150.0;
    const HISTORY_P95_MS: f64 = 10.0;

    let dir = tempfile::tempdir().unwrap();
    let clock = Arc::new(ManualClock::new(1_000_000_000));
    let g = Grit::open(dir.path().join("env.db"), Options::new("env").clock(clock)).unwrap();
    let mut rng = Rng(0x9E37_79B9_7F4A_7C15);

    let mut node_ids: Vec<Uuid> = Vec::with_capacity(N_NODES);
    for i in 0..N_NODES {
        let id = g.new_id();
        let (w1, w2, w3) = (
            WORDS[rng.below(WORDS.len())],
            WORDS[rng.below(WORDS.len())],
            WORDS[rng.below(WORDS.len())],
        );
        g.apply(GraphOp::AddNode {
            id,
            kind: "concept".into(),
            name: format!("{w1} {w2} {i}"),
            summary: format!("the {w3} in context {i}"),
            attrs: serde_json::json!({}),
            group_id: format!("g{}", i % 8),
        })
        .unwrap();
        node_ids.push(id);
    }
    let mut edge_ids: Vec<Uuid> = Vec::with_capacity(N_EDGES);
    for i in 0..N_EDGES {
        let id = g.new_id();
        let (w1, w2) = (WORDS[rng.below(WORDS.len())], WORDS[rng.below(WORDS.len())]);
        g.apply(GraphOp::AddEdge {
            id,
            src: node_ids[rng.below(N_NODES)],
            dst: node_ids[rng.below(N_NODES)],
            rel: "RELATES".into(),
            fact: format!("the {w1} constrains the {w2} ({i})"),
            attrs: serde_json::json!({}),
            group_id: format!("g{}", i % 8),
            valid_at: Some(1_000),
        })
        .unwrap();
        edge_ids.push(id);
    }
    for _ in 0..N_EDGES / 20 {
        g.apply(GraphOp::InvalidateEdge {
            edge_id: edge_ids[rng.below(N_EDGES)],
            invalid_at: 500_000_000,
        })
        .unwrap();
    }

    const ITERS: usize = 100;
    let mut traverse_ms = Vec::with_capacity(ITERS);
    let mut search_ms = Vec::with_capacity(ITERS);
    let mut history_ms = Vec::with_capacity(ITERS);
    for _ in 0..ITERS {
        let seed = node_ids[rng.below(N_NODES)];
        let t = Instant::now();
        g.traverse(&[seed], &Traversal::default().depth(3)).unwrap();
        traverse_ms.push(t.elapsed().as_secs_f64() * 1e3);

        let (w1, w2) = (WORDS[rng.below(WORDS.len())], WORDS[rng.below(WORDS.len())]);
        let t = Instant::now();
        g.search(Query::text(format!("{w1} {w2}")).budget(Budget::items(20)))
            .unwrap();
        search_ms.push(t.elapsed().as_secs_f64() * 1e3);

        let id = node_ids[rng.below(N_NODES)];
        let t = Instant::now();
        g.node_history(id).unwrap();
        history_ms.push(t.elapsed().as_secs_f64() * 1e3);
    }

    let (t, s, h) = (p95(traverse_ms), p95(search_ms), p95(history_ms));
    println!("envelope tripwire: traverse p95 {t:.2}ms, search p95 {s:.2}ms, history p95 {h:.2}ms");
    assert!(
        t <= TRAVERSE_P95_MS,
        "traverse p95 {t:.2}ms > {TRAVERSE_P95_MS}ms tripwire"
    );
    assert!(
        s <= SEARCH_P95_MS,
        "search p95 {s:.2}ms > {SEARCH_P95_MS}ms tripwire"
    );
    assert!(
        h <= HISTORY_P95_MS,
        "node_history p95 {h:.2}ms > {HISTORY_P95_MS}ms tripwire"
    );
}
