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

use super::test_ledger;
use crate::ranking::executor::ProfileExecutor;
use crate::ranking::profile::{CandidateStrategy, DiversitySpec, RankingProfile, Sort};
use crate::schema::{DecaySpec, EntityId, EntityKind, SchemaBuilder, Timestamp, Window};
use crate::signals::{NoopWalWriter, SignalLedger};

// -- M6p3: New sort variant unit tests ────────────────────────────────

fn make_profile(sort: Sort) -> RankingProfile {
    RankingProfile {
        name: "test".into(),
        version: 1,
        candidate_strategy: CandidateStrategy::Scan {
            sort_field: "created_at".into(),
        },
        boosts: vec![],
        decay: None,
        gates: vec![],
        penalties: vec![],
        excludes: vec![],
        diversity: DiversitySpec::default(),
        exploration: 0.0,
        sort: Some(sort),
        is_builtin: false,
    }
}

#[test]
fn sort_alphabetical_asc_a_before_z() {
    let ledger = test_ledger();
    let mut meta: HashMap<u64, HashMap<String, String>> = HashMap::new();
    meta.insert(1, [("title".into(), "Banana".into())].into());
    meta.insert(2, [("title".into(), "Apple".into())].into());
    meta.insert(3, [("title".into(), "Cherry".into())].into());

    let executor = ProfileExecutor::new(&ledger).with_item_metadata(&meta);
    let candidates: Vec<EntityId> = (1..=3).map(EntityId::new).collect();
    let now = Timestamp::from_nanos(1_708_000_000_000_000_000);
    let profile = make_profile(Sort::AlphabeticalAsc);
    let result = executor.score(&candidates, &profile, now);

    assert_eq!(result[0].entity_id, EntityId::new(2)); // Apple
    assert_eq!(result[1].entity_id, EntityId::new(1)); // Banana
    assert_eq!(result[2].entity_id, EntityId::new(3)); // Cherry
}

#[test]
fn sort_alphabetical_desc_z_before_a() {
    let ledger = test_ledger();
    let mut meta: HashMap<u64, HashMap<String, String>> = HashMap::new();
    meta.insert(1, [("title".into(), "Banana".into())].into());
    meta.insert(2, [("title".into(), "Apple".into())].into());
    meta.insert(3, [("title".into(), "Cherry".into())].into());

    let executor = ProfileExecutor::new(&ledger).with_item_metadata(&meta);
    let candidates: Vec<EntityId> = (1..=3).map(EntityId::new).collect();
    let now = Timestamp::from_nanos(1_708_000_000_000_000_000);
    let profile = make_profile(Sort::AlphabeticalDesc);
    let result = executor.score(&candidates, &profile, now);

    assert_eq!(result[0].entity_id, EntityId::new(3)); // Cherry
    assert_eq!(result[1].entity_id, EntityId::new(1)); // Banana
    assert_eq!(result[2].entity_id, EntityId::new(2)); // Apple
}

#[test]
fn sort_alphabetical_missing_title_last() {
    let ledger = test_ledger();
    let mut meta: HashMap<u64, HashMap<String, String>> = HashMap::new();
    meta.insert(1, [("title".into(), "Banana".into())].into());
    // Entity 2 has no title.
    meta.insert(2, HashMap::new());
    meta.insert(3, [("title".into(), "Apple".into())].into());

    let executor = ProfileExecutor::new(&ledger).with_item_metadata(&meta);
    let candidates: Vec<EntityId> = (1..=3).map(EntityId::new).collect();
    let now = Timestamp::from_nanos(1_708_000_000_000_000_000);
    let profile = make_profile(Sort::AlphabeticalAsc);
    let result = executor.score(&candidates, &profile, now);

    // Entity 2 (no title) should be last.
    assert_eq!(result[2].entity_id, EntityId::new(2));
}

#[test]
fn sort_shortest_first() {
    let ledger = test_ledger();
    let mut meta: HashMap<u64, HashMap<String, String>> = HashMap::new();
    meta.insert(1, [("duration".into(), "120".into())].into());
    meta.insert(2, [("duration".into(), "60".into())].into());
    meta.insert(3, [("duration".into(), "300".into())].into());

    let executor = ProfileExecutor::new(&ledger).with_item_metadata(&meta);
    let candidates: Vec<EntityId> = (1..=3).map(EntityId::new).collect();
    let now = Timestamp::from_nanos(1_708_000_000_000_000_000);
    let profile = make_profile(Sort::Shortest);
    let result = executor.score(&candidates, &profile, now);

    assert_eq!(result[0].entity_id, EntityId::new(2)); // 60s
    assert_eq!(result[1].entity_id, EntityId::new(1)); // 120s
    assert_eq!(result[2].entity_id, EntityId::new(3)); // 300s
}

#[test]
fn sort_longest_first() {
    let ledger = test_ledger();
    let mut meta: HashMap<u64, HashMap<String, String>> = HashMap::new();
    meta.insert(1, [("duration".into(), "120".into())].into());
    meta.insert(2, [("duration".into(), "60".into())].into());
    meta.insert(3, [("duration".into(), "300".into())].into());

    let executor = ProfileExecutor::new(&ledger).with_item_metadata(&meta);
    let candidates: Vec<EntityId> = (1..=3).map(EntityId::new).collect();
    let now = Timestamp::from_nanos(1_708_000_000_000_000_000);
    let profile = make_profile(Sort::Longest);
    let result = executor.score(&candidates, &profile, now);

    assert_eq!(result[0].entity_id, EntityId::new(3)); // 300s
    assert_eq!(result[1].entity_id, EntityId::new(1)); // 120s
    assert_eq!(result[2].entity_id, EntityId::new(2)); // 60s
}

#[test]
fn sort_missing_duration_last() {
    let ledger = test_ledger();
    let mut meta: HashMap<u64, HashMap<String, String>> = HashMap::new();
    meta.insert(1, [("duration".into(), "120".into())].into());
    meta.insert(2, HashMap::new()); // no duration
    meta.insert(3, [("duration".into(), "60".into())].into());

    let executor = ProfileExecutor::new(&ledger).with_item_metadata(&meta);
    let candidates: Vec<EntityId> = (1..=3).map(EntityId::new).collect();
    let now = Timestamp::from_nanos(1_708_000_000_000_000_000);
    let profile = make_profile(Sort::Shortest);
    let result = executor.score(&candidates, &profile, now);

    // Entity 2 (no duration) should be last.
    assert_eq!(result[2].entity_id, EntityId::new(2));
}

#[test]
fn sort_most_commented() {
    // Build a ledger with "comment" signal.
    let mut builder = SchemaBuilder::new();
    for sig in &["view", "share", "like", "comment"] {
        let _ = builder
            .signal(
                sig,
                EntityKind::Item,
                DecaySpec::Exponential {
                    half_life: Duration::from_secs(7 * 24 * 3600),
                },
            )
            .windows(&[
                Window::OneHour,
                Window::TwentyFourHours,
                Window::SevenDays,
                Window::AllTime,
            ])
            .velocity(true)
            .add();
    }
    let schema = builder.build().unwrap();
    let ledger = SignalLedger::new(schema, Box::new(NoopWalWriter));

    let now = Timestamp::now();
    // Entity 1: 3 comments, Entity 2: 1 comment, Entity 3: 5 comments
    for _ in 0..3 {
        ledger
            .record_signal("comment", EntityId::new(1), 1.0, now)
            .unwrap();
    }
    ledger
        .record_signal("comment", EntityId::new(2), 1.0, now)
        .unwrap();
    for _ in 0..5 {
        ledger
            .record_signal("comment", EntityId::new(3), 1.0, now)
            .unwrap();
    }

    let executor = ProfileExecutor::new(&ledger);
    let candidates: Vec<EntityId> = (1..=3).map(EntityId::new).collect();
    // Use AllTime to avoid wall-clock-relative bucket staleness.
    let profile = make_profile(Sort::MostCommented {
        window: Window::AllTime,
    });
    let result = executor.score(&candidates, &profile, now);

    assert_eq!(result[0].entity_id, EntityId::new(3)); // 5 comments
    assert_eq!(result[1].entity_id, EntityId::new(1)); // 3 comments
    assert_eq!(result[2].entity_id, EntityId::new(2)); // 1 comment
}

#[test]
fn sort_most_shared() {
    // Build a dedicated ledger with "share" signal and known event counts.
    let mut builder = SchemaBuilder::new();
    for sig in &["view", "share", "like"] {
        let _ = builder
            .signal(
                sig,
                EntityKind::Item,
                DecaySpec::Exponential {
                    half_life: Duration::from_secs(7 * 24 * 3600),
                },
            )
            .windows(&[Window::OneHour, Window::AllTime])
            .velocity(false)
            .add();
    }
    let schema = builder.build().unwrap();
    let ledger = SignalLedger::new(schema, Box::new(NoopWalWriter));

    let now = Timestamp::now();
    // Entity 1: 1 share, Entity 2: 4 shares, Entity 3: 7 shares
    for _ in 0..1 {
        ledger
            .record_signal("share", EntityId::new(1), 1.0, now)
            .unwrap();
    }
    for _ in 0..4 {
        ledger
            .record_signal("share", EntityId::new(2), 1.0, now)
            .unwrap();
    }
    for _ in 0..7 {
        ledger
            .record_signal("share", EntityId::new(3), 1.0, now)
            .unwrap();
    }

    let executor = ProfileExecutor::new(&ledger);
    let candidates: Vec<EntityId> = (1..=3).map(EntityId::new).collect();
    // Use AllTime to avoid wall-clock-relative bucket staleness.
    let profile = make_profile(Sort::MostShared {
        window: Window::AllTime,
    });
    let result = executor.score(&candidates, &profile, now);

    // Entity 3 has 7 shares (highest). Should be first.
    assert_eq!(result[0].entity_id, EntityId::new(3));
    assert_eq!(result[1].entity_id, EntityId::new(2));
    assert_eq!(result[2].entity_id, EntityId::new(1));
}

#[test]
fn sort_live_viewer_count() {
    let mut builder = SchemaBuilder::new();
    let _ = builder
        .signal(
            "viewer_count",
            EntityKind::Item,
            DecaySpec::Exponential {
                half_life: Duration::from_secs(300),
            },
        )
        .windows(&[Window::AllTime])
        .velocity(false)
        .add();
    // Need at least one more signal for the builder.
    let _ = builder
        .signal(
            "view",
            EntityKind::Item,
            DecaySpec::Exponential {
                half_life: Duration::from_secs(3600),
            },
        )
        .windows(&[Window::OneHour])
        .velocity(false)
        .add();
    let schema = builder.build().unwrap();
    let ledger = SignalLedger::new(schema, Box::new(NoopWalWriter));

    // Use current time so decay scores are still live (5-min half-life).
    let now = Timestamp::now();
    // Entity 1: 10 viewers, Entity 2: 50 viewers, Entity 3: 5 viewers
    for _ in 0..10 {
        ledger
            .record_signal("viewer_count", EntityId::new(1), 1.0, now)
            .unwrap();
    }
    for _ in 0..50 {
        ledger
            .record_signal("viewer_count", EntityId::new(2), 1.0, now)
            .unwrap();
    }
    for _ in 0..5 {
        ledger
            .record_signal("viewer_count", EntityId::new(3), 1.0, now)
            .unwrap();
    }

    let executor = ProfileExecutor::new(&ledger);
    let candidates: Vec<EntityId> = (1..=3).map(EntityId::new).collect();
    let profile = make_profile(Sort::LiveViewerCount);
    let result = executor.score(&candidates, &profile, now);

    assert_eq!(result[0].entity_id, EntityId::new(2)); // 50 viewers
    assert_eq!(result[1].entity_id, EntityId::new(1)); // 10 viewers
    assert_eq!(result[2].entity_id, EntityId::new(3)); // 5 viewers
}

#[test]
fn sort_date_saved_latest_first() {
    let ledger = test_ledger();
    let user_state = crate::entities::UserStateIndex::new();
    user_state.add_save_timestamped(42, 1, 1000);
    user_state.add_save_timestamped(42, 2, 3000);
    user_state.add_save_timestamped(42, 3, 2000);

    let executor = ProfileExecutor::new(&ledger).with_date_saved_context(&user_state, 42);
    let candidates: Vec<EntityId> = (1..=3).map(EntityId::new).collect();
    let now = Timestamp::from_nanos(1_708_000_000_000_000_000);
    let profile = make_profile(Sort::DateSaved);
    let result = executor.score(&candidates, &profile, now);

    assert_eq!(result[0].entity_id, EntityId::new(2)); // saved at 3000 (latest)
    assert_eq!(result[1].entity_id, EntityId::new(3)); // saved at 2000
    assert_eq!(result[2].entity_id, EntityId::new(1)); // saved at 1000
}

#[test]
fn sort_date_saved_no_save_sorted_last() {
    let ledger = test_ledger();
    let user_state = crate::entities::UserStateIndex::new();
    user_state.add_save_timestamped(42, 1, 1000);
    // Entity 2 has no save.
    user_state.add_save_timestamped(42, 3, 2000);

    let executor = ProfileExecutor::new(&ledger).with_date_saved_context(&user_state, 42);
    let candidates: Vec<EntityId> = (1..=3).map(EntityId::new).collect();
    let now = Timestamp::from_nanos(1_708_000_000_000_000_000);
    let profile = make_profile(Sort::DateSaved);
    let result = executor.score(&candidates, &profile, now);

    // Entity 2 (no save timestamp) should be last.
    assert_eq!(result[2].entity_id, EntityId::new(2));
}
