use super::helpers::json_err;
use std::sync::Mutex;
use std::time::Instant;
static DOCTOR_CACHE: Mutex<Option<(Instant, String)>> = Mutex::new(None);
const DOCTOR_TTL_SECS: u64 = 600;
pub(super) fn handle(
path: &str,
_query_str: &str,
method: &str,
_body: &str,
) -> Option<(&'static str, &'static str, String)> {
match path {
"/api/doctor/fix" if method.eq_ignore_ascii_case("POST") => {
if let Ok(mut guard) = DOCTOR_CACHE.lock() {
*guard = None;
}
Some(post_fix())
}
"/api/doctor/fix" => Some((
"405 Method Not Allowed",
"application/json",
json_err("use POST to run doctor --fix"),
)),
"/api/doctor" => Some(get_doctor_cached()),
_ => None,
}
}
fn get_doctor_cached() -> (&'static str, &'static str, String) {
if let Ok(guard) = DOCTOR_CACHE.lock() {
if let Some((ts, ref body)) = *guard {
if ts.elapsed().as_secs() < DOCTOR_TTL_SECS {
return ("200 OK", "application/json", body.clone());
}
}
}
let result = get_doctor();
if result.0 == "200 OK" {
if let Ok(mut guard) = DOCTOR_CACHE.lock() {
*guard = Some((Instant::now(), result.2.clone()));
}
}
result
}
fn get_doctor() -> (&'static str, &'static str, String) {
let report = crate::doctor::health_report();
serde_json::to_string(&report).map_or_else(
|e| {
(
"500 Internal Server Error",
"application/json",
json_err(&format!("failed to serialize doctor report: {e}")),
)
},
|body| ("200 OK", "application/json", body),
)
}
fn post_fix() -> (&'static str, &'static str, String) {
match crate::doctor::run_fix_report() {
Ok(report) => serde_json::to_string(&report).map_or_else(
|e| {
(
"500 Internal Server Error",
"application/json",
json_err(&format!("failed to serialize fix report: {e}")),
)
},
|body| ("200 OK", "application/json", body),
),
Err(e) => (
"500 Internal Server Error",
"application/json",
json_err(&format!("doctor --fix failed: {e}")),
),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn get_doctor_returns_structured_health_json() {
let (status, mime, body) = handle("/api/doctor", "", "GET", "").expect("route matches");
assert_eq!(status, "200 OK");
assert_eq!(mime, "application/json");
let v: serde_json::Value = serde_json::from_str(&body).expect("valid JSON");
assert!(
matches!(v["level"].as_str(), Some("good" | "warnings" | "issues")),
"level must be one of the three badge states, got {:?}",
v["level"]
);
assert!(v["total"].as_u64().is_some(), "total must be present");
assert!(v["checks"].is_array(), "checks must be an array");
}
#[test]
fn fix_route_rejects_get_with_405() {
let (status, _mime, body) =
handle("/api/doctor/fix", "", "GET", "").expect("route matches");
assert_eq!(status, "405 Method Not Allowed");
assert!(
body.contains("POST"),
"the 405 must hint at the right method"
);
}
#[test]
fn unrelated_paths_pass_through() {
assert!(handle("/api/stats", "", "GET", "").is_none());
assert!(handle("/api/doctorx", "", "GET", "").is_none());
assert!(handle("/", "", "GET", "").is_none());
}
}