//! Step-timing helper used by the future runner to populate
//! [`TestStep`] timing fields.
//!
//! The runner calls [`time_step`] (async) or [`time_step_sync`] (sync)
//! around the work it wants to measure. The helper captures a wall-clock
//! start time, runs the closure, captures the end time, and rolls the
//! result into a [`TestStep`] with RFC 3339 `started_at` / `ended_at`
//! strings and an integer-millisecond `duration_ms`. Cache hits short
//! circuit timing and emit `duration_ms = 1` to match the
//! convention.

use std::future::Future;
use std::time::{Instant, SystemTime, UNIX_EPOCH};

use crate::report::TestStep;

/// Cache-hit duration in milliseconds. Matches 's convention so
/// reporters can sum across cached and cold steps without special-casing
/// the zero branch.
const CACHE_HIT_DURATION_MS: u64 = 1;

/// Time an async step and produce a populated [`TestStep`].
///
/// The runner uses this to wrap every individual call it makes against
/// an MCP server. Pass `cache_hit = true` when the response was served
/// from the local cache; the helper will skip the wall-clock duration
/// (it would be near-zero anyway) and emit the canonical `1` so reporter
/// math stays well-defined. Future returns are dropped on the floor here
/// because the runner consumes them through its own state machine; the
/// helper exists only to attach timing metadata.
pub async fn time_step<F, Fut>(name: impl Into<String>, cache_hit: bool, f: F) -> TestStep
where
    F: FnOnce() -> Fut,
    Fut: Future<Output = ()>,
{
    let label = name.into();
    if cache_hit {
        let now = rfc3339_now();
        f().await;
        return TestStep {
            name: label,
            started_at: Some(now.clone()),
            ended_at: Some(now),
            duration_ms: CACHE_HIT_DURATION_MS,
            cache_hit: true,
            consensus: None,
        };
    }
    let started_at = rfc3339_now();
    let start = Instant::now();
    f().await;
    let elapsed = start.elapsed();
    let ended_at = rfc3339_now();
    TestStep {
        name: label,
        started_at: Some(started_at),
        ended_at: Some(ended_at),
        duration_ms: elapsed.as_millis() as u64,
        cache_hit: false,
        consensus: None,
    }
}

/// Time a synchronous step and produce a populated [`TestStep`].
///
/// Same contract as [`time_step`] but for callers that have no future to
/// await. Useful for matcher-only steps where the work is pure CPU.
pub fn time_step_sync<F>(name: impl Into<String>, cache_hit: bool, f: F) -> TestStep
where
    F: FnOnce(),
{
    let label = name.into();
    if cache_hit {
        let now = rfc3339_now();
        f();
        return TestStep {
            name: label,
            started_at: Some(now.clone()),
            ended_at: Some(now),
            duration_ms: CACHE_HIT_DURATION_MS,
            cache_hit: true,
            consensus: None,
        };
    }
    let started_at = rfc3339_now();
    let start = Instant::now();
    f();
    let elapsed = start.elapsed();
    let ended_at = rfc3339_now();
    TestStep {
        name: label,
        started_at: Some(started_at),
        ended_at: Some(ended_at),
        duration_ms: elapsed.as_millis() as u64,
        cache_hit: false,
        consensus: None,
    }
}

/// RFC 3339 UTC timestamp string with second precision. Mirrors the
/// implementation in `report::upload` so the wire shape stays consistent
/// without pulling `chrono` for one call site.
fn rfc3339_now() -> String {
    let now = SystemTime::now()
        .duration_since(UNIX_EPOCH)
        .unwrap_or_default();
    let secs = now.as_secs() as i64;
    let (y, mo, d, h, mi, s) = epoch_to_civil(secs);
    format!("{y:04}-{mo:02}-{d:02}T{h:02}:{mi:02}:{s:02}Z")
}

fn epoch_to_civil(secs: i64) -> (i32, u32, u32, u32, u32, u32) {
    let days = secs.div_euclid(86_400);
    let sod = secs.rem_euclid(86_400) as u32;
    let hour = sod / 3600;
    let minute = (sod % 3600) / 60;
    let second = sod % 60;
    let z = days + 719_468;
    let era = if z >= 0 { z } else { z - 146_096 }.div_euclid(146_097);
    let doe = (z - era * 146_097) as u32;
    let yoe = (doe - doe / 1460 + doe / 36_524 - doe / 146_096) / 365;
    let y = yoe as i32 + (era * 400) as i32;
    let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
    let mp = (5 * doy + 2) / 153;
    let d = doy - (153 * mp + 2) / 5 + 1;
    let m = if mp < 10 { mp + 3 } else { mp - 9 };
    let year = if m <= 2 { y + 1 } else { y };
    (year, m, d, hour, minute, second)
}

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

    #[test]
    fn cache_hit_emits_one_millisecond() {
        let step = time_step_sync("warm-call", true, || {
            std::thread::sleep(std::time::Duration::from_millis(20));
        });
        assert_eq!(step.duration_ms, 1);
        assert!(step.cache_hit);
        assert!(step.started_at.is_some());
        assert!(step.ended_at.is_some());
    }

    #[test]
    fn cold_step_records_observed_duration_in_millis() {
        let step = time_step_sync("cold-call", false, || {
            std::thread::sleep(std::time::Duration::from_millis(50));
        });
        // Lower bound only: the point is that a cold step records the real
        // observed duration (not the 1ms cache-hit convention). A fixed upper
        // bound flaked on loaded CI runners (macOS), which can stretch a 50ms
        // sleep past any ceiling.
        assert!(
            step.duration_ms >= 45,
            "expected a ~50ms step to record at least 45ms, got {}",
            step.duration_ms
        );
        assert!(!step.cache_hit);
        assert!(step.started_at.is_some());
        assert!(step.ended_at.is_some());
    }

    #[test]
    fn step_label_round_trips_into_test_step() {
        let step = time_step_sync("create_issue", false, || {});
        assert_eq!(step.name, "create_issue");
    }

    #[test]
    fn rfc3339_timestamps_look_well_formed() {
        let step = time_step_sync("probe", false, || {});
        let started = step.started_at.expect("started_at populated");
        // Length 20 (YYYY-MM-DDTHH:MM:SSZ) and ends with Z.
        assert_eq!(started.len(), 20);
        assert!(started.ends_with('Z'));
        assert_eq!(&started[4..5], "-");
        assert_eq!(&started[7..8], "-");
        assert_eq!(&started[10..11], "T");
    }

    #[tokio::test]
    async fn async_helper_matches_sync_behavior() {
        let step = time_step("async-call", false, || async {
            tokio::time::sleep(std::time::Duration::from_millis(50)).await;
        })
        .await;
        // Lower bound only; see cold_step_records_observed_duration_in_millis.
        assert!(
            step.duration_ms >= 45,
            "expected a ~50ms async step to record at least 45ms, got {}",
            step.duration_ms
        );
    }

    #[tokio::test]
    async fn async_cache_hit_short_circuits_to_one_ms() {
        let step = time_step("hit", true, || async {
            tokio::time::sleep(std::time::Duration::from_millis(20)).await;
        })
        .await;
        assert_eq!(step.duration_ms, 1);
        assert!(step.cache_hit);
    }
}
