//! Integration tests for geo_shape field type and query.

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_geo_shape_{}_{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_shape_index(name: &str) -> (std::path::PathBuf, Index) {
    let path = test_dir(name);
    let schema = Mapping::builder()
        .field("name", FieldType::Keyword)
        .field("boundary", FieldType::GeoShape)
        .build();
    let mut index = Index::create_with_mapping(&path, schema).unwrap();

    // Doc 0: NYC area (polygon around Manhattan)
    index.add(json!({
        "name": "nyc",
        "boundary": {
            "type": "Polygon",
            "coordinates": [[
                [-74.05, 40.68], [-73.90, 40.68], [-73.90, 40.82],
                [-74.05, 40.82], [-74.05, 40.68]
            ]]
        }
    })).unwrap();

    // Doc 1: London area
    index.add(json!({
        "name": "london",
        "boundary": {
            "type": "Polygon",
            "coordinates": [[
                [-0.30, 51.40], [0.10, 51.40], [0.10, 51.60],
                [-0.30, 51.60], [-0.30, 51.40]
            ]]
        }
    })).unwrap();

    // Doc 2: Paris area
    index.add(json!({
        "name": "paris",
        "boundary": {
            "type": "Polygon",
            "coordinates": [[
                [2.20, 48.80], [2.50, 48.80], [2.50, 48.92],
                [2.20, 48.92], [2.20, 48.80]
            ]]
        }
    })).unwrap();

    // Doc 3: A point geometry (Empire State Building)
    index.add(json!({
        "name": "empire_state",
        "boundary": {
            "type": "Point",
            "coordinates": [-73.9857, 40.7484]
        }
    })).unwrap();

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

/// geo_shape intersects: polygon query overlapping NYC
#[test]
fn geo_shape_intersects() {
    let (path, mut index) = build_shape_index("intersects");

    // Query polygon overlaps NYC and the Empire State point
    let results = index.search(json!({
        "query": {
            "geo_shape": {
                "boundary": {
                    "shape": {
                        "type": "Polygon",
                        "coordinates": [[
                            [-74.00, 40.74], [-73.96, 40.74], [-73.96, 40.76],
                            [-74.00, 40.76], [-74.00, 40.74]
                        ]]
                    },
                    "relation": "intersects"
                }
            }
        }
    }), 10).unwrap();

    let names: Vec<String> = results.hits.iter()
        .filter_map(|h| h.source.as_ref()?.get("name")?.as_str().map(String::from))
        .collect();
    assert!(names.contains(&"nyc".to_string()), "NYC should intersect: {names:?}");
    assert!(names.contains(&"empire_state".to_string()), "Empire State point should intersect: {names:?}");
    assert!(!names.contains(&"london".to_string()), "London should NOT intersect: {names:?}");
    assert!(!names.contains(&"paris".to_string()), "Paris should NOT intersect: {names:?}");

    cleanup(&path);
}

/// geo_shape disjoint: shapes NOT overlapping the query
#[test]
fn geo_shape_disjoint() {
    let (path, mut index) = build_shape_index("disjoint");

    // Small query polygon inside NYC — disjoint returns everything that DOESN'T overlap
    let results = index.search(json!({
        "query": {
            "geo_shape": {
                "boundary": {
                    "shape": {
                        "type": "Polygon",
                        "coordinates": [[
                            [-74.00, 40.74], [-73.96, 40.74], [-73.96, 40.76],
                            [-74.00, 40.76], [-74.00, 40.74]
                        ]]
                    },
                    "relation": "disjoint"
                }
            }
        }
    }), 10).unwrap();

    let names: Vec<String> = results.hits.iter()
        .filter_map(|h| h.source.as_ref()?.get("name")?.as_str().map(String::from))
        .collect();
    assert!(names.contains(&"london".to_string()), "London should be disjoint: {names:?}");
    assert!(names.contains(&"paris".to_string()), "Paris should be disjoint: {names:?}");
    assert!(!names.contains(&"nyc".to_string()), "NYC should NOT be disjoint: {names:?}");

    cleanup(&path);
}

/// geo_shape within: doc shape entirely inside query shape
#[test]
fn geo_shape_within() {
    let (path, mut index) = build_shape_index("within");

    // Large query polygon covering all of Western Europe + Eastern US
    let results = index.search(json!({
        "query": {
            "geo_shape": {
                "boundary": {
                    "shape": {
                        "type": "Polygon",
                        "coordinates": [[
                            [-75.0, 40.0], [-75.0, 52.0], [3.0, 52.0],
                            [3.0, 40.0], [-75.0, 40.0]
                        ]]
                    },
                    "relation": "within"
                }
            }
        }
    }), 10).unwrap();

    let names: Vec<String> = results.hits.iter()
        .filter_map(|h| h.source.as_ref()?.get("name")?.as_str().map(String::from))
        .collect();
    // All shapes should be within this large polygon
    assert!(names.contains(&"nyc".to_string()), "NYC should be within: {names:?}");
    assert!(names.contains(&"london".to_string()), "London should be within: {names:?}");
    assert!(names.contains(&"paris".to_string()), "Paris should be within: {names:?}");
    assert!(names.contains(&"empire_state".to_string()), "Empire State should be within: {names:?}");

    cleanup(&path);
}

/// geo_shape contains: doc shape entirely encloses query shape
#[test]
fn geo_shape_contains() {
    let (path, mut index) = build_shape_index("contains");

    // Tiny query shape inside NYC
    let results = index.search(json!({
        "query": {
            "geo_shape": {
                "boundary": {
                    "shape": {
                        "type": "Point",
                        "coordinates": [-73.98, 40.75]
                    },
                    "relation": "contains"
                }
            }
        }
    }), 10).unwrap();

    let names: Vec<String> = results.hits.iter()
        .filter_map(|h| h.source.as_ref()?.get("name")?.as_str().map(String::from))
        .collect();
    assert!(names.contains(&"nyc".to_string()), "NYC polygon should contain the point: {names:?}");
    assert!(!names.contains(&"london".to_string()), "London should NOT contain it: {names:?}");

    cleanup(&path);
}

/// geo_shape with envelope (bounding box shorthand)
#[test]
fn geo_shape_envelope() {
    let (path, mut index) = build_shape_index("envelope");

    let results = index.search(json!({
        "query": {
            "geo_shape": {
                "boundary": {
                    "shape": {
                        "type": "envelope",
                        "coordinates": [[-74.1, 40.85], [-73.85, 40.65]]
                    },
                    "relation": "intersects"
                }
            }
        }
    }), 10).unwrap();

    let names: Vec<String> = results.hits.iter()
        .filter_map(|h| h.source.as_ref()?.get("name")?.as_str().map(String::from))
        .collect();
    assert!(names.contains(&"nyc".to_string()), "NYC should intersect envelope: {names:?}");
    assert!(names.contains(&"empire_state".to_string()), "Empire State should intersect envelope: {names:?}");

    cleanup(&path);
}

/// Default relation is intersects
#[test]
fn geo_shape_default_relation() {
    let (path, mut index) = build_shape_index("default_relation");

    // No explicit "relation" — should default to intersects
    let results = index.search(json!({
        "query": {
            "geo_shape": {
                "boundary": {
                    "shape": {
                        "type": "Point",
                        "coordinates": [-73.98, 40.75]
                    }
                }
            }
        }
    }), 10).unwrap();

    // The point is inside NYC and coincides with empire_state
    assert!(results.total_hits.value >= 1);

    cleanup(&path);
}
