//! Shared test fixtures for the `ranking` subsystem.
//!
//! Before this module the same `SignalLedger` and `RankingProfile` builders were
//! re-declared verbatim across five ranking test modules (`executor::tests`,
//! `executor::tests::sort_tests`, `executor::helpers::tests`, `profile::tests`,
//! `registry::tests`). The duplication drifted (different window sets, different
//! seed timestamps) and every new sort test re-pasted the 14-field profile
//! literal. These builders are the single home for that boilerplate.
//!
//! Compiled only under `cfg(test)`; never linked into a release build.

use std::time::Duration;

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

/// Fixed base timestamp shared by ledger seeds so windowed reads are
/// deterministic across modules (not wall-clock-relative).
pub const BASE_NS: u64 = 1_708_000_000_000_000_000;

/// A 7-day exponential half-life, the default for ranking-test signals.
const WEEK: Duration = Duration::from_secs(7 * 24 * 3600);

/// The standard window set used by ranking tests: every coarse-grained bucket
/// plus `AllTime`, with velocity enabled.
const STD_WINDOWS: &[Window] = &[
    Window::OneHour,
    Window::TwentyFourHours,
    Window::SevenDays,
    Window::AllTime,
];

/// Build a `SignalLedger` defining each name in `signals` with a 7-day half-life,
/// the standard window set, and velocity enabled. No events are recorded.
///
/// This is the union of the previously-duplicated `ledger_for` / `ledger_with`
/// builders; both used identical schemas.
#[must_use]
pub fn ledger_for(signals: &[&str]) -> SignalLedger {
    let mut builder = SchemaBuilder::new();
    for sig in signals {
        let _ = builder
            .signal(
                sig,
                EntityKind::Item,
                DecaySpec::Exponential { half_life: WEEK },
            )
            .windows(STD_WINDOWS)
            .velocity(true)
            .add();
    }
    let schema = builder.build().expect("ranking test schema must build");
    SignalLedger::new(schema, Box::new(NoopWalWriter))
}

/// The canonical seeded ledger used by the bulk of executor tests: `view`,
/// `share`, and `like` signals, seeded with a small deterministic event spread
/// across five entities (ids 1..=5) anchored at [`BASE_NS`].
///
/// Entity `i` (1-indexed) gets `5 - (i-1)` views, `(i-1) % 3` shares, and
/// `(i-1) % 2` likes, each timestamped one hour earlier than the previous
/// entity. This is the exact seed the old per-module `test_ledger` produced.
#[must_use]
#[allow(clippy::cast_precision_loss)]
pub fn seeded_ledger() -> SignalLedger {
    let ledger = ledger_for(&["view", "share", "like"]);
    for i in 0u64..5 {
        let entity_id = EntityId::new(i + 1);
        let ts = Timestamp::from_nanos(BASE_NS - i * 3_600_000_000_000);
        ledger
            .record_signal("view", entity_id, (5 - i) as f64, ts)
            .expect("seed view");
        ledger
            .record_signal("share", entity_id, (i % 3) as f64, ts)
            .expect("seed share");
        ledger
            .record_signal("like", entity_id, (i % 2) as f64, ts)
            .expect("seed like");
    }
    ledger
}

/// Build a minimal `RankingProfile` skeleton: empty boosts/penalties/excludes/
/// gates, no decay, no sort, version 1, not built-in. This is the 14-field
/// literal every ranking test re-pasted.
#[must_use]
pub fn minimal_profile(name: &str) -> RankingProfile {
    RankingProfile {
        name: name.to_owned(),
        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: None,
        is_builtin: false,
    }
}

/// A minimal profile at an explicit `version` (used by registry version tests).
#[must_use]
pub fn minimal_profile_versioned(name: &str, version: u32) -> RankingProfile {
    let mut p = minimal_profile(name);
    p.version = version;
    p
}

/// A minimal profile with the given [`Sort`] mode set.
#[must_use]
pub fn profile_with_sort(sort: Sort) -> RankingProfile {
    let mut p = minimal_profile("test");
    p.sort = Some(sort);
    p
}

/// A minimal profile mutated by `mutate` -- the ergonomic builder for tests that
/// tweak boosts/penalties/decay/excludes on top of the skeleton.
#[must_use]
pub fn profile_with(name: &str, mut mutate: impl FnMut(&mut RankingProfile)) -> RankingProfile {
    let mut p = minimal_profile(name);
    mutate(&mut p);
    p
}
