use std::time::Duration;

use super::*;
use crate::entities::collection::{Collection, Visibility};
use crate::schema::{DecaySpec, EntityKind, SchemaBuilder, Timestamp};
use crate::signals::ledger::types::NoopWalWriter;

fn test_schema() -> crate::schema::Schema {
    let mut builder = SchemaBuilder::new();
    let _ = builder
        .signal(
            "view",
            EntityKind::Item,
            DecaySpec::Exponential {
                half_life: Duration::from_secs(7 * 24 * 3600),
            },
        )
        .windows(&[Window::OneHour, Window::TwentyFourHours, Window::AllTime])
        .velocity(true)
        .add();
    let _ = builder
        .signal(
            "share",
            EntityKind::Item,
            DecaySpec::Exponential {
                half_life: Duration::from_secs(7 * 24 * 3600),
            },
        )
        .windows(&[Window::OneHour, Window::TwentyFourHours, Window::AllTime])
        .velocity(true)
        .add();
    builder.build().expect("valid test schema")
}

fn build_universe(n: u32) -> RoaringBitmap {
    let mut bm = RoaringBitmap::new();
    for i in 1..=n {
        bm.insert(i);
    }
    bm
}

// ── window_to_schema_window ─────────────────────────────────────────

#[test]
fn window_mapping() {
    assert_eq!(window_to_schema_window(1), Window::OneHour);
    assert_eq!(window_to_schema_window(12), Window::TwentyFourHours);
    assert_eq!(window_to_schema_window(24), Window::TwentyFourHours);
    assert_eq!(window_to_schema_window(48), Window::SevenDays);
    assert_eq!(window_to_schema_window(168), Window::SevenDays);
    assert_eq!(window_to_schema_window(200), Window::AllTime);
}

// ── percentile_bitmap ───────────────────────────────────────────────

#[test]
fn percentile_small_n_keeps_all_positive() {
    // With n=4, p25_asc_idx = (4-1)*25/100 = 0, so the cutoff is the
    // minimum velocity. All items with vel > 0 are kept (degenerate case).
    let mut map = HashMap::new();
    map.insert(1, 100);
    map.insert(2, 50);
    map.insert(3, 25);
    map.insert(4, 10);

    let bm = percentile_bitmap(&map);
    assert!(bm.contains(1));
    assert!(bm.contains(2));
    assert!(bm.contains(3));
    assert!(bm.contains(4));
}

#[test]
fn percentile_large_n_keeps_top_75_percent() {
    // With n=100 items with velocities 1..=100, the p25 cutoff in ascending
    // order is at index (99*25/100)=24, velocity=25. Items with vel >= 25
    // (i.e., IDs 24..=99, corresponding to vel 25..=100) should be included.
    // That is 76 items (vel 25 through 100 inclusive).
    let mut map = HashMap::new();
    for i in 0u64..100 {
        map.insert(i, i + 1); // velocities 1..=100, ID == vel - 1
    }
    let bm = percentile_bitmap(&map);
    // p25 cutoff: ascending index 24 → velocity = 25
    // Items with vel >= 25: IDs 24..=99 (76 items)
    assert_eq!(bm.len(), 76, "expected 76 items above p75 cutoff for n=100");
    // Highest-velocity item must be included.
    assert!(bm.contains(99), "ID 99 (vel=100) must be in top 75%");
    // Lowest-velocity item must be excluded.
    assert!(!bm.contains(0), "ID 0 (vel=1) must be below p75 cutoff");
}

#[test]
fn percentile_excludes_zero_velocity() {
    let mut map = HashMap::new();
    map.insert(1, 100);
    map.insert(2, 0);

    let bm = percentile_bitmap(&map);
    assert!(bm.contains(1));
    assert!(!bm.contains(2));
}

// ── resolve_category ────────────────────────────────────────────────

#[test]
fn resolve_category_returns_matching_items() {
    let schema = test_schema();
    let ledger = SignalLedger::new(schema, Box::new(NoopWalWriter));
    let cat_idx = BitmapIndex::new("category");
    cat_idx.insert(1, "music");
    cat_idx.insert(2, "music");
    cat_idx.insert(3, "tech");

    let resolver = ScopeResolver::new(&ledger, build_universe(3)).with_category_index(&cat_idx);

    let scope = WithinScope::Category {
        name: "music".into(),
    };
    let bm = resolver.resolve(&scope, None).unwrap();
    assert!(bm.contains(1));
    assert!(bm.contains(2));
    assert!(!bm.contains(3));
}

#[test]
fn resolve_category_unknown_returns_empty() {
    let schema = test_schema();
    let ledger = SignalLedger::new(schema, Box::new(NoopWalWriter));
    let cat_idx = BitmapIndex::new("category");

    let resolver = ScopeResolver::new(&ledger, build_universe(3)).with_category_index(&cat_idx);

    let scope = WithinScope::Category {
        name: "nonexistent".into(),
    };
    let bm = resolver.resolve(&scope, None).unwrap();
    assert!(bm.is_empty());
}

// ── resolve_collection ──────────────────────────────────────────────

#[test]
fn resolve_collection_returns_items() {
    let schema = test_schema();
    let ledger = SignalLedger::new(schema, Box::new(NoopWalWriter));
    let coll_idx = CollectionIndex::new();
    let cid = coll_idx.next_collection_id();
    coll_idx.create(Collection {
        id: cid,
        owner_id: 1,
        name: "favorites".into(),
        visibility: Visibility::Private,
        created_at_ns: 0,
        item_ids: vec![5, 10, 15],
    });

    let resolver = ScopeResolver::new(&ledger, build_universe(20)).with_collection_index(&coll_idx);

    let scope = WithinScope::Collection { id: cid };
    let bm = resolver.resolve(&scope, None).unwrap();
    assert!(bm.contains(5));
    assert!(bm.contains(10));
    assert!(bm.contains(15));
    assert!(!bm.contains(20));
}

// ── resolve_following ───────────────────────────────────────────────

#[test]
fn resolve_following_requires_for_user() {
    let schema = test_schema();
    let ledger = SignalLedger::new(schema, Box::new(NoopWalWriter));

    let resolver = ScopeResolver::new(&ledger, build_universe(10));
    let result = resolver.resolve(&WithinScope::Following, None);
    assert!(matches!(result, Err(QueryError::InvalidFilter { .. })));
}

#[test]
fn resolve_following_returns_creator_items() {
    let schema = test_schema();
    let ledger = SignalLedger::new(schema, Box::new(NoopWalWriter));
    let user_state = UserStateIndex::new();
    let creator_items = CreatorItemsBitmap::new();

    // User 1 follows creator 100.
    user_state.add_follow(1, 100);
    // Creator 100 has items 10, 20.
    creator_items.add_item(100, 10);
    creator_items.add_item(100, 20);
    // Creator 200 has item 30 (not followed).
    creator_items.add_item(200, 30);

    let resolver = ScopeResolver::new(&ledger, build_universe(30))
        .with_user_context(&user_state, &creator_items);

    let bm = resolver.resolve(&WithinScope::Following, Some(1)).unwrap();
    assert!(bm.contains(10));
    assert!(bm.contains(20));
    assert!(!bm.contains(30));
}

// ── resolve_trending ────────────────────────────────────────────────

#[test]
fn resolve_trending_returns_universe_when_no_signals() {
    let schema = test_schema();
    let ledger = SignalLedger::new(schema, Box::new(NoopWalWriter));
    let universe = build_universe(5);

    let resolver = ScopeResolver::new(&ledger, universe.clone());
    let bm = resolver
        .resolve(&WithinScope::Trending { window_hours: 24 }, None)
        .unwrap();
    assert_eq!(bm, universe);
}

#[test]
fn resolve_trending_filters_by_velocity() {
    let schema = test_schema();
    let ledger = SignalLedger::new(schema, Box::new(NoopWalWriter));
    let ts = Timestamp::now();

    // Item 1: 10 views, Item 2: 5 views, Item 3: 1 view, Item 4: 0 views.
    for _ in 0..10 {
        ledger
            .record_signal("view", crate::schema::EntityId::new(1), 1.0, ts)
            .unwrap();
    }
    for _ in 0..5 {
        ledger
            .record_signal("view", crate::schema::EntityId::new(2), 1.0, ts)
            .unwrap();
    }
    ledger
        .record_signal("view", crate::schema::EntityId::new(3), 1.0, ts)
        .unwrap();

    let resolver = ScopeResolver::new(&ledger, build_universe(4));
    let bm = resolver
        .resolve(&WithinScope::Trending { window_hours: 1 }, None)
        .unwrap();

    // All signaled items should be included (they all have vel > 0).
    assert!(bm.contains(1));
    assert!(bm.contains(2));
    assert!(bm.contains(3));
    // Item 4 has no signals, so it should be excluded.
    assert!(!bm.contains(4));
}

// ── resolve_cohort_trending ─────────────────────────────────────────

#[test]
fn resolve_cohort_trending_unknown_cohort_returns_error() {
    let schema = test_schema();
    let ledger = SignalLedger::new(schema.clone(), Box::new(NoopWalWriter));
    let registry = CohortRegistry::new();
    let cohort_ledger = CohortSignalLedger::new(&schema);

    let resolver =
        ScopeResolver::new(&ledger, build_universe(5)).with_cohort(&cohort_ledger, &registry);

    let scope = WithinScope::CohortTrending {
        cohort: "nonexistent".into(),
        window_hours: 24,
    };
    let result = resolver.resolve(&scope, None);
    assert!(matches!(result, Err(QueryError::NotFound(_))));
}

#[test]
fn resolve_cohort_trending_returns_top_items() {
    let schema = test_schema();
    let ledger = SignalLedger::new(schema.clone(), Box::new(NoopWalWriter));
    let registry = CohortRegistry::new();
    use crate::cohort::{CohortDef, Predicate};
    registry
        .define(CohortDef {
            name: "tech".into(),
            predicate: Predicate::Eq {
                field: "interest".into(),
                value: "tech".into(),
            },
        })
        .unwrap();

    let cohort_ledger = CohortSignalLedger::new(&schema);
    let view_id = cohort_ledger.resolve_signal_type("view").unwrap();
    let ts = Timestamp::now().as_nanos();

    // Record signals in cohort.
    for _ in 0..10 {
        cohort_ledger.record("tech", crate::schema::EntityId::new(1), view_id, 1.0, ts);
    }
    cohort_ledger.record("tech", crate::schema::EntityId::new(2), view_id, 1.0, ts);

    let resolver =
        ScopeResolver::new(&ledger, build_universe(3)).with_cohort(&cohort_ledger, &registry);

    let scope = WithinScope::CohortTrending {
        cohort: "tech".into(),
        window_hours: 1,
    };
    let bm = resolver.resolve(&scope, None).unwrap();
    assert!(bm.contains(1));
    assert!(bm.contains(2));
}
