//! Query execution statistics for operational visibility.
//!
//! `QueryStats` captures per-query timing, candidate counts, and pipeline
//! metrics. Populated unconditionally by both `RetrieveExecutor` and
//! `SearchExecutor` -- not gated behind a feature flag because the overhead
//! of `Instant::now()` on the query path is negligible compared to scoring
//! and diversity enforcement.

/// Per-query execution statistics.
///
/// Populated by the executor pipeline and returned in `Results.stats` and
/// `SearchResults.stats`. All timing values are in microseconds.
///
/// # Invariants (in practice)
///
/// - `candidates_considered >= candidates_after_filter >= candidates_after_diversity`
/// - `total_time_us >= scoring_time_us + diversity_time_us`
/// - `profile_name` matches the profile used for scoring
#[derive(Debug, Clone)]
pub struct QueryStats {
    /// Total candidates entering the pipeline (Stage 1 output).
    pub candidates_considered: usize,
    /// Candidates surviving all filter stages (Stage 2 output).
    pub candidates_after_filter: usize,
    /// Candidates surviving diversity enforcement (Stage 4 output).
    pub candidates_after_diversity: usize,
    /// Number of filter expressions applied.
    pub filters_applied: usize,
    /// Time spent in Stage 3 (signal scoring), in microseconds.
    pub scoring_time_us: u64,
    /// Time spent in Stage 4 (diversity enforcement), in microseconds.
    pub diversity_time_us: u64,
    /// Total wall-clock time for the entire query, in microseconds.
    pub total_time_us: u64,
    /// Degradation level at which this query executed (0 = Full fidelity).
    pub degradation_level: u8,
    /// Name of the ranking profile used for scoring.
    pub profile_name: String,
}

impl QueryStats {
    /// Create a new `QueryStats` with all counters zeroed.
    ///
    /// Only `profile_name` is populated; all numeric fields default to zero.
    #[must_use]
    pub const fn new(profile_name: String) -> Self {
        Self {
            candidates_considered: 0,
            candidates_after_filter: 0,
            candidates_after_diversity: 0,
            filters_applied: 0,
            scoring_time_us: 0,
            diversity_time_us: 0,
            total_time_us: 0,
            degradation_level: 0,
            profile_name,
        }
    }
}

// ── Tests ───────────────────────────────────────────────────────────────────

#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
    use super::*;

    #[test]
    fn query_stats_new_zeroed() {
        let stats = QueryStats::new("trending".to_owned());
        assert_eq!(stats.candidates_considered, 0);
        assert_eq!(stats.candidates_after_filter, 0);
        assert_eq!(stats.candidates_after_diversity, 0);
        assert_eq!(stats.filters_applied, 0);
        assert_eq!(stats.scoring_time_us, 0);
        assert_eq!(stats.diversity_time_us, 0);
        assert_eq!(stats.total_time_us, 0);
        assert_eq!(stats.degradation_level, 0);
        assert_eq!(stats.profile_name, "trending");
    }

    #[test]
    fn query_stats_clone_works() {
        let mut stats = QueryStats::new("hot".to_owned());
        stats.candidates_considered = 1000;
        stats.candidates_after_filter = 500;
        stats.candidates_after_diversity = 20;
        stats.filters_applied = 3;
        stats.scoring_time_us = 1234;
        stats.diversity_time_us = 567;
        stats.total_time_us = 2000;
        stats.degradation_level = 2;

        let cloned = stats.clone();
        assert_eq!(cloned.candidates_considered, 1000);
        assert_eq!(cloned.candidates_after_filter, 500);
        assert_eq!(cloned.candidates_after_diversity, 20);
        assert_eq!(cloned.filters_applied, 3);
        assert_eq!(cloned.scoring_time_us, 1234);
        assert_eq!(cloned.diversity_time_us, 567);
        assert_eq!(cloned.total_time_us, 2000);
        assert_eq!(cloned.degradation_level, 2);
        assert_eq!(cloned.profile_name, "hot");
    }
}
