//! Search latency profiling across all query types and configurations.

use super::harness::*;
use serde_json::json;

const WARMUP: usize = 5;
const ITERS: usize = 200;

#[test]
fn basic_queries() {
    println!("\n=== Search: Basic Queries (50K docs, single segment) ===\n");

    let (path, mut index) = build_text_corpus("search_basic", 50_000);

    print_measurement_header();
    let queries: Vec<(&str, serde_json::Value)> = vec![
        ("match_all", json!({"match_all": {}})),
        ("term_keyword", json!({"term": {"tag": "tech"}})),
        ("match_single", json!({"match": {"title": "document"}})),
        ("match_multi", json!({"match": {"body": "body text content"}})),
        ("phrase_2term", json!({"match_phrase": {"body": "body text"}})),
        ("phrase_3term", json!({"match_phrase": {"body": "body text for"}})),
        ("prefix", json!({"prefix": {"tag": "sci"}})),
        ("exists", json!({"exists": {"field": "title"}})),
    ];

    // Regression bands: (floor_us, ceiling_us)
    // Run each test IN ISOLATION to get accurate p99:
    //   cargo test -p luci-index --test profile --release search::basic_queries -- --nocapture
    // Floor = ~0.2x isolated p99. Ceiling = ~5x isolated p99.
    // Baseline: M2 Max, 64GB, release, post-P3 cached reader fix, isolated.
    let bands_us: Vec<(&str, u64, u64)> = vec![
        //                          floor    ceiling
        ("match_all",                50,      2_000),     // isolated p99 ~259us
        ("term_keyword",             20,      1_000),     // isolated p99 ~200us
        ("match_single",             50,      2_000),     // isolated p99 ~500us
        ("match_multi",             500,    100_000),     // isolated p99 ~20ms
        ("phrase_2term",            200,     10_000),     // isolated p99 ~754us (post phrase optimizations)
        ("phrase_3term",            200,     10_000),     // isolated p99 ~800us (post phrase optimizations)
        ("prefix",                   20,      1_000),     // isolated p99 ~200us
        ("exists",                   20,      1_000),     // isolated p99 ~200us
    ];

    for (name, query) in &queries {
        let m = measure_query(name, &mut index, query.clone(), 10, WARMUP, ITERS);
        print_measurement(&m);
        if let Some(&(_, floor, ceiling)) = bands_us.iter().find(|(n, _, _)| n == name) {
            m.assert_p99_between_us(floor, ceiling);
        }
    }
    cleanup(&path);
}

#[test]
fn bool_query_variations() {
    println!("\n=== Search: Bool Query Variations (50K docs) ===\n");

    let (path, mut index) = build_text_corpus("search_bool", 50_000);

    print_measurement_header();
    let queries: Vec<(&str, serde_json::Value)> = vec![
        ("must_1clause", json!({"bool": {"must": [{"match": {"title": "document"}}]}})),
        ("must_2clause", json!({"bool": {"must": [
            {"match": {"title": "document"}},
            {"term": {"tag": "tech"}}
        ]}})),
        ("must_3clause", json!({"bool": {"must": [
            {"match": {"title": "document"}},
            {"match": {"body": "text"}},
            {"term": {"tag": "tech"}}
        ]}})),
        ("should_2clause", json!({"bool": {"should": [
            {"term": {"tag": "tech"}},
            {"term": {"tag": "science"}}
        ]}})),
        ("should_5clause", json!({"bool": {"should": [
            {"term": {"tag": "tech"}},
            {"term": {"tag": "science"}},
            {"term": {"tag": "sports"}},
            {"term": {"tag": "politics"}},
            {"term": {"tag": "entertainment"}}
        ]}})),
        ("must+filter", json!({"bool": {
            "must": [{"match": {"title": "document"}}],
            "filter": [{"term": {"tag": "tech"}}]
        }})),
        ("must+must_not", json!({"bool": {
            "must": [{"match": {"title": "document"}}],
            "must_not": [{"term": {"tag": "tech"}}]
        }})),
        ("must+should+filter", json!({"bool": {
            "must": [{"match": {"title": "document"}}],
            "should": [{"match": {"body": "content"}}],
            "filter": [{"term": {"tag": "tech"}}]
        }})),
        ("nested_bool", json!({"bool": {
            "must": [{"bool": {
                "should": [
                    {"term": {"tag": "tech"}},
                    {"term": {"tag": "science"}}
                ]
            }}],
            "filter": [{"match": {"title": "document"}}]
        }})),
        ("const_score_filter", json!({"constant_score": {
            "filter": {"term": {"tag": "tech"}},
            "boost": 1.0
        }})),
    ];

    for (name, query) in &queries {
        let m = measure_query(name, &mut index, query.clone(), 10, WARMUP, ITERS);
        print_measurement(&m);
    }
    cleanup(&path);
}

#[test]
fn multi_segment() {
    println!("\n=== Search: Multi-Segment Scaling ===\n");

    for &(segs, docs_per) in &[(1, 50_000), (5, 10_000), (10, 5_000), (50, 1_000)] {
        let total = segs * docs_per;
        let label = format!("{segs}seg_x_{docs_per}docs");
        let (path, mut index) = build_multi_segment_corpus(&label, segs, docs_per);

        let m = measure_query(
            &format!("{segs} segs ({total} total)"),
            &mut index,
            json!({"term": {"tag": "tech"}}),
            10, WARMUP, ITERS,
        );
        print_measurement(&m);
        cleanup(&path);
    }
}

#[test]
fn top_k_scaling() {
    println!("\n=== Search: Top-K Scaling (50K docs, match_all) ===\n");

    let (path, mut index) = build_text_corpus("search_topk", 50_000);

    print_measurement_header();
    for &k in &[1, 10, 100, 1_000] {
        let m = measure_query(
            &format!("top_{k}"),
            &mut index,
            json!({"match_all": {}}),
            k, WARMUP, ITERS,
        );
        print_measurement(&m);
    }
    cleanup(&path);
}

#[test]
fn disjunction_term_count() {
    println!("\n=== Search: Disjunction Scaling by Term Count (50K docs) ===\n");

    let (path, mut index) = build_text_corpus("search_disj", 50_000);

    print_measurement_header();
    let term_lists: Vec<(&str, serde_json::Value)> = vec![
        ("should_2terms", json!({"bool": {"should": [
            {"match": {"body": "body"}},
            {"match": {"body": "text"}}
        ]}})),
        ("should_3terms", json!({"bool": {"should": [
            {"match": {"body": "body"}},
            {"match": {"body": "text"}},
            {"match": {"body": "content"}}
        ]}})),
        ("should_5terms", json!({"bool": {"should": [
            {"match": {"body": "body"}},
            {"match": {"body": "text"}},
            {"match": {"body": "content"}},
            {"match": {"body": "covering"}},
            {"match": {"body": "discussions"}}
        ]}})),
        ("match_5words", json!({"match": {"body": "body text content covering discussions"}})),
    ];

    for (name, query) in &term_lists {
        let m = measure_query(name, &mut index, query.clone(), 10, WARMUP, ITERS);
        print_measurement(&m);
    }
    cleanup(&path);
}

#[test]
fn large_documents() {
    println!("\n=== Search: Large Document Scaling ===\n");

    for &body_words in &[10, 100, 500, 2_000] {
        let label = format!("large_{body_words}w");
        let path = profile_dir(&label);
        let mut index = luci_index::index::Index::create_with_mapping(&path, text_schema()).unwrap();

        let body_template: String = (0..body_words)
            .map(|i| format!("word{} ", i % 500))
            .collect();

        let start = std::time::Instant::now();
        for i in 0..5_000usize {
            index.add(json!({
                "title": format!("doc {i}"),
                "body": format!("{body_template} unique{i}"),
                "tag": "test",
                "price": 9.99,
            })).unwrap();
        }
        index.commit().unwrap();
        let index_time = start.elapsed();

        let m = measure_query(
            &format!("{body_words}w/doc (5K docs)"),
            &mut index,
            json!({"match": {"body": "word0"}}),
            10, WARMUP, ITERS,
        );
        println!("{body_words:>5} words/doc: index={:.0}ms  search_p50={:.1}us  hits={}",
            index_time.as_millis(), m.p50.as_micros() as f64, m.hits);

        cleanup(&path);
    }
}
