use super::*;
use crate::ranking::profile::{CandidateStrategy, DiversitySpec, RankingProfile};
use crate::ranking::registry::ProfileRegistry;
use base64::Engine as _;

// ── ProfileRef ──────────────────────────────────────────────────────

#[test]
fn profile_ref_latest() {
    let pr = ProfileRef::new("trending");
    assert_eq!(pr.name, "trending");
    assert!(pr.version.is_none());
}

#[test]
fn profile_ref_versioned() {
    let pr = ProfileRef::versioned("trending", 3);
    assert_eq!(pr.name, "trending");
    assert_eq!(pr.version, Some(3));
}

// ── RetrieveBuilder ─────────────────────────────────────────────────

#[test]
fn builder_defaults() {
    let q = RetrieveBuilder::new(EntityKind::Item, ProfileRef::new("trending"))
        .build()
        .unwrap();
    assert_eq!(q.limit, 20);
    assert!(q.filters.is_empty());
    assert!(q.diversity.is_none());
    assert!(q.cursor.is_none());
    assert!(q.exclude.is_empty());
    assert!(q.for_user.is_none());
    assert!(q.similar_to.is_none());
    assert!(q.context.is_none());
}

#[test]
fn builder_limit_zero_rejected() {
    let result = RetrieveBuilder::new(EntityKind::Item, ProfileRef::new("trending"))
        .limit(0)
        .build();
    assert!(matches!(result, Err(QueryError::InvalidLimit { .. })));
}

#[test]
fn builder_limit_501_rejected() {
    let result = RetrieveBuilder::new(EntityKind::Item, ProfileRef::new("trending"))
        .limit(501)
        .build();
    assert!(matches!(result, Err(QueryError::InvalidLimit { .. })));
}

#[test]
fn builder_limit_500_accepted() {
    let q = RetrieveBuilder::new(EntityKind::Item, ProfileRef::new("trending"))
        .limit(500)
        .build()
        .unwrap();
    assert_eq!(q.limit, 500);
}

#[test]
fn builder_limit_1_accepted() {
    let q = RetrieveBuilder::new(EntityKind::Item, ProfileRef::new("trending"))
        .limit(1)
        .build()
        .unwrap();
    assert_eq!(q.limit, 1);
}

#[test]
fn builder_with_filter() {
    let q = RetrieveBuilder::new(EntityKind::Item, ProfileRef::new("trending"))
        .filter(FilterExpr::eq("category", "jazz"))
        .build()
        .unwrap();
    assert_eq!(q.filters.len(), 1);
}

#[test]
fn builder_with_diversity() {
    let q = RetrieveBuilder::new(EntityKind::Item, ProfileRef::new("trending"))
        .diversity(DiversityConstraints::new().max_per_creator(3))
        .build()
        .unwrap();
    assert!(q.diversity.is_some());
}

#[test]
fn builder_with_exclude() {
    let q = RetrieveBuilder::new(EntityKind::Item, ProfileRef::new("trending"))
        .exclude(vec![EntityId::new(1), EntityId::new(2)])
        .build()
        .unwrap();
    assert_eq!(q.exclude.len(), 2);
}

#[test]
fn builder_with_cursor() {
    let cursor = Cursor::new(0.95, EntityId::new(42));
    let q = RetrieveBuilder::new(EntityKind::Item, ProfileRef::new("trending"))
        .cursor(cursor)
        .build()
        .unwrap();
    assert!(q.cursor.is_some());
}

#[test]
fn builder_with_for_user() {
    let q = RetrieveBuilder::new(EntityKind::Item, ProfileRef::new("trending"))
        .for_user(99)
        .build()
        .unwrap();
    assert_eq!(q.for_user, Some(99));
}

#[test]
fn builder_with_similar_to() {
    let q = RetrieveBuilder::new(EntityKind::Item, ProfileRef::new("trending"))
        .similar_to(EntityId::new(7))
        .build()
        .unwrap();
    assert_eq!(q.similar_to, Some(EntityId::new(7)));
}

#[test]
fn builder_with_context() {
    let q = RetrieveBuilder::new(EntityKind::Item, ProfileRef::new("trending"))
        .context("jazz improvisation")
        .build()
        .unwrap();
    assert_eq!(q.context.as_deref(), Some("jazz improvisation"));
}

// ── combined_filter ─────────────────────────────────────────────────

#[test]
fn combined_filter_empty() {
    let q = RetrieveBuilder::new(EntityKind::Item, ProfileRef::new("trending"))
        .build()
        .unwrap();
    assert!(q.combined_filter().is_none());
}

#[test]
fn combined_filter_single() {
    let q = RetrieveBuilder::new(EntityKind::Item, ProfileRef::new("trending"))
        .filter(FilterExpr::eq("category", "jazz"))
        .build()
        .unwrap();
    let combined = q.combined_filter().unwrap();
    assert!(matches!(combined, FilterExpr::CategoryEq(_)));
}

#[test]
fn combined_filter_multiple() {
    let q = RetrieveBuilder::new(EntityKind::Item, ProfileRef::new("trending"))
        .filter(FilterExpr::eq("category", "jazz"))
        .filter(FilterExpr::eq("format", "video"))
        .build()
        .unwrap();
    let combined = q.combined_filter().unwrap();
    assert!(matches!(combined, FilterExpr::And(_)));
}

// ── Cursor ──────────────────────────────────────────────────────────

#[test]
fn cursor_roundtrip() {
    let cursor = Cursor::new(0.95, EntityId::new(42));
    let encoded = cursor.encode();
    let decoded = Cursor::decode(&encoded).unwrap();
    assert_eq!(decoded, cursor);
}

#[test]
fn cursor_decode_invalid_base64() {
    let result = Cursor::decode("!!!not-base64!!!");
    assert!(matches!(result, Err(QueryError::InvalidCursor(_))));
}

#[test]
fn cursor_decode_wrong_length() {
    let short = base64::engine::general_purpose::URL_SAFE_NO_PAD.encode([0u8; 8]);
    let result = Cursor::decode(&short);
    assert!(matches!(result, Err(QueryError::InvalidCursor(_))));
}

#[test]
fn cursor_display_matches_encode() {
    let cursor = Cursor::new(0.5, EntityId::new(100));
    assert_eq!(cursor.to_string(), cursor.encode());
}

// ── Signal ──────────────────────────────────────────────────────────

#[test]
fn signal_construction() {
    let sig = Signal {
        name: "view".into(),
        value: 42.0,
        source: "decay_score".into(),
    };
    assert_eq!(sig.name, "view");
    assert!((sig.value - 42.0).abs() < f64::EPSILON);
    assert_eq!(sig.source, "decay_score");
}

// ── Results ─────────────────────────────────────────────────────────

#[test]
fn results_len_and_empty() {
    let results = Results {
        items: vec![],
        next_cursor: None,
        total_candidates: 0,
        constraints_satisfied: true,
        warnings: vec![],
        session_snapshot: None,
        degradation_level: crate::load::DegradationLevel::Full,
        stats: QueryStats::new("test".to_owned()),
    };
    assert_eq!(results.len(), 0);
    assert!(results.is_empty());

    let results_with_items = Results {
        items: vec![RetrieveResult {
            entity_id: EntityId::new(1),
            score: 0.9,
            rank: 1,
            signals: vec![],
        }],
        next_cursor: None,
        total_candidates: 1,
        constraints_satisfied: true,
        warnings: vec![],
        session_snapshot: None,
        degradation_level: crate::load::DegradationLevel::Full,
        stats: QueryStats::new("test".to_owned()),
    };
    assert_eq!(results_with_items.len(), 1);
    assert!(!results_with_items.is_empty());
}

// ── validate ────────────────────────────────────────────────────────

fn make_registry_with_profile(name: &str, strategy: CandidateStrategy) -> ProfileRegistry {
    let mut registry = ProfileRegistry::new();
    let profile = RankingProfile {
        name: name.to_owned(),
        version: 1,
        candidate_strategy: strategy,
        boosts: vec![],
        decay: None,
        gates: vec![],
        penalties: vec![],
        excludes: vec![],
        diversity: DiversitySpec::default(),
        exploration: 0.0,
        sort: None,
        is_builtin: false,
    };
    registry.register(profile).unwrap();
    registry
}

#[test]
fn validate_profile_not_found() {
    let registry = ProfileRegistry::new();
    let q = RetrieveBuilder::new(EntityKind::Item, ProfileRef::new("nonexistent"))
        .build()
        .unwrap();
    let result = q.validate(&registry);
    assert!(matches!(result, Err(QueryError::ProfileNotFound(_))));
}

#[test]
fn validate_limit_zero() {
    let registry = make_registry_with_profile(
        "test",
        CandidateStrategy::Scan {
            sort_field: "created_at".into(),
        },
    );
    // Build a Retrieve directly (bypassing builder validation) to test validate().
    let q = Retrieve {
        entity_kind: EntityKind::Item,
        profile: ProfileRef::new("test"),
        filters: vec![],
        diversity: None,
        limit: 0,
        exclude: vec![],
        cursor: None,
        for_user: None,
        similar_to: None,
        context: None,
        for_session: None,
        cohort: None,
        cohort_predicate: None,
        notification_caps: None,
        for_creator: None,
    };
    let result = q.validate(&registry);
    assert!(matches!(result, Err(QueryError::InvalidLimit { .. })));
}

#[test]
fn validate_for_user_accepted() {
    // M3: FOR USER clause is now supported.
    let registry = make_registry_with_profile(
        "test",
        CandidateStrategy::Scan {
            sort_field: "created_at".into(),
        },
    );
    let q = Retrieve {
        entity_kind: EntityKind::Item,
        profile: ProfileRef::new("test"),
        filters: vec![],
        diversity: None,
        limit: 20,
        exclude: vec![],
        cursor: None,
        for_user: Some(42),
        similar_to: None,
        context: None,
        for_session: None,
        cohort: None,
        cohort_predicate: None,
        notification_caps: None,
        for_creator: None,
    };
    assert!(q.validate(&registry).is_ok());
}

#[test]
fn validate_similar_to_accepted() {
    // M3: SIMILAR TO clause is now supported.
    let registry = make_registry_with_profile(
        "test",
        CandidateStrategy::Scan {
            sort_field: "created_at".into(),
        },
    );
    let q = Retrieve {
        entity_kind: EntityKind::Item,
        profile: ProfileRef::new("test"),
        filters: vec![],
        diversity: None,
        limit: 20,
        exclude: vec![],
        cursor: None,
        for_user: None,
        similar_to: Some(EntityId::new(7)),
        context: None,
        for_session: None,
        cohort: None,
        cohort_predicate: None,
        notification_caps: None,
        for_creator: None,
    };
    assert!(q.validate(&registry).is_ok());
}

#[test]
fn validate_unsupported_strategy() {
    let registry = make_registry_with_profile("hybrid_test", CandidateStrategy::Hybrid);
    let q = RetrieveBuilder::new(EntityKind::Item, ProfileRef::new("hybrid_test"))
        .build()
        .unwrap();
    let result = q.validate(&registry);
    assert!(matches!(result, Err(QueryError::UnsupportedStrategy(_))));
}

#[test]
fn validate_scan_strategy_accepted() {
    let registry = make_registry_with_profile(
        "scan_test",
        CandidateStrategy::Scan {
            sort_field: "created_at".into(),
        },
    );
    let q = RetrieveBuilder::new(EntityKind::Item, ProfileRef::new("scan_test"))
        .build()
        .unwrap();
    assert!(q.validate(&registry).is_ok());
}

#[test]
fn validate_ann_strategy_accepted() {
    let registry = make_registry_with_profile(
        "ann_test",
        CandidateStrategy::Ann {
            slot: "default".into(),
            limit: 100,
        },
    );
    let q = RetrieveBuilder::new(EntityKind::Item, ProfileRef::new("ann_test"))
        .build()
        .unwrap();
    assert!(q.validate(&registry).is_ok());
}

#[test]
fn validate_versioned_profile() {
    let registry = make_registry_with_profile(
        "versioned",
        CandidateStrategy::Scan {
            sort_field: "created_at".into(),
        },
    );
    let q = RetrieveBuilder::new(EntityKind::Item, ProfileRef::versioned("versioned", 1))
        .build()
        .unwrap();
    assert!(q.validate(&registry).is_ok());
}

#[test]
fn validate_versioned_profile_wrong_version() {
    let registry = make_registry_with_profile(
        "versioned",
        CandidateStrategy::Scan {
            sort_field: "created_at".into(),
        },
    );
    let q = RetrieveBuilder::new(EntityKind::Item, ProfileRef::versioned("versioned", 99))
        .build()
        .unwrap();
    let result = q.validate(&registry);
    assert!(matches!(result, Err(QueryError::ProfileNotFound(_))));
}

// ── Property tests ──────────────────────────────────────────────────

mod proptests {
    use super::*;
    use proptest::prelude::*;

    proptest! {
        #[test]
        fn cursor_roundtrip_prop(
            score in proptest::num::f64::NORMAL,
            id in 0u64..u64::MAX,
        ) {
            let cursor = Cursor::new(score, EntityId::new(id));
            let encoded = cursor.encode();
            let decoded = Cursor::decode(&encoded).unwrap();
            prop_assert_eq!(decoded.last_id, cursor.last_id);
            // f64 roundtrip through be_bytes is exact.
            prop_assert!((decoded.last_score - cursor.last_score).abs() < f64::EPSILON
                || (decoded.last_score.is_nan() && cursor.last_score.is_nan()));
        }

        #[test]
        fn builder_limit_in_range_always_succeeds(limit in 1usize..=500) {
            let result = RetrieveBuilder::new(
                EntityKind::Item,
                ProfileRef::new("test"),
            )
            .limit(limit)
            .build();
            prop_assert!(result.is_ok());
            prop_assert_eq!(result.unwrap().limit, limit);
        }

        #[test]
        fn builder_limit_out_of_range_always_fails(limit in prop::sample::select(vec![0usize, 501, 1000, usize::MAX])) {
            let result = RetrieveBuilder::new(
                EntityKind::Item,
                ProfileRef::new("test"),
            )
            .limit(limit)
            .build();
            let is_invalid_limit = matches!(result, Err(QueryError::InvalidLimit { .. }));
            prop_assert!(is_invalid_limit);
        }
    }
}
