//! Milestone 6 Phase 3 Integration Tests: Engagement + Geographic Filters.
//!
//! Exercises the M6P3 filter feature set end-to-end through `TidalDb`:
//!
//! 1. `MinSignal` engagement threshold filter.
//! 2. `MaxSignal` engagement threshold filter.
//! 3. `NearLocation` geographic post-filter.
//!
//! Sort modes live in `m6p3_sorts.rs`; missing-metadata and context error
//! cases live in `m6p3_edge_cases.rs`.

#![allow(
    clippy::too_many_lines,
    clippy::unwrap_used,
    clippy::cast_precision_loss
)]

use std::{collections::HashMap, time::Duration};

use tidaldb::{
    TidalDb,
    query::retrieve::Retrieve,
    schema::{DecaySpec, EntityId, EntityKind, SchemaBuilder, Timestamp, Window},
    storage::indexes::filter::FilterExpr,
};

// ── Schema ──────────────────────────────────────────────────────────────────

fn m6p3_schema_without_viewer_count() -> tidaldb::schema::Schema {
    let mut builder = SchemaBuilder::new();
    for &(name, half_life_days) in &[
        ("view", 7),
        ("like", 14),
        ("share", 7),
        ("comment", 7),
        ("skip", 1),
        ("hide", 1),
    ] {
        let _ = builder
            .signal(
                name,
                EntityKind::Item,
                DecaySpec::Exponential {
                    half_life: Duration::from_secs(half_life_days * 24 * 3600),
                },
            )
            .windows(&[
                Window::OneHour,
                Window::TwentyFourHours,
                Window::SevenDays,
                Window::AllTime,
            ])
            .velocity(true)
            .add();
    }
    builder.build().expect("schema must be valid")
}

// ── Helpers ─────────────────────────────────────────────────────────────────

fn item_meta(title: &str, category: &str, creator_id: u32) -> HashMap<String, String> {
    let mut m = HashMap::new();
    m.insert("title".to_string(), title.to_string());
    m.insert("category".to_string(), category.to_string());
    m.insert("creator_id".to_string(), creator_id.to_string());
    m
}

fn item_meta_with_location(
    title: &str,
    category: &str,
    creator_id: u32,
    lat: f64,
    lng: f64,
) -> HashMap<String, String> {
    let mut m = item_meta(title, category, creator_id);
    m.insert("latitude".to_string(), lat.to_string());
    m.insert("longitude".to_string(), lng.to_string());
    m
}

// ── Tests ───────────────────────────────────────────────────────────────────

#[test]
fn min_signal_filter() {
    let schema = m6p3_schema_without_viewer_count();
    let db = TidalDb::builder()
        .ephemeral()
        .with_schema(schema)
        .open()
        .unwrap();

    for id in 1u64..=3 {
        db.write_item_with_metadata(EntityId::new(id), &item_meta("Item", "music", 100))
            .unwrap();
    }

    let ts = Timestamp::now();
    // Entity 1: 10 views, Entity 2: 2 views, Entity 3: 5 views.
    for _ in 0..10 {
        db.signal("view", EntityId::new(1), 1.0, ts).unwrap();
    }
    for _ in 0..2 {
        db.signal("view", EntityId::new(2), 1.0, ts).unwrap();
    }
    for _ in 0..5 {
        db.signal("view", EntityId::new(3), 1.0, ts).unwrap();
    }

    // Filter: view >= 5 (should exclude entity 2)
    let query = Retrieve::builder()
        .profile("new")
        .filter(FilterExpr::min_signal("view", 5.0))
        .limit(10)
        .build()
        .unwrap();
    let results = db.retrieve(&query).unwrap();

    let result_ids: Vec<u64> = results.items.iter().map(|r| r.entity_id.as_u64()).collect();
    assert!(
        !result_ids.contains(&2),
        "entity 2 (2 views) should be excluded by MinSignal(5.0), got: {result_ids:?}"
    );
    assert!(
        result_ids.contains(&1),
        "entity 1 (10 views) should pass MinSignal(5.0)"
    );
    assert!(
        result_ids.contains(&3),
        "entity 3 (5 views) should pass MinSignal(5.0)"
    );

    db.close().unwrap();
}

#[test]
fn max_signal_filter() {
    let schema = m6p3_schema_without_viewer_count();
    let db = TidalDb::builder()
        .ephemeral()
        .with_schema(schema)
        .open()
        .unwrap();

    for id in 1u64..=3 {
        db.write_item_with_metadata(EntityId::new(id), &item_meta("Item", "music", 100))
            .unwrap();
    }

    let ts = Timestamp::now();
    // Entity 1: 1 hide, Entity 2: 10 hides, Entity 3: 3 hides.
    db.signal("hide", EntityId::new(1), 1.0, ts).unwrap();
    for _ in 0..10 {
        db.signal("hide", EntityId::new(2), 1.0, ts).unwrap();
    }
    for _ in 0..3 {
        db.signal("hide", EntityId::new(3), 1.0, ts).unwrap();
    }
    // Also give them views so they appear as candidates.
    for id in 1u64..=3 {
        db.signal("view", EntityId::new(id), 1.0, ts).unwrap();
    }

    // Filter: hide <= 5 (should exclude entity 2)
    let query = Retrieve::builder()
        .profile("new")
        .filter(FilterExpr::max_signal("hide", 5.0))
        .limit(10)
        .build()
        .unwrap();
    let results = db.retrieve(&query).unwrap();

    let result_ids: Vec<u64> = results.items.iter().map(|r| r.entity_id.as_u64()).collect();
    assert!(
        !result_ids.contains(&2),
        "entity 2 (10 hides) should be excluded by MaxSignal(5.0), got: {result_ids:?}"
    );
    assert!(
        result_ids.contains(&1),
        "entity 1 (1 hide) should pass MaxSignal(5.0)"
    );
    assert!(
        result_ids.contains(&3),
        "entity 3 (3 hides) should pass MaxSignal(5.0)"
    );

    db.close().unwrap();
}

#[test]
fn near_location_filter() {
    let schema = m6p3_schema_without_viewer_count();
    let db = TidalDb::builder()
        .ephemeral()
        .with_schema(schema)
        .open()
        .unwrap();

    // NYC (40.7128, -74.0060)
    db.write_item_with_metadata(
        EntityId::new(1),
        &item_meta_with_location("NYC Spot", "food", 100, 40.7128, -74.0060),
    )
    .unwrap();
    // Brooklyn (40.6782, -73.9442) -- ~5 km from NYC center
    db.write_item_with_metadata(
        EntityId::new(2),
        &item_meta_with_location("Brooklyn Spot", "food", 100, 40.6782, -73.9442),
    )
    .unwrap();
    // Los Angeles (34.0522, -118.2437) -- ~3940 km from NYC
    db.write_item_with_metadata(
        EntityId::new(3),
        &item_meta_with_location("LA Spot", "food", 100, 34.0522, -118.2437),
    )
    .unwrap();

    let ts = Timestamp::now();
    for id in 1u64..=3 {
        db.signal("view", EntityId::new(id), 1.0, ts).unwrap();
    }

    // Filter: within 10 km of NYC center (should include NYC and Brooklyn, exclude LA).
    let query = Retrieve::builder()
        .profile("new")
        .filter(FilterExpr::near_location(40.7128, -74.0060, 10.0))
        .limit(10)
        .build()
        .unwrap();
    let results = db.retrieve(&query).unwrap();

    let result_ids: Vec<u64> = results.items.iter().map(|r| r.entity_id.as_u64()).collect();
    assert!(
        result_ids.contains(&1),
        "NYC Spot should be within 10 km of NYC center"
    );
    assert!(
        result_ids.contains(&2),
        "Brooklyn Spot should be within 10 km of NYC center"
    );
    assert!(
        !result_ids.contains(&3),
        "LA Spot should NOT be within 10 km of NYC center, got: {result_ids:?}"
    );

    db.close().unwrap();
}
