//! Milestone 6 Phase 3 Integration Tests: Missing-Metadata + Context Error Cases.
//!
//! Exercises M6P3 edge cases end-to-end through `TidalDb`:
//!
//! 1. Missing title sorted last under alphabetical sort.
//! 2. Missing duration sorted last under duration sort.
//! 3. `DateSaved` sort errors without `FOR USER` context.
//!
//! Sort modes live in `m6p3_sorts.rs`; engagement + geographic filters live in
//! `m6p3_filters.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},
};

// ── 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_duration(
    title: &str,
    category: &str,
    creator_id: u32,
    duration_secs: u32,
) -> HashMap<String, String> {
    let mut m = item_meta(title, category, creator_id);
    m.insert("duration".to_string(), duration_secs.to_string());
    m
}

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

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

    // Item 1 has a title; item 2 has no title; item 3 has a title.
    db.write_item_with_metadata(EntityId::new(1), &item_meta("Beta", "music", 100))
        .unwrap();
    let mut no_title = HashMap::new();
    no_title.insert("category".to_string(), "music".to_string());
    no_title.insert("creator_id".to_string(), "100".to_string());
    db.write_item_with_metadata(EntityId::new(2), &no_title)
        .unwrap();
    db.write_item_with_metadata(EntityId::new(3), &item_meta("Alpha", "music", 100))
        .unwrap();

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

    let query = Retrieve::builder()
        .profile("alphabetical_asc")
        .limit(10)
        .build()
        .unwrap();
    let results = db.retrieve(&query).unwrap();

    assert!(results.items.len() >= 3);
    // Alpha, Beta, then no-title (sorted last)
    assert_eq!(results.items[0].entity_id, EntityId::new(3)); // Alpha
    assert_eq!(results.items[1].entity_id, EntityId::new(1)); // Beta
    assert_eq!(results.items[2].entity_id, EntityId::new(2)); // no title

    db.close().unwrap();
}

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

    db.write_item_with_metadata(
        EntityId::new(1),
        &item_meta_with_duration("Short", "music", 100, 60),
    )
    .unwrap();
    // Item 2: no duration
    db.write_item_with_metadata(EntityId::new(2), &item_meta("NoDuration", "music", 100))
        .unwrap();
    db.write_item_with_metadata(
        EntityId::new(3),
        &item_meta_with_duration("Long", "music", 100, 3600),
    )
    .unwrap();

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

    let query = Retrieve::builder()
        .profile("shortest")
        .limit(10)
        .build()
        .unwrap();
    let results = db.retrieve(&query).unwrap();

    assert!(results.items.len() >= 3);
    // No-duration item should be last.
    assert_eq!(results.items[2].entity_id, EntityId::new(2));

    db.close().unwrap();
}

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

    db.write_item_with_metadata(EntityId::new(1), &item_meta("Item", "music", 100))
        .unwrap();
    db.signal("view", EntityId::new(1), 1.0, Timestamp::now())
        .unwrap();

    // Query with DateSaved but NO for_user -> should error.
    let query = Retrieve::builder()
        .profile("date_saved")
        .limit(10)
        .build()
        .unwrap();
    let result = db.retrieve(&query);
    assert!(
        result.is_err(),
        "DateSaved sort without FOR USER should fail"
    );

    db.close().unwrap();
}
