#[cfg(test)]
mod grid_index_tests {
    use crate::grid_index::GridIndex;

    #[test]
    fn query_bounds_fully_below_grid_returns_empty() {
        let lat = vec![50.0_f32, 50.5, 51.0];
        let lon = vec![0.0_f32, 0.5, 1.0];
        let grid = GridIndex::build(&lat, &lon, 0.01);

        let results = grid.query(10.0, -10.0, 20.0, -5.0);
        assert!(
            results.is_empty(),
            "Should return empty for bounds fully below grid"
        );
    }

    #[test]
    fn query_bounds_fully_above_grid_returns_empty() {
        let lat = vec![50.0_f32, 50.5, 51.0];
        let lon = vec![0.0_f32, 0.5, 1.0];
        let grid = GridIndex::build(&lat, &lon, 0.01);

        let results = grid.query(80.0, 50.0, 90.0, 60.0);
        assert!(
            results.is_empty(),
            "Should return empty for bounds fully above grid"
        );
    }

    #[test]
    fn query_inverted_bounds_returns_empty() {
        let lat = vec![50.0_f32, 50.5, 51.0];
        let lon = vec![0.0_f32, 0.5, 1.0];
        let grid = GridIndex::build(&lat, &lon, 0.01);

        // south > north
        let results = grid.query(52.0, 0.0, 49.0, 1.0);
        assert!(
            results.is_empty(),
            "Should return empty for inverted bounds"
        );
    }

    #[test]
    fn for_each_bounds_fully_outside_yields_nothing() {
        let lat = vec![50.0_f32, 50.5, 51.0];
        let lon = vec![0.0_f32, 0.5, 1.0];
        let grid = GridIndex::build(&lat, &lon, 0.01);

        let mut count = 0;
        grid.for_each_in_bounds(10.0, -10.0, 20.0, -5.0, |_| count += 1);
        assert_eq!(
            count, 0,
            "for_each should yield nothing for out-of-bounds query"
        );
    }

    #[test]
    fn query_with_large_cells_outside_returns_empty() {
        // Previously, out-of-bounds queries with large cell sizes would
        // scan cell (0,0) which could contain data. Now returns empty.
        let lat = vec![50.0_f32];
        let lon = vec![0.0_f32];
        let grid = GridIndex::build(&lat, &lon, 1.0);

        let results = grid.query(0.0, -50.0, 10.0, -40.0);
        assert!(
            results.is_empty(),
            "Should return empty even with large cell size"
        );
    }

    #[test]
    fn query_within_bounds_returns_correct_results() {
        let lat = vec![50.0_f32, 50.5, 51.0];
        let lon = vec![0.0_f32, 0.5, 1.0];
        let grid = GridIndex::build(&lat, &lon, 0.01);

        let results = grid.query(49.9, -0.1, 51.1, 1.1);
        assert_eq!(results.len(), 3, "Should return all 3 points within bounds");
    }

    #[test]
    fn query_partial_bounds_returns_subset() {
        let lat = vec![50.0_f32, 51.0, 52.0];
        let lon = vec![0.0_f32, 0.0, 0.0];
        let grid = GridIndex::build(&lat, &lon, 0.01);

        let results = grid.query(49.9, -0.1, 50.1, 0.1);
        assert_eq!(results.len(), 1, "Should return only the point at lat=50");
    }
}

#[cfg(test)]
mod filter_tests {
    use crate::data::EnumFeatureData;
    use crate::filter::{parse_filters, row_passes_filters};

    #[test]
    fn nan_rows_fail_numeric_filter_even_with_infinite_range() {
        let feature_names = vec!["price".to_string()];
        let feature_data = vec![f32::NAN];
        let enum_features: Vec<EnumFeatureData> = vec![];
        let enum_data: Vec<u8> = vec![];

        let (numeric, enums) =
            parse_filters(Some("price:-inf:inf"), &feature_names, &enum_features);
        assert_eq!(numeric.len(), 1, "Should parse -inf:inf as valid filter");

        let passes = row_passes_filters(0, &numeric, &enums, &feature_data, 1, &enum_data, 0);
        assert!(!passes, "NaN should fail filter even with infinite range");
    }

    #[test]
    fn empty_enum_filter_value_rejects_all() {
        let enum_features = vec![EnumFeatureData {
            name: "rating".to_string(),
            values: vec!["A".to_string(), "B".to_string()],
        }];
        let feature_names: Vec<String> = vec![];
        // Row-major enum data: 1 row, 1 enum, value=0 (index into "A")
        let enum_data: Vec<u8> = vec![0];

        let (numeric, enums) = parse_filters(Some("rating:"), &feature_names, &enum_features);
        assert_eq!(enums.len(), 1);
        assert!(enums[0].allowed.is_empty());

        let passes = row_passes_filters(0, &numeric, &enums, &[], 0, &enum_data, 1);
        assert!(!passes, "Empty allowed set should reject all rows");
    }

    #[test]
    fn enum_filter_with_nonexistent_values_produces_empty_allowed() {
        let enum_features = vec![EnumFeatureData {
            name: "rating".to_string(),
            values: vec!["A".to_string(), "B".to_string()],
        }];
        let feature_names: Vec<String> = vec![];

        let (_, enums) = parse_filters(Some("rating:X|Y|Z"), &feature_names, &enum_features);
        assert_eq!(enums.len(), 1);
        assert!(enums[0].allowed.is_empty());
    }

    #[test]
    fn malformed_numeric_min_is_silently_skipped() {
        let feature_names = vec!["price".to_string()];
        let enum_features: Vec<EnumFeatureData> = vec![];

        let (numeric, enums) = parse_filters(
            Some("price:not_a_number:200"),
            &feature_names,
            &enum_features,
        );
        assert_eq!(numeric.len(), 0);
        assert_eq!(enums.len(), 0);
    }
}

#[cfg(test)]
mod json_tests {
    #[test]
    fn json_escaped_postcode_with_quotes_is_valid() {
        use crate::routes::hexagons::write_json_escaped;

        let mut buf = String::new();
        buf.push_str("{\"postcode\":\"");
        write_json_escaped(&mut buf, "SW1A \"test");
        buf.push_str("\"}");

        let result: Result<serde_json::Value, _> = serde_json::from_str(&buf);
        assert!(
            result.is_ok(),
            "Escaped quote should produce valid JSON: {}",
            buf
        );
        assert_eq!(result.unwrap()["postcode"].as_str().unwrap(), "SW1A \"test");
    }

    #[test]
    fn json_escaped_postcode_with_backslash_is_valid() {
        use crate::routes::hexagons::write_json_escaped;

        let mut buf = String::new();
        buf.push_str("{\"postcode\":\"");
        write_json_escaped(&mut buf, "SW1A\\2AA");
        buf.push_str("\"}");

        let result: Result<serde_json::Value, _> = serde_json::from_str(&buf);
        assert!(
            result.is_ok(),
            "Escaped backslash should produce valid JSON: {}",
            buf
        );
        assert_eq!(result.unwrap()["postcode"].as_str().unwrap(), "SW1A\\2AA");
    }

    #[test]
    fn nan_is_not_valid_json() {
        use std::fmt::Write;
        // Verify that raw NaN in write! is still invalid JSON (documenting the risk
        // that the is_finite() guard in write_hexagons_json protects against).
        let mut buf = String::new();
        write!(buf, "{{\"min_price\":{}}}", f64::NAN).unwrap();

        let result: Result<serde_json::Value, _> = serde_json::from_str(&buf);
        assert!(result.is_err(), "Raw NaN should produce invalid JSON");
    }

    #[test]
    fn infinity_is_not_valid_json() {
        use std::fmt::Write;
        let mut buf = String::new();
        write!(buf, "{{\"min_price\":{}}}", f64::INFINITY).unwrap();

        let result: Result<serde_json::Value, _> = serde_json::from_str(&buf);
        assert!(result.is_err(), "Raw Infinity should produce invalid JSON");
    }
}

#[cfg(test)]
mod enum_encoding_tests {
    #[test]
    fn u8_cast_wraps_around_beyond_255() {
        // Documents the underlying u8 wrapping behavior that the truncation
        // guard in property.rs now prevents.
        let num_values = 300usize;
        let indices: Vec<u8> = (0..num_values).map(|index| index as u8).collect();

        assert_eq!(indices[0], indices[256], "u8 wraps: 0 == 256");
        assert_eq!(indices[1], indices[257], "u8 wraps: 1 == 257");

        use std::collections::HashMap;
        let values: Vec<String> = (0..num_values).map(|i| format!("val_{}", i)).collect();
        let value_to_idx: HashMap<&str, u8> = values
            .iter()
            .enumerate()
            .map(|(index, value)| (value.as_str(), index as u8))
            .collect();

        let unique_indices: std::collections::HashSet<u8> =
            value_to_idx.values().cloned().collect();
        assert!(
            unique_indices.len() < num_values,
            "Without the truncation guard, {} values produce only {} unique u8 indices",
            num_values,
            unique_indices.len()
        );
    }
}
