//! Metabolic energy expenditure — mechanical work in, kilocalories out.
//!
//! One model, used in two places: the live readout on the ride screen
//! (`src-tauri/src/derive.rs`) and the `total_calories` written into the FIT
//! activity (`bikecontrol-fit`). They must agree, so the arithmetic lives here
//! rather than being written twice.
//!
//! **The model.** A rider burns metabolic energy in two ways during a ride:
//!
//! * *Work.* Mechanical work measured at the pedals, divided by the rider's
//!   efficiency at converting food energy into it. Cycling net efficiency sits
//!   around 20–25%; [`NET_EFFICIENCY`] takes the top of that range because the
//!   figure most riders compare against (Strava, Garmin) is effectively the
//!   1 kJ ≈ 1 kcal convention, which corresponds to ~24%.
//! * *Rest.* Being alive costs roughly one kilocalorie per kilogram per hour —
//!   the definition of 1 MET. Over a two-hour ride that is another ~150 kcal
//!   for an 80 kg rider, so it is worth counting rather than rounding away.
//!
//! Splitting the two is why this uses *net* efficiency (work above baseline)
//! and not *gross* efficiency (which already has the resting cost folded in) —
//! using gross efficiency and then adding rest back would count it twice.
//!
//! **What this is not.** It is an estimate, not a measurement. Real efficiency
//! varies by rider, cadence and intensity, and no power meter can see the
//! difference. Treat a figure from here as ±10%.

/// Joules in one dietary kilocalorie.
pub const JOULES_PER_KCAL: f64 = 4184.0;

/// Fraction of the metabolic energy spent *above resting* that reaches the
/// pedals as mechanical work.
pub const NET_EFFICIENCY: f64 = 0.25;

/// Resting metabolic rate, kcal per kilogram of body mass per hour. This is
/// 1 MET, the standard baseline.
pub const RESTING_KCAL_PER_KG_HOUR: f64 = 1.0;

/// Kilocalories burned by `work_j` joules of pedalling spread over `active_s`
/// seconds, by a rider of `rider_kg`.
///
/// `active_s` should be time the rider was actually riding — a paused ride
/// still burns calories, but they are not this ride's to claim. Passing a
/// `rider_kg` of zero (an unknown rider) drops the resting term and leaves the
/// work term intact, which is the right degradation: an underestimate rather
/// than a fabricated one.
pub fn kcal(work_j: f64, rider_kg: f32, active_s: f64) -> f64 {
    let from_work = work_j.max(0.0) / JOULES_PER_KCAL / NET_EFFICIENCY;
    let from_rest =
        f64::from(rider_kg.max(0.0)) * RESTING_KCAL_PER_KG_HOUR * active_s.max(0.0) / 3600.0;
    from_work + from_rest
}

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

    /// The sanity check every cyclist knows: an hour at 250 W — 900 kJ of work
    /// — costs somewhere close to a thousand kilocalories. Anything far from
    /// that means the constants are wrong, whatever the arithmetic says.
    #[test]
    fn an_hour_at_250_w_is_about_a_thousand_kcal() {
        let work_j = 250.0 * 3600.0;
        let out = kcal(work_j, 75.0, 3600.0);
        assert!((900.0..=1000.0).contains(&out), "got {out} kcal");
    }

    /// The work term alone must stay near the 1 kJ ≈ 1 kcal convention, so the
    /// number is recognisable next to the kJ readout beside it.
    #[test]
    fn work_alone_tracks_the_kilojoule_convention() {
        let ratio = kcal(1_000_000.0, 0.0, 0.0) / 1000.0;
        assert!((0.9..=1.1).contains(&ratio), "kcal/kJ ratio {ratio}");
    }

    /// Resting metabolism accrues with time, not with work.
    #[test]
    fn resting_burn_accrues_without_any_work() {
        let out = kcal(0.0, 80.0, 3600.0);
        assert!((out - 80.0).abs() < 1e-9, "got {out} kcal");
    }

    /// An unknown rider mass must not invent a resting burn.
    #[test]
    fn unknown_rider_mass_drops_the_resting_term() {
        assert_eq!(kcal(100_000.0, 0.0, 3600.0), kcal(100_000.0, 0.0, 0.0));
    }

    /// Garbage in must not produce a negative calorie count.
    #[test]
    fn negative_inputs_clamp_rather_than_subtract() {
        assert_eq!(kcal(-500.0, -80.0, -60.0), 0.0);
    }
}
