use std::path::PathBuf;
use serde_json::Value;
use super::{sun_moon_ecef, sun_moon_eci, sun_moon_eci_at};
use crate::astro::constants::time::{DAYS_PER_JULIAN_CENTURY, J2000_JD};
use crate::astro::time::scales::TimeScales;
fn fixture_path() -> PathBuf {
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("tests/fixtures/bodies/sun_moon_skyfield_golden.json")
.canonicalize()
.expect("locate sun_moon_skyfield_golden.json")
}
fn vec3(v: &Value) -> [f64; 3] {
let a = v.as_array().expect("vec3 array");
[
a[0].as_f64().unwrap(),
a[1].as_f64().unwrap(),
a[2].as_f64().unwrap(),
]
}
fn norm(v: &[f64; 3]) -> f64 {
(v[0] * v[0] + v[1] * v[1] + v[2] * v[2]).sqrt()
}
fn angle_deg(a: &[f64; 3], b: &[f64; 3]) -> f64 {
let dot = a[0] * b[0] + a[1] * b[1] + a[2] * b[2];
let c = (dot / (norm(a) * norm(b))).clamp(-1.0, 1.0);
c.acos().to_degrees()
}
#[test]
fn sun_moon_eci_at_matches_century_argument_bit_for_bit() {
let ts = TimeScales::from_utc(2026, 4, 30, 9, 45, 0.0).expect("valid UTC instant");
let t = (ts.jd_tt - J2000_JD) / DAYS_PER_JULIAN_CENTURY;
let from_t = sun_moon_eci(t).expect("valid century argument");
let from_ts = sun_moon_eci_at(&ts).expect("valid time scales");
for i in 0..3 {
assert_eq!(from_ts.sun[i].to_bits(), from_t.sun[i].to_bits());
assert_eq!(from_ts.moon[i].to_bits(), from_t.moon[i].to_bits());
}
}
#[test]
fn sun_moon_ecef_matches_skyfield_de440_golden() {
const SUN_DIR_TOL_DEG: f64 = 0.10;
const SUN_DIST_TOL: f64 = 0.003; const MOON_DIR_TOL_DEG: f64 = 0.60;
const MOON_DIST_TOL: f64 = 0.012;
let raw = std::fs::read_to_string(fixture_path()).expect("read golden");
let doc: Value = serde_json::from_str(&raw).expect("parse golden");
let cases = doc["cases"].as_array().expect("cases");
let (mut max_sun_dir, mut max_sun_dist) = (0.0_f64, 0.0_f64);
let (mut max_moon_dir, mut max_moon_dist) = (0.0_f64, 0.0_f64);
let mut failures: Vec<String> = Vec::new();
for case in cases {
let utc = &case["utc"];
let ts = TimeScales::from_utc(
utc["year"].as_i64().unwrap() as i32,
utc["month"].as_i64().unwrap() as i32,
utc["day"].as_i64().unwrap() as i32,
utc["hour"].as_i64().unwrap() as i32,
utc["minute"].as_i64().unwrap() as i32,
utc["second"].as_f64().unwrap(),
)
.expect("golden UTC instant is valid");
let sm = sun_moon_ecef(&ts).expect("valid time scales");
let sun_ref = vec3(&case["sun_itrs_m"]);
let moon_ref = vec3(&case["moon_itrs_m"]);
let sun_dir = angle_deg(&sm.sun, &sun_ref);
let moon_dir = angle_deg(&sm.moon, &moon_ref);
let sun_dist = (norm(&sm.sun) - norm(&sun_ref)).abs() / norm(&sun_ref);
let moon_dist = (norm(&sm.moon) - norm(&moon_ref)).abs() / norm(&moon_ref);
max_sun_dir = max_sun_dir.max(sun_dir);
max_moon_dir = max_moon_dir.max(moon_dir);
max_sun_dist = max_sun_dist.max(sun_dist);
max_moon_dist = max_moon_dist.max(moon_dist);
if sun_dir > SUN_DIR_TOL_DEG
|| sun_dist > SUN_DIST_TOL
|| moon_dir > MOON_DIR_TOL_DEG
|| moon_dist > MOON_DIST_TOL
{
failures.push(format!(
"{}: sun_dir={sun_dir:.4}deg sun_dist={sun_dist:.4e} moon_dir={moon_dir:.4}deg moon_dist={moon_dist:.4e}",
case["utc"]
));
}
}
println!(
"sun_moon golden max dev: sun_dir={max_sun_dir:.4}deg sun_dist={max_sun_dist:.4e} moon_dir={max_moon_dir:.4}deg moon_dist={max_moon_dist:.4e}"
);
assert!(
failures.is_empty(),
"sun_moon_ecef golden out of tolerance:\n{}",
failures.join("\n")
);
}