//! Exit criteria integration tests for Milestone 3: Aggregations.

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_m3_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_test_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("price", FieldType::Float)
        .field("quantity", FieldType::Integer)
        .build();
    let mut index = Index::create_with_mapping(&path, schema).unwrap();

    let docs = vec![
        json!({"title": "Widget A", "tag": "electronics", "price": 29.99, "quantity": 100}),
        json!({"title": "Widget B", "tag": "electronics", "price": 49.99, "quantity": 50}),
        json!({"title": "Gadget C", "tag": "electronics", "price": 99.99, "quantity": 25}),
        json!({"title": "Book D", "tag": "books", "price": 14.99, "quantity": 200}),
        json!({"title": "Book E", "tag": "books", "price": 24.99, "quantity": 150}),
        json!({"title": "Toy F", "tag": "toys", "price": 9.99, "quantity": 500}),
        json!({"title": "Toy G", "tag": "toys", "price": 19.99, "quantity": 300}),
        json!({"title": "Toy H", "tag": "toys", "price": 4.99, "quantity": 1000}),
    ];

    for doc in docs {
        index.add(doc).unwrap();
    }
    index.commit().unwrap();

    (path, index)
}

/// Exit Criterion 1: Terms aggregation returns correct bucket counts.
#[test]
fn terms_aggregation() {
    let (path, mut index) = build_test_index("terms");

    let results = index.search(json!({
        "query": {"match_all": {}},
        "aggs": {
            "by_tag": {"terms": {"field": "tag"}}
        }
    }), 10).unwrap();

    assert_eq!(results.total_hits.value, 8);
    let by_tag = &results.aggregations["by_tag"];
    let buckets_json = by_tag.to_json();
    let buckets = buckets_json["buckets"].as_array().unwrap();

    // Find each tag bucket
    let find_count = |key: &str| -> u64 {
        buckets.iter()
            .find(|b| b["key"].as_str() == Some(key))
            .map(|b| b["doc_count"].as_u64().unwrap())
            .unwrap_or(0)
    };

    assert_eq!(find_count("electronics"), 3);
    assert_eq!(find_count("books"), 2);
    assert_eq!(find_count("toys"), 3);

    cleanup(&path);
}

/// Exit Criterion 2: Metric aggregations produce correct results.
#[test]
fn metric_aggregations() {
    let (path, mut index) = build_test_index("metrics");

    let results = index.search(json!({
        "query": {"match_all": {}},
        "aggs": {
            "avg_price": {"avg": {"field": "price"}},
            "total_price": {"sum": {"field": "price"}},
            "min_price": {"min": {"field": "price"}},
            "max_price": {"max": {"field": "price"}},
            "count": {"value_count": {"field": "price"}}
        }
    }), 0).unwrap();

    let avg = results.aggregations["avg_price"].to_json()["value"].as_f64().unwrap();
    let sum = results.aggregations["total_price"].to_json()["value"].as_f64().unwrap();
    let min = results.aggregations["min_price"].to_json()["value"].as_f64().unwrap();
    let max = results.aggregations["max_price"].to_json()["value"].as_f64().unwrap();
    let count = results.aggregations["count"].to_json()["value"].as_f64().unwrap();

    // Prices: 29.99, 49.99, 99.99, 14.99, 24.99, 9.99, 19.99, 4.99
    let expected_sum = 29.99 + 49.99 + 99.99 + 14.99 + 24.99 + 9.99 + 19.99 + 4.99;
    assert!((sum - expected_sum).abs() < 0.1, "sum: {sum} expected: {expected_sum}");
    assert!((avg - expected_sum / 8.0).abs() < 0.1);
    assert!((min - 4.99).abs() < 0.1);
    assert!((max - 99.99).abs() < 0.1);
    assert_eq!(count, 8.0);

    cleanup(&path);
}

/// Exit Criterion 3: Stats aggregation.
#[test]
fn stats_aggregation() {
    let (path, mut index) = build_test_index("stats");

    let results = index.search(json!({
        "query": {"match_all": {}},
        "aggs": {
            "price_stats": {"stats": {"field": "price"}}
        }
    }), 0).unwrap();

    let stats = results.aggregations["price_stats"].to_json();
    assert!(stats["count"].as_f64().unwrap() == 8.0);
    assert!(stats["min"].as_f64().unwrap() < 5.0);
    assert!(stats["max"].as_f64().unwrap() > 99.0);
    assert!(stats["sum"].as_f64().unwrap() > 200.0);
    assert!(stats["value"].as_f64().unwrap() > 20.0); // avg

    cleanup(&path);
}

/// Exit Criterion 4: Aggregations with a filtered query.
#[test]
fn aggs_with_query_filter() {
    let (path, mut index) = build_test_index("filtered_aggs");

    // Only aggregate over "electronics" docs
    let results = index.search(json!({
        "query": {"term": {"tag": "electronics"}},
        "aggs": {
            "avg_price": {"avg": {"field": "price"}}
        }
    }), 10).unwrap();

    assert_eq!(results.total_hits.value, 3);
    let avg = results.aggregations["avg_price"].to_json()["value"].as_f64().unwrap();
    // Electronics prices: 29.99, 49.99, 99.99 → avg ≈ 59.99
    assert!((avg - 59.99).abs() < 1.0, "avg: {avg}");

    cleanup(&path);
}

/// Exit Criterion 5: Sub-aggregation nesting (terms → avg).
#[test]
fn sub_aggregation_nesting() {
    let (path, mut index) = build_test_index("nesting");

    let results = index.search(json!({
        "query": {"match_all": {}},
        "aggs": {
            "by_tag": {
                "terms": {"field": "tag"},
                "aggs": {
                    "avg_price": {"avg": {"field": "price"}}
                }
            }
        }
    }), 0).unwrap();

    let by_tag = results.aggregations["by_tag"].to_json();
    let buckets = by_tag["buckets"].as_array().unwrap();

    // Find the electronics bucket
    let electronics = buckets.iter()
        .find(|b| b["key"].as_str() == Some("electronics"))
        .expect("should have electronics bucket");
    assert_eq!(electronics["doc_count"], 3);

    // Sub-agg: avg_price for electronics = (29.99 + 49.99 + 99.99) / 3 ≈ 59.99
    let avg = electronics["avg_price"]["value"].as_f64().unwrap();
    assert!((avg - 59.99).abs() < 1.0, "electronics avg_price={avg}");

    // Books avg = (14.99 + 24.99) / 2 = 19.99
    let books = buckets.iter()
        .find(|b| b["key"].as_str() == Some("books"))
        .expect("should have books bucket");
    let books_avg = books["avg_price"]["value"].as_f64().unwrap();
    assert!((books_avg - 19.99).abs() < 1.0, "books avg_price={books_avg}");

    cleanup(&path);
}

/// Exit Criterion 6: Histogram aggregation.
#[test]
fn histogram_aggregation() {
    let (path, mut index) = build_test_index("histogram");

    let results = index.search(json!({
        "query": {"match_all": {}},
        "aggs": {
            "price_hist": {"histogram": {"field": "price", "interval": 25.0}}
        }
    }), 0).unwrap();

    let hist = results.aggregations["price_hist"].to_json();
    let buckets = hist["buckets"].as_array().unwrap();
    assert!(!buckets.is_empty(), "histogram should have buckets");

    // Verify buckets are ordered by key
    for i in 0..buckets.len() - 1 {
        let k1 = buckets[i]["key"].as_f64().unwrap();
        let k2 = buckets[i + 1]["key"].as_f64().unwrap();
        assert!(k1 <= k2, "histogram buckets should be ordered");
    }

    cleanup(&path);
}
