//! Profiles shipped with the app, so there is always something to ride and the
//! YAML schema (`crates/core/src/profile.rs`) has worked examples.

use crate::commands::SampleProfile;

const OVER_UNDERS: &str = r#"name: Over-unders
description: Ten minutes up to threshold, then eight over-under cycles, then easy.
looping: false
blocks:
  - type: ramp
    channel: power
    from: 110
    to: 210
    extent: { seconds: 600 }
  - type: wave
    channel: power
    shape: sine
    midpoint: 245
    amplitude: 45
    period: { seconds: 120 }
    repeats: 8
  - type: constant
    channel: power
    value: 120
    extent: { seconds: 300 }
"#;

const HILL_REPEATS: &str = r#"name: Hill repeats
description: Four kilometres of rolling terrain, looped. Gradient by distance.
looping: true
blocks:
  - type: segments
    segments:
      - { distance_m: 600,  gradient_pct: 1.0 }
      - { distance_m: 900,  gradient_pct: 6.5 }
      - { distance_m: 300,  gradient_pct: 9.0 }
      - { distance_m: 500,  gradient_pct: -3.0 }
      - { distance_m: 700,  gradient_pct: 4.0 }
      - { distance_m: 1000, gradient_pct: -2.0 }
"#;

const SAWTOOTH_GRADE: &str = r#"name: Sawtooth grade
description: A gradient sawtooth for shakedown testing — every 400 m ramps 0 to 8%.
looping: true
blocks:
  - type: wave
    channel: gradient
    shape: sawtooth
    midpoint: 4.0
    amplitude: 4.0
    period: { metres: 400 }
    repeats: 20
"#;

const STEADY_ENDURANCE: &str = r#"name: Steady endurance
description: Ninety minutes at a fixed grade, with a gentle triangular trim.
looping: false
blocks:
  - type: constant
    channel: gradient
    value: 2.0
    extent: { seconds: 900 }
  - type: wave
    channel: gradient
    shape: triangle
    midpoint: 3.0
    amplitude: 2.5
    period: { seconds: 600 }
    repeats: 7
  - type: constant
    channel: gradient
    value: 0.0
    extent: { seconds: 600 }
"#;

/// The gearing bench test, shipped rather than kept in a scratch file because
/// it is the fastest way to answer "do the gears and the resistance work?" on
/// real hardware. Flat on purpose: on a slope, gravity swamps everything and a
/// broken gear ratio still feels like a hill.
const DRAG_RACE: &str = r#"name: Drag race
description: >-
  A standing-start kilometre on a dead-flat road, for testing that the gears and
  the resistance actually do something. Start in bottom gear from a stop and
  wind it up: every shift should land under the pedals at once, and holding one
  gear should get harder as you speed up, because on the flat drag is the only
  thing resisting you and it grows with the square of speed. If shifting feels
  like nothing, the control writes are not reaching the trainer. Loops, so you
  can go again in a different gear and compare the time.
looping: true
blocks:
  - type: segments
    segments:
      - distance_m: 1000.0
        gradient_pct: 0.0
"#;

/// A real GPX, bundled so the route view has something to draw on first run.
const SAMPLE_CLIMB_GPX: &str = include_str!("../../testdata/sample-climb.gpx");

pub fn all() -> Vec<SampleProfile> {
    // Drag race first among the written profiles: it is the bench test, and the
    // thing most likely to be wanted in a hurry when the gearing feels wrong.
    let mut out: Vec<SampleProfile> = [
        DRAG_RACE,
        OVER_UNDERS,
        HILL_REPEATS,
        SAWTOOTH_GRADE,
        STEADY_ENDURANCE,
    ]
    .iter()
    .map(|yaml| {
        let (name, summary) = header(yaml);
        SampleProfile {
            name,
            summary,
            text: (*yaml).to_string(),
            is_gpx: false,
        }
    })
    .collect();
    out.insert(
        0,
        SampleProfile {
            name: "Sample climb".into(),
            summary: "3 km GPX with real GPS elevation noise — smoothed on import.".into(),
            text: SAMPLE_CLIMB_GPX.to_string(),
            is_gpx: true,
        },
    );
    out
}

fn header(yaml: &str) -> (String, String) {
    let mut name = String::from("Profile");
    let mut summary = String::new();
    for line in yaml.lines() {
        if let Some(rest) = line.strip_prefix("name: ") {
            name = rest.trim().to_string();
        } else if let Some(rest) = line.strip_prefix("description: ") {
            summary = rest.trim().to_string();
        }
    }
    (name, summary)
}

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

    #[test]
    fn every_shipped_profile_parses() {
        // These are only ever exercised when a rider clicks one, so a typo in a
        // heredoc ships and stays shipped. Parsing them here is the difference
        // between finding that at compile time and finding it mid-warm-up.
        for sample in all() {
            if sample.is_gpx {
                continue;
            }
            bikecontrol_core::profile::Profile::from_yaml(&sample.text)
                .unwrap_or_else(|e| panic!("sample profile {:?} does not parse: {e}", sample.name));
        }
    }

    #[test]
    fn the_drag_race_is_offered_and_is_the_flat_kilometre_it_claims() {
        // Flat is the whole point: on a slope gravity swamps the gearing and a
        // broken ratio still feels like a hill.
        let sample = all()
            .into_iter()
            .find(|s| s.name == "Drag race")
            .expect("the drag race must reach the picker — it was defined but unlisted once");
        let profile = bikecontrol_core::profile::Profile::from_yaml(&sample.text).unwrap();
        assert!(
            profile.looping,
            "you must be able to go again without reloading"
        );

        let extent = profile.total_extent();
        let metres = extent
            .metres
            .expect("a drag race is measured in distance, not time");
        assert!(
            (metres - 1000.0).abs() < 1.0,
            "expected a kilometre, got {metres} m"
        );
    }

    #[test]
    fn a_name_and_summary_are_extracted_for_every_sample() {
        for sample in all() {
            assert!(!sample.name.is_empty(), "a nameless entry in the picker");
            assert_ne!(sample.name, "Profile", "fell back to the placeholder name");
        }
    }
}
