//! Executor unit tests — part 1.

#![cfg(test)]
#![allow(clippy::unwrap_used)]

use std::sync::RwLock;
use std::time::Duration;

use roaring::RoaringBitmap;

use super::*;
use crate::entities::{CreatorItemsBitmap, HardNegIndex, InteractionLedger, UserStateIndex};
use crate::query::retrieve::{QueryError, Retrieve};
use crate::ranking::builtins::register_builtins;
use crate::ranking::registry::ProfileRegistry;
use crate::schema::{DecaySpec, EntityId, EntityKind, SchemaBuilder, Timestamp, Window};
use crate::signals::{NoopWalWriter, SignalLedger};
use crate::storage::indexes::bitmap::BitmapIndex;
use crate::storage::indexes::filter::FilterExpr;
use crate::storage::indexes::range::RangeIndex;

fn test_schema() -> crate::schema::Schema {
    let mut builder = SchemaBuilder::new();
    for sig in &["view", "like", "share", "skip", "completion"] {
        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 setup_registry() -> ProfileRegistry {
    let mut reg = ProfileRegistry::new();
    register_builtins(&mut reg).unwrap();
    reg
}

/// Add an item to the in-memory indexes and universe bitmap.
#[allow(clippy::too_many_arguments)]
fn add_item(
    category_idx: &BitmapIndex,
    format_idx: &BitmapIndex,
    creator_idx: &BitmapIndex,
    duration_idx: &RangeIndex<u32>,
    created_at_idx: &RangeIndex<u64>,
    universe: &mut RoaringBitmap,
    id: u64,
    category: &str,
    format: &str,
    creator: u64,
) {
    #[allow(clippy::cast_possible_truncation)]
    let id_u32 = id as u32;
    category_idx.insert(id_u32, category);
    format_idx.insert(id_u32, format);
    creator_idx.insert(id_u32, creator.to_string());
    duration_idx.insert(id_u32, 0u32);
    created_at_idx.insert(id_u32, Timestamp::now().as_nanos());
    universe.insert(id_u32);
}

/// Helper: build executor from test indexes.
#[allow(clippy::too_many_arguments)]
fn make_executor<'a>(
    ledger: &'a SignalLedger,
    profile_reg: &'a ProfileRegistry,
    cat: &'a BitmapIndex,
    fmt: &'a BitmapIndex,
    creator: &'a BitmapIndex,
    tag: &'a BitmapIndex,
    dur: &'a RangeIndex<u32>,
    ts: &'a RangeIndex<u64>,
    universe: &'a RwLock<RoaringBitmap>,
) -> RetrieveExecutor<'a> {
    RetrieveExecutor::new(
        ledger,
        profile_reg,
        Some(cat),
        Some(fmt),
        Some(creator),
        Some(tag),
        Some(dur),
        Some(ts),
        Some(universe),
        None, // embedding_registry
    )
}

#[test]
fn empty_database_returns_empty_results() {
    let schema = test_schema();
    let ledger = SignalLedger::new(schema, Box::new(NoopWalWriter));
    let profile_reg = setup_registry();
    let cat = BitmapIndex::new("category");
    let fmt = BitmapIndex::new("format");
    let creator = BitmapIndex::new("creator");
    let tag = BitmapIndex::new("tags");
    let dur: RangeIndex<u32> = RangeIndex::new("duration");
    let ts: RangeIndex<u64> = RangeIndex::new("created_at");
    let universe = RwLock::new(RoaringBitmap::new());

    let exec = make_executor(
        &ledger,
        &profile_reg,
        &cat,
        &fmt,
        &creator,
        &tag,
        &dur,
        &ts,
        &universe,
    );

    let query = Retrieve::builder().profile("new").build().unwrap();
    let results = exec.execute(&query).unwrap();
    assert!(results.items.is_empty());
    assert_eq!(results.total_candidates, 0);
    assert!(results.constraints_satisfied);
}

#[test]
fn scan_returns_items_ranked_by_new() {
    let schema = test_schema();
    let ledger = SignalLedger::new(schema, Box::new(NoopWalWriter));
    let profile_reg = setup_registry();
    let cat = BitmapIndex::new("category");
    let fmt = BitmapIndex::new("format");
    let creator_idx = BitmapIndex::new("creator");
    let tag = BitmapIndex::new("tags");
    let dur: RangeIndex<u32> = RangeIndex::new("duration");
    let ts: RangeIndex<u64> = RangeIndex::new("created_at");
    let mut universe_bm = RoaringBitmap::new();

    for i in 1..=10u64 {
        add_item(
            &cat,
            &fmt,
            &creator_idx,
            &dur,
            &ts,
            &mut universe_bm,
            i,
            "jazz",
            "video",
            1,
        );
    }

    let universe = RwLock::new(universe_bm);
    let exec = make_executor(
        &ledger,
        &profile_reg,
        &cat,
        &fmt,
        &creator_idx,
        &tag,
        &dur,
        &ts,
        &universe,
    );

    let query = Retrieve::builder().profile("new").limit(5).build().unwrap();
    let results = exec.execute(&query).unwrap();
    assert_eq!(results.items.len(), 5);
    assert_eq!(results.total_candidates, 10);
    // "new" sorts by entity_id descending -- highest IDs first.
    assert_eq!(results.items[0].entity_id, EntityId::new(10));
    assert_eq!(results.items[0].rank, 1);
}

#[test]
fn filter_narrows_candidates() {
    let schema = test_schema();
    let ledger = SignalLedger::new(schema, Box::new(NoopWalWriter));
    let profile_reg = setup_registry();
    let cat = BitmapIndex::new("category");
    let fmt = BitmapIndex::new("format");
    let creator_idx = BitmapIndex::new("creator");
    let tag = BitmapIndex::new("tags");
    let dur: RangeIndex<u32> = RangeIndex::new("duration");
    let ts: RangeIndex<u64> = RangeIndex::new("created_at");
    let mut universe_bm = RoaringBitmap::new();

    for i in 1..=10u64 {
        let c = if i <= 5 { "jazz" } else { "blues" };
        add_item(
            &cat,
            &fmt,
            &creator_idx,
            &dur,
            &ts,
            &mut universe_bm,
            i,
            c,
            "video",
            1,
        );
    }

    let universe = RwLock::new(universe_bm);
    let exec = make_executor(
        &ledger,
        &profile_reg,
        &cat,
        &fmt,
        &creator_idx,
        &tag,
        &dur,
        &ts,
        &universe,
    );

    let query = Retrieve::builder()
        .profile("new")
        .filter(FilterExpr::CategoryEq("jazz".into()))
        .build()
        .unwrap();
    let results = exec.execute(&query).unwrap();
    // Only 5 jazz items should survive the filter.
    assert_eq!(results.total_candidates, 5);
    for item in &results.items {
        assert!(item.entity_id.as_u64() <= 5);
    }
}

#[test]
fn exclude_removes_ids() {
    let schema = test_schema();
    let ledger = SignalLedger::new(schema, Box::new(NoopWalWriter));
    let profile_reg = setup_registry();
    let cat = BitmapIndex::new("category");
    let fmt = BitmapIndex::new("format");
    let creator_idx = BitmapIndex::new("creator");
    let tag = BitmapIndex::new("tags");
    let dur: RangeIndex<u32> = RangeIndex::new("duration");
    let ts: RangeIndex<u64> = RangeIndex::new("created_at");
    let mut universe_bm = RoaringBitmap::new();

    for i in 1..=5u64 {
        add_item(
            &cat,
            &fmt,
            &creator_idx,
            &dur,
            &ts,
            &mut universe_bm,
            i,
            "jazz",
            "video",
            1,
        );
    }

    let universe = RwLock::new(universe_bm);
    let exec = make_executor(
        &ledger,
        &profile_reg,
        &cat,
        &fmt,
        &creator_idx,
        &tag,
        &dur,
        &ts,
        &universe,
    );

    let query = Retrieve::builder()
        .profile("new")
        .exclude(vec![EntityId::new(3), EntityId::new(5)])
        .build()
        .unwrap();
    let results = exec.execute(&query).unwrap();
    let ids: Vec<u64> = results.items.iter().map(|r| r.entity_id.as_u64()).collect();
    assert!(!ids.contains(&3));
    assert!(!ids.contains(&5));
}

#[test]
fn pagination_works() {
    let schema = test_schema();
    let ledger = SignalLedger::new(schema, Box::new(NoopWalWriter));
    let profile_reg = setup_registry();
    let cat = BitmapIndex::new("category");
    let fmt = BitmapIndex::new("format");
    let creator_idx = BitmapIndex::new("creator");
    let tag = BitmapIndex::new("tags");
    let dur: RangeIndex<u32> = RangeIndex::new("duration");
    let ts: RangeIndex<u64> = RangeIndex::new("created_at");
    let mut universe_bm = RoaringBitmap::new();

    for i in 1..=10u64 {
        add_item(
            &cat,
            &fmt,
            &creator_idx,
            &dur,
            &ts,
            &mut universe_bm,
            i,
            "jazz",
            "video",
            i,
        );
    }

    let universe = RwLock::new(universe_bm);
    let exec = make_executor(
        &ledger,
        &profile_reg,
        &cat,
        &fmt,
        &creator_idx,
        &tag,
        &dur,
        &ts,
        &universe,
    );

    // Page 1
    let query = Retrieve::builder().profile("new").limit(3).build().unwrap();
    let page1 = exec.execute(&query).unwrap();
    assert_eq!(page1.items.len(), 3);
    assert!(page1.next_cursor.is_some());
    assert_eq!(page1.items[0].rank, 1);
    assert_eq!(page1.items[2].rank, 3);

    // Page 2
    let query = Retrieve::builder()
        .profile("new")
        .limit(3)
        .cursor(page1.next_cursor.unwrap())
        .build()
        .unwrap();
    let page2 = exec.execute(&query).unwrap();
    assert_eq!(page2.items.len(), 3);
    assert_eq!(page2.items[0].rank, 4);
}

#[test]
fn profile_not_found_returns_error() {
    let schema = test_schema();
    let ledger = SignalLedger::new(schema, Box::new(NoopWalWriter));
    let profile_reg = setup_registry();
    let cat = BitmapIndex::new("category");
    let fmt = BitmapIndex::new("format");
    let creator_idx = BitmapIndex::new("creator");
    let tag = BitmapIndex::new("tags");
    let dur: RangeIndex<u32> = RangeIndex::new("duration");
    let ts: RangeIndex<u64> = RangeIndex::new("created_at");
    let universe = RwLock::new(RoaringBitmap::new());

    let exec = make_executor(
        &ledger,
        &profile_reg,
        &cat,
        &fmt,
        &creator_idx,
        &tag,
        &dur,
        &ts,
        &universe,
    );

    let query = Retrieve::builder()
        .profile("nonexistent_profile")
        .build()
        .unwrap();
    let result = exec.execute(&query);
    assert!(matches!(result, Err(QueryError::ProfileNotFound(_))));
}

#[test]
fn saved_filter_retains_only_saved_items() {
    let schema = test_schema();
    let ledger = SignalLedger::new(schema, Box::new(NoopWalWriter));
    let profile_reg = setup_registry();
    let cat = BitmapIndex::new("category");
    let fmt = BitmapIndex::new("format");
    let creator_idx = BitmapIndex::new("creator");
    let tag = BitmapIndex::new("tags");
    let dur: RangeIndex<u32> = RangeIndex::new("duration");
    let ts: RangeIndex<u64> = RangeIndex::new("created_at");
    let mut universe_bm = RoaringBitmap::new();

    // Create 10 items.
    for i in 1..=10u64 {
        add_item(
            &cat,
            &fmt,
            &creator_idx,
            &dur,
            &ts,
            &mut universe_bm,
            i,
            "jazz",
            "video",
            1,
        );
    }

    let universe = RwLock::new(universe_bm);

    // User 42 has saved items 3 and 7.
    let user_state = UserStateIndex::new();
    user_state.add_save(42, 3);
    user_state.add_save(42, 7);

    let hard_neg = HardNegIndex::new();
    let interaction = InteractionLedger::new();
    let creator_items = CreatorItemsBitmap::new();

    let exec = make_executor(
        &ledger,
        &profile_reg,
        &cat,
        &fmt,
        &creator_idx,
        &tag,
        &dur,
        &ts,
        &universe,
    )
    .with_user_context(&user_state, &hard_neg, &interaction, &creator_items);

    // Query with Saved(42) filter and FOR USER 42.
    let query = Retrieve::builder()
        .profile("new")
        .for_user(42)
        .filter(FilterExpr::Saved(42))
        .limit(20)
        .build()
        .unwrap();

    let results = exec.execute(&query).unwrap();
    let ids: Vec<u64> = results.items.iter().map(|r| r.entity_id.as_u64()).collect();
    assert_eq!(ids.len(), 2);
    assert!(ids.contains(&3));
    assert!(ids.contains(&7));
}
