//! Exit criteria integration tests for Milestone 5: Hybrid Search & RRF.

use luci_index::index::Index;
use luci_mapping::{FieldType,Mapping};
use serde_json::json;

fn test_dir(name: &str) -> std::path::PathBuf {
    let dir = std::env::temp_dir().join(format!(
        "luci_m5_integration_{}_{name}",
        std::process::id()
    ));
    let _ = std::fs::remove_dir_all(&dir);
    dir
}

fn cleanup(path: &std::path::Path) {
    let _ = std::fs::remove_dir_all(path);
}

fn build_hybrid_index(name: &str) -> (std::path::PathBuf, Index) {
    let path = test_dir(name);
    let schema = Mapping::builder()
        .field("title", FieldType::Text)
        .field("tag", FieldType::Keyword)
        .field("embedding", FieldType::DenseVector { dims: 4 })
        .build();
    let mut index = Index::create_with_mapping(&path, schema).unwrap();

    // Doc 0: relevant by text ("search engine"), embedding near [1,0,0,0]
    index.add(json!({"title": "search engine design", "tag": "tech", "embedding": [0.9, 0.1, 0.0, 0.0]})).unwrap();
    // Doc 1: relevant by text ("search"), embedding near [0,1,0,0]
    index.add(json!({"title": "search algorithms", "tag": "tech", "embedding": [0.1, 0.9, 0.0, 0.0]})).unwrap();
    // Doc 2: irrelevant by text, but embedding near [1,0,0,0]
    index.add(json!({"title": "cute cats", "tag": "animal", "embedding": [0.8, 0.2, 0.0, 0.0]})).unwrap();
    // Doc 3: relevant by text, embedding far
    index.add(json!({"title": "search optimization", "tag": "tech", "embedding": [0.0, 0.0, 0.9, 0.1]})).unwrap();
    // Doc 4: irrelevant by both
    index.add(json!({"title": "happy dog", "tag": "animal", "embedding": [0.0, 0.0, 0.0, 1.0]})).unwrap();

    index.commit().unwrap();
    (path, index)
}

/// Exit Criterion 1: Hybrid query combines text + vector via RRF.
#[test]
fn hybrid_search_rrf() {
    let (path, mut index) = build_hybrid_index("hybrid");

    let results = index.search(json!({
        "query": {"match": {"title": "search engine"}},
        "knn": {
            "field": "embedding",
            "query_vector": [1.0, 0.0, 0.0, 0.0],
            "k": 5,
            "num_candidates": 10
        },
        "size": 5
    }), 5).unwrap();

    // Doc 0 should rank highest: relevant by BOTH text and vector
    assert!(!results.hits.is_empty());
    assert_eq!(results.hits[0].doc_id.as_u32(), 0,
        "Doc 0 ('search engine design' + near [1,0,0,0]) should be top result");

    // Doc 2 (cute cats, near [1,0,0,0]) and Doc 1 (search algorithms, near [0,1,0,0])
    // should both appear — one from vector, one from text
    let ids: Vec<u32> = results.hits.iter().map(|h| h.doc_id.as_u32()).collect();
    assert!(ids.contains(&1), "Doc 1 (text match) should appear");
    assert!(ids.contains(&2), "Doc 2 (vector match) should appear");

    cleanup(&path);
}

/// Exit Criterion 2: Text-only query still works in structured format.
#[test]
fn text_only_structured() {
    let (path, mut index) = build_hybrid_index("text_only");

    let results = index.search(json!({
        "query": {"match": {"title": "search"}},
        "size": 10
    }), 10).unwrap();

    assert!(results.total_hits.value >= 3); // docs 0, 1, 3 mention "search"

    cleanup(&path);
}

/// Exit Criterion 3: KNN-only query works.
#[test]
fn knn_only_structured() {
    let (path, mut index) = build_hybrid_index("knn_only");

    let results = index.search(json!({
        "knn": {
            "field": "embedding",
            "query_vector": [1.0, 0.0, 0.0, 0.0],
            "k": 3,
            "num_candidates": 10
        },
        "size": 3
    }), 3).unwrap();

    assert_eq!(results.hits.len(), 3);
    // Closest to [1,0,0,0]: doc 0 ([0.9,0.1,0,0]) then doc 2 ([0.8,0.2,0,0])
    assert_eq!(results.hits[0].doc_id.as_u32(), 0);
    assert_eq!(results.hits[1].doc_id.as_u32(), 2);

    cleanup(&path);
}

/// Exit Criterion 4: RRF scores are rank-based, not raw scores.
#[test]
fn rrf_rank_based_scores() {
    let (path, mut index) = build_hybrid_index("rrf_scores");

    let results = index.search(json!({
        "query": {"match": {"title": "search"}},
        "knn": {
            "field": "embedding",
            "query_vector": [1.0, 0.0, 0.0, 0.0],
            "k": 5,
            "num_candidates": 10
        },
        "size": 5
    }), 5).unwrap();

    // All RRF scores should be positive and in descending order
    for i in 0..results.hits.len().saturating_sub(1) {
        assert!(results.hits[i].score >= results.hits[i + 1].score,
            "RRF scores should be in descending order");
    }
    for hit in &results.hits {
        assert!(hit.score > 0.0, "RRF scores should be positive");
    }

    cleanup(&path);
}
