//! Plan pinning: `EXPLAIN QUERY PLAN` snapshots on core queries. At the
//! ≤10ms/≤50ms latency budget an index regression is a correctness bug, so a
//! full table scan of `edges` or `nodes` in the hot paths fails CI.

use std::sync::Arc;

use grit_core::{GraphOp, Grit, ManualClock, Options};
use serde_json::json;

/// Create a real database file (so the planner sees the real schema), then
/// inspect plans over a plain rusqlite connection.
fn plan_of(sql: &str, params_count: usize) -> String {
    let dir = tempfile::tempdir().unwrap();
    let path = dir.path().join("plan.db");
    {
        let clock = Arc::new(ManualClock::new(1_000));
        let g = Grit::open(&path, Options::new("dev").clock(clock)).unwrap();
        // A little data so the planner has something to chew on.
        let a = g.new_id();
        let b = g.new_id();
        for (id, name) in [(a, "left"), (b, "right")] {
            g.apply(GraphOp::AddNode {
                id,
                kind: "k".into(),
                name: name.into(),
                summary: String::new(),
                attrs: json!({}),
                group_id: String::new(),
            })
            .unwrap();
        }
        g.apply(GraphOp::AddEdge {
            id: g.new_id(),
            src: a,
            dst: b,
            rel: "R".into(),
            fact: "left relates to right".into(),
            attrs: json!({}),
            group_id: String::new(),
            valid_at: None,
        })
        .unwrap();
    }

    let conn = rusqlite::Connection::open(&path).unwrap();
    let mut stmt = conn.prepare(&format!("EXPLAIN QUERY PLAN {sql}")).unwrap();
    let dummy: Vec<Box<dyn rusqlite::types::ToSql>> = (0..params_count)
        .map(|_| Box::new(1i64) as Box<dyn rusqlite::types::ToSql>)
        .collect();
    let refs: Vec<&dyn rusqlite::types::ToSql> = dummy.iter().map(|b| b.as_ref()).collect();
    let rows = stmt
        .query_map(refs.as_slice(), |r| r.get::<_, String>(3))
        .unwrap()
        .collect::<Result<Vec<_>, _>>()
        .unwrap();
    rows.join("\n")
}

#[test]
fn traversal_expansion_uses_edge_indexes() {
    for (name, sql) in [
        ("current", grit_core::EXPAND_SQL_CURRENT),
        ("as_at", grit_core::EXPAND_SQL_AS_AT),
    ] {
        let plan = plan_of(sql, 4);
        assert!(
            plan.contains("idx_edges_src") && plan.contains("idx_edges_dst"),
            "BFS expansion ({name}) must probe both edge indexes (OR-optimized), got:\n{plan}"
        );
        assert!(
            !plan.lines().any(|l| l.trim().starts_with("SCAN e")),
            "BFS expansion ({name}) must not full-scan edges, got:\n{plan}"
        );
    }
}

#[test]
fn incident_edge_lookup_uses_indexes() {
    let plan = plan_of("SELECT id FROM edges WHERE src = ?1 OR dst = ?1", 1);
    assert!(
        plan.contains("idx_edges_src") && plan.contains("idx_edges_dst"),
        "incident-edge lookup must be index-driven, got:\n{plan}"
    );
}

#[test]
fn node_point_lookup_uses_primary_key() {
    let plan = plan_of("SELECT name FROM nodes WHERE id = ?1", 1);
    assert!(
        plan.contains("USING INDEX sqlite_autoindex_nodes_1")
            || plan.contains("USING INTEGER PRIMARY KEY"),
        "node lookup must use the primary key, got:\n{plan}"
    );
}
