#![allow(clippy::unwrap_used)]
//! M3 User Acceptance Test: Personalized Ranking.
//!
//! Proves the full feedback loop: user entities, relationships, preference
//! vectors, hard negatives, interaction weights, signal dispatch, and
//! personalized RETRIEVE queries with FOR USER clauses work end-to-end.

use std::collections::HashMap;
use std::time::Duration;

use tidaldb::TidalDb;
use tidaldb::entities::RelationshipType;
use tidaldb::query::retrieve::{ProfileRef, RetrieveBuilder};
use tidaldb::schema::{DecaySpec, EntityId, EntityKind, SchemaBuilder, Timestamp, Window};

fn test_schema() -> tidaldb::schema::Schema {
    let mut builder = SchemaBuilder::new();
    for sig in &[
        "view",
        "like",
        "share",
        "skip",
        "completion",
        "dislike",
        "hide",
    ] {
        let _ = builder
            .signal(
                sig,
                EntityKind::Item,
                DecaySpec::Exponential {
                    half_life: Duration::from_secs(7 * 24 * 3600),
                },
            )
            .windows(&[Window::OneHour, Window::TwentyFourHours, Window::SevenDays])
            .velocity(true)
            .add();
    }
    builder.build().unwrap()
}

fn test_db() -> TidalDb {
    TidalDb::builder()
        .ephemeral()
        .with_schema(test_schema())
        .open()
        .unwrap()
}

// ── Test 1: User + Creator CRUD ────────────────────────────────────────────

#[test]
fn user_and_creator_crud() {
    let db = test_db();

    // Write a user.
    let mut user_meta = HashMap::new();
    user_meta.insert("name".to_string(), "alice".to_string());
    db.write_user(EntityId::new(1000), &user_meta).unwrap();

    // Read it back.
    let meta = db.get_user_metadata(EntityId::new(1000)).unwrap().unwrap();
    assert_eq!(meta.get("name").unwrap(), "alice");

    // Write a creator.
    let mut creator_meta = HashMap::new();
    creator_meta.insert("name".to_string(), "bob".to_string());
    db.write_creator(EntityId::new(2000), &creator_meta)
        .unwrap();

    // Read it back.
    let meta = db
        .get_creator_metadata(EntityId::new(2000))
        .unwrap()
        .unwrap();
    assert_eq!(meta.get("name").unwrap(), "bob");

    // Non-existent returns None.
    assert!(db.get_user_metadata(EntityId::new(9999)).unwrap().is_none());
    assert!(
        db.get_creator_metadata(EntityId::new(9999))
            .unwrap()
            .is_none()
    );
}

// ── Test 2: Relationship Graph ─────────────────────────────────────────────

#[test]
fn relationship_graph_crud() {
    let db = test_db();
    let user = EntityId::new(1);
    let creator = EntityId::new(100);
    let ts = Timestamp::now();

    // Write a Follows relationship.
    db.write_relationship(user, RelationshipType::Follows, creator, 1.0, ts)
        .unwrap();

    // List relationships.
    let rels = db
        .list_relationships(user, RelationshipType::Follows)
        .unwrap();
    assert_eq!(rels.len(), 1);
    assert_eq!(rels[0].0, creator);
    assert!((rels[0].1 - 1.0).abs() < f64::EPSILON);

    // Delete relationship.
    db.delete_relationship(user, RelationshipType::Follows, creator)
        .unwrap();
    let rels = db
        .list_relationships(user, RelationshipType::Follows)
        .unwrap();
    assert!(rels.is_empty());
}

// ── Test 3: User State (seen, blocked, saved, liked) ───────────────────────

#[test]
fn user_state_tracking() {
    let db = test_db();
    let us = db.user_state();

    // Seen.
    us.mark_seen(1, 10);
    assert!(us.is_seen(1, 10));
    assert!(!us.is_seen(1, 20));

    // Blocked creator via relationship.
    db.write_relationship(
        EntityId::new(1),
        RelationshipType::Blocks,
        EntityId::new(200),
        1.0,
        Timestamp::now(),
    )
    .unwrap();
    let blocked = us.blocked_creators(1);
    assert!(blocked.contains(&200));

    // Hidden item via relationship.
    db.write_relationship(
        EntityId::new(1),
        RelationshipType::Hide,
        EntityId::new(50),
        1.0,
        Timestamp::now(),
    )
    .unwrap();
    let hidden = us.hidden_items(1);
    assert!(hidden.contains(50));

    // Saved + liked.
    us.add_save(1, 30);
    us.add_like(1, 40);
    assert!(us.is_saved(1, 30));
    assert!(us.is_liked(1, 40));

    // Completion.
    us.record_completion(1, 10, 0.5);
    assert!(us.is_in_progress(1, 10, 0.8));
}

// ── Test 4: Signal Dispatch (side effects) ─────────────────────────────────

#[test]
fn signal_dispatch_records_side_effects() {
    let db = test_db();
    let user_id = 42u64;
    let entity_id = EntityId::new(100);
    let creator_id = 200u64;
    let ts = Timestamp::now();

    // A "view" signal with user context: should mark seen + update interaction.
    db.signal_with_context("view", entity_id, 1.0, ts, Some(user_id), Some(creator_id))
        .unwrap();

    // Check seen tracking.
    assert!(db.user_state().is_seen(user_id, 100));

    // Check interaction ledger.
    let score = db
        .interaction_ledger()
        .score(user_id, creator_id, ts.as_nanos());
    assert!(score > 0.0);

    // A "skip" signal: should record hard negative.
    let item2 = EntityId::new(200);
    db.signal_with_context("skip", item2, 1.0, ts, Some(user_id), Some(creator_id))
        .unwrap();
    assert!(db.hard_negatives().is_negative(user_id, 200));
}

// ── Test 5: Preference Vectors ─────────────────────────────────────────────

#[test]
fn preference_vector_operations() {
    let db = test_db();
    let pv = db.preference_vectors();

    // Initially empty.
    assert!(pv.get(1).is_none());

    // Set initial preference.
    assert!(pv.set(1, vec![1.0; 128]));
    let v = pv.get(1).unwrap();
    assert_eq!(v.len(), 128);

    // Cosine similarity with self should be ~1.0.
    let sim = pv.cosine_similarity(1, &vec![1.0; 128]).unwrap();
    assert!((sim - 1.0).abs() < 1e-5);

    // Wrong dimension rejected.
    assert!(!pv.set(2, vec![1.0; 64]));
}

// ── Test 6: Hard-Negative Exclusion in Query ───────────────────────────────

#[test]
fn hard_negatives_excluded_from_personalized_query() {
    let db = test_db();
    let user_id = 1u64;
    let ts = Timestamp::now();

    // Write 10 items.
    for i in 1..=10u64 {
        let mut meta = HashMap::new();
        meta.insert("category".to_string(), "jazz".to_string());
        meta.insert("format".to_string(), "video".to_string());
        meta.insert("creator_id".to_string(), "100".to_string());
        db.write_item_with_metadata(EntityId::new(i), &meta)
            .unwrap();
    }

    // Record views on all items.
    for i in 1..=10u64 {
        db.signal("view", EntityId::new(i), 1.0, ts).unwrap();
    }

    // Skip items 3 and 7 (hard negatives).
    db.signal_with_context("skip", EntityId::new(3), 1.0, ts, Some(user_id), None)
        .unwrap();
    db.signal_with_context("skip", EntityId::new(7), 1.0, ts, Some(user_id), None)
        .unwrap();

    // Query with FOR USER.
    let query = RetrieveBuilder::new(EntityKind::Item, ProfileRef::new("new"))
        .for_user(user_id)
        .limit(20)
        .build()
        .unwrap();
    let results = db.retrieve(&query).unwrap();

    // Items 3 and 7 should be excluded.
    let ids: Vec<u64> = results.items.iter().map(|r| r.entity_id.as_u64()).collect();
    assert!(!ids.contains(&3), "hard-negative item 3 should be excluded");
    assert!(!ids.contains(&7), "hard-negative item 7 should be excluded");
}

// ── Test 7: Seen Items Excluded from Personalized Query ────────────────────

#[test]
fn seen_items_excluded_from_personalized_query() {
    let db = test_db();
    let user_id = 1u64;
    let ts = Timestamp::now();

    // Write 5 items.
    for i in 1..=5u64 {
        let mut meta = HashMap::new();
        meta.insert("category".to_string(), "blues".to_string());
        meta.insert("format".to_string(), "audio".to_string());
        db.write_item_with_metadata(EntityId::new(i), &meta)
            .unwrap();
        db.signal("view", EntityId::new(i), 1.0, ts).unwrap();
    }

    // Mark items 1, 2, 3 as seen.
    db.user_state().mark_seen(user_id, 1);
    db.user_state().mark_seen(user_id, 2);
    db.user_state().mark_seen(user_id, 3);

    // Query with FOR USER.
    let query = RetrieveBuilder::new(EntityKind::Item, ProfileRef::new("new"))
        .for_user(user_id)
        .limit(20)
        .build()
        .unwrap();
    let results = db.retrieve(&query).unwrap();

    // Only items 4 and 5 should remain.
    let ids: Vec<u64> = results.items.iter().map(|r| r.entity_id.as_u64()).collect();
    assert_eq!(ids.len(), 2);
    assert!(ids.contains(&4));
    assert!(ids.contains(&5));
}

// ── Test 8: Blocked Creator Items Excluded ─────────────────────────────────

#[test]
fn blocked_creator_items_excluded() {
    let db = test_db();
    let user_id = 1u64;
    let ts = Timestamp::now();

    // Write items from two creators.
    for i in 1..=5u64 {
        let mut meta = HashMap::new();
        meta.insert("category".to_string(), "rock".to_string());
        meta.insert("format".to_string(), "video".to_string());
        // Items 1-3 from creator 100, items 4-5 from creator 200.
        let cid = if i <= 3 { "100" } else { "200" };
        meta.insert("creator_id".to_string(), cid.to_string());
        db.write_item_with_metadata(EntityId::new(i), &meta)
            .unwrap();
        db.signal("view", EntityId::new(i), 1.0, ts).unwrap();
    }

    // Block creator 100.
    db.write_relationship(
        EntityId::new(user_id),
        RelationshipType::Blocks,
        EntityId::new(100),
        1.0,
        ts,
    )
    .unwrap();

    // Query with FOR USER.
    let query = RetrieveBuilder::new(EntityKind::Item, ProfileRef::new("new"))
        .for_user(user_id)
        .limit(20)
        .build()
        .unwrap();
    let results = db.retrieve(&query).unwrap();

    // Items 1-3 (from blocked creator 100) should be excluded.
    let ids: Vec<u64> = results.items.iter().map(|r| r.entity_id.as_u64()).collect();
    assert!(
        !ids.contains(&1) && !ids.contains(&2) && !ids.contains(&3),
        "items from blocked creator 100 should be excluded, got: {ids:?}"
    );
    assert_eq!(ids.len(), 2, "only items from creator 200 should remain");
}

// ── Test 9: Personalized Profiles Exist and Work ───────────────────────────

#[test]
fn personalized_profiles_available() {
    let db = test_db();
    let ts = Timestamp::now();

    // Write some items.
    for i in 1..=10u64 {
        let mut meta = HashMap::new();
        meta.insert("category".to_string(), "jazz".to_string());
        meta.insert("format".to_string(), "video".to_string());
        meta.insert("creator_id".to_string(), (i % 3).to_string());
        db.write_item_with_metadata(EntityId::new(i), &meta)
            .unwrap();
        db.signal("view", EntityId::new(i), 1.0, ts).unwrap();
    }

    // Test each personalized profile.
    for profile_name in &["for_you", "following", "related", "notification"] {
        let query = RetrieveBuilder::new(EntityKind::Item, ProfileRef::new(*profile_name))
            .for_user(1)
            .limit(5)
            .build()
            .unwrap();
        let results = db.retrieve(&query);
        assert!(
            results.is_ok(),
            "profile '{profile_name}' should execute without error: {:?}",
            results.err()
        );
    }
}

// ── Test 10: Interaction Ledger Decay ──────────────────────────────────────

#[test]
fn interaction_weight_decays() {
    let db = test_db();
    let il = db.interaction_ledger();
    let base_ns = 1_000_000_000_000u64;

    // Record interaction.
    il.record(1, 100, 10.0, base_ns);

    // Score at write time.
    let score_now = il.score(1, 100, base_ns);
    assert!((score_now - 10.0).abs() < 1e-6);

    // Score after 7 days (one half-life with default 7-day half-life).
    let one_week_later = base_ns + 7 * 24 * 3600 * 1_000_000_000;
    let score_later = il.score(1, 100, one_week_later);
    // Should be approximately half.
    assert!(
        (score_later - 5.0).abs() < 0.5,
        "score after one half-life should be ~5.0, got {score_later}"
    );
}

// ── Test 11: Full Feedback Loop ────────────────────────────────────────────

#[test]
fn full_feedback_loop() {
    let db = test_db();
    let user_id = 42u64;
    let ts = Timestamp::now();

    // 1. Create a user.
    let mut user_meta = HashMap::new();
    user_meta.insert("name".to_string(), "charlie".to_string());
    db.write_user(EntityId::new(user_id), &user_meta).unwrap();

    // 2. Create creators.
    for cid in [100, 200, 300] {
        let mut meta = HashMap::new();
        meta.insert("name".to_string(), format!("creator_{cid}"));
        db.write_creator(EntityId::new(cid), &meta).unwrap();
    }

    // 3. Write items from each creator.
    for i in 1..=9u64 {
        let cid = match i {
            1..=3 => "100",
            4..=6 => "200",
            _ => "300",
        };
        let mut meta = HashMap::new();
        meta.insert("category".to_string(), "jazz".to_string());
        meta.insert("format".to_string(), "video".to_string());
        meta.insert("creator_id".to_string(), cid.to_string());
        db.write_item_with_metadata(EntityId::new(i), &meta)
            .unwrap();
    }

    // 4. User follows creator 100.
    db.write_relationship(
        EntityId::new(user_id),
        RelationshipType::Follows,
        EntityId::new(100),
        1.0,
        ts,
    )
    .unwrap();

    // 5. User views items from creator 100 (high interaction weight).
    //    Use weight=3.0 to ensure creator 100 has the highest interaction score.
    for i in 1..=3u64 {
        db.signal_with_context("view", EntityId::new(i), 3.0, ts, Some(user_id), Some(100))
            .unwrap();
    }

    // 6. Record view signals on items 4-9 (no user context — these should NOT
    //    be marked as "seen" so they remain eligible for personalized queries).
    for i in 4..=9u64 {
        db.signal("view", EntityId::new(i), 1.0, ts).unwrap();
    }
    // Record interaction weights for creators 200 and 300 directly so the
    // interaction ledger is populated for the assertion in step 10.
    for i in 4..=6u64 {
        db.interaction_ledger()
            .record(user_id, 200, 1.0, ts.as_nanos());
        let _ = i; // suppress unused-variable warning
    }
    for i in 7..=9u64 {
        db.interaction_ledger()
            .record(user_id, 300, 1.0, ts.as_nanos());
        let _ = i;
    }

    // 7. User blocks creator 300.
    db.write_relationship(
        EntityId::new(user_id),
        RelationshipType::Blocks,
        EntityId::new(300),
        1.0,
        ts,
    )
    .unwrap();

    // 8. User skips item 5 (hard negative). Pass `None` for creator_id
    //    because a skip should not boost creator interaction weight.
    db.signal_with_context("skip", EntityId::new(5), 1.0, ts, Some(user_id), None)
        .unwrap();

    // 9. Query: FOR USER with new profile.
    //    Expected: items 7,8,9 excluded (blocked creator 300),
    //    item 5 excluded (hard negative), items 1-3 excluded (seen),
    //    only items 4 and 6 remain.
    let query = RetrieveBuilder::new(EntityKind::Item, ProfileRef::new("new"))
        .for_user(user_id)
        .limit(20)
        .build()
        .unwrap();
    let results = db.retrieve(&query).unwrap();

    let ids: Vec<u64> = results.items.iter().map(|r| r.entity_id.as_u64()).collect();

    // Blocked creator 300's items (7,8,9) should be excluded.
    assert!(
        !ids.contains(&7) && !ids.contains(&8) && !ids.contains(&9),
        "blocked creator items should be excluded, got: {ids:?}"
    );
    // Hard negative (item 5) should be excluded.
    assert!(
        !ids.contains(&5),
        "hard-negative item 5 should be excluded, got: {ids:?}"
    );
    // Seen items (1,2,3) should be excluded.
    assert!(
        !ids.contains(&1) && !ids.contains(&2) && !ids.contains(&3),
        "seen items should be excluded, got: {ids:?}"
    );
    // Only items 4 and 6 should remain.
    assert_eq!(
        ids.len(),
        2,
        "expected exactly 2 items (4 and 6), got: {ids:?}"
    );
    assert!(ids.contains(&4) && ids.contains(&6));

    // 10. Verify interaction ledger has creator 100 as top interaction.
    let top = db
        .interaction_ledger()
        .top_creators(user_id, 3, ts.as_nanos());
    assert!(
        !top.is_empty(),
        "interaction ledger should have entries for user"
    );
    // Creator 100 should have highest interaction weight (3 views).
    assert_eq!(
        top[0].0, 100,
        "creator 100 should be top interaction, got: {top:?}"
    );
}
