#![cfg(sidereon_repo_tests)]
// Fixture provenance: `tests/fixtures/geodetic/geodetic_proj.json` is generated by
// the committed script
// `crates/sidereon-core/fixtures-generators/generate_geodetic_proj.py`.
// Reference oracle: pyproj 3.6.1 / PROJ 9.3.0, `EPSG:4978 -> EPSG:4979`,
// `always_xy=True`. Inputs are public EPSG CRS definitions plus edge cases encoded
// in the generator. Output order is `(longitude_deg, latitude_deg, altitude_m)`.
// Pinned generation environment: Python 3.11.15, PyProj 3.6.1, PROJ 9.3.0,
// macOS-26.5.1-arm64. All floating-point values are serialized as f64 hex-bit
// strings and must be compared with `f64::to_bits`, never tolerances.
use std::path::PathBuf;

use serde_json::Value;
use sidereon_core::astro::frames::transforms::geodetic_from_ecef_proj;

fn fixture_path() -> PathBuf {
    PathBuf::from(env!("CARGO_MANIFEST_DIR"))
        .join("tests")
        .join("fixtures")
        .join("geodetic")
        .join("geodetic_proj.json")
}

fn bits(value: &Value) -> f64 {
    f64_from_hex(value.as_str().expect("hex-bit string")).expect("valid f64 bits")
}

fn f64_from_hex(input: &str) -> Result<f64, std::num::ParseIntError> {
    let hex = input
        .strip_prefix("0x")
        .or_else(|| input.strip_prefix("0X"))
        .unwrap_or(input);
    u64::from_str_radix(hex, 16).map(f64::from_bits)
}

fn bits3(value: &Value) -> [f64; 3] {
    let values: Vec<f64> = value
        .as_array()
        .expect("hex-bit array")
        .iter()
        .map(bits)
        .collect();
    [values[0], values[1], values[2]]
}

fn assert_f64_slice_bits_eq(context: &str, actual: &[f64], expected: &[f64]) {
    assert_eq!(
        actual.len(),
        expected.len(),
        "{context}: length {} != {}",
        actual.len(),
        expected.len()
    );
    for (idx, (&actual, &expected)) in actual.iter().zip(expected).enumerate() {
        assert_eq!(
            actual.to_bits(),
            expected.to_bits(),
            "{context}[{idx}]: actual={actual:?} ({:016x}) expected={expected:?} ({:016x})",
            actual.to_bits(),
            expected.to_bits()
        );
    }
}

#[test]
fn geodetic_from_ecef_proj_matches_pyproj_bits() {
    let raw = std::fs::read_to_string(fixture_path()).expect("read geodetic fixture");
    let doc: Value = serde_json::from_str(&raw).expect("parse geodetic fixture");
    assert_eq!(doc["schema"], "astrodynamics-geodetic-proj-v1");
    assert_eq!(doc["pyproj_version"], "3.6.1");
    assert_eq!(doc["proj_version"], "9.3.0");

    let mut checked = 0usize;
    for case in doc["cases"].as_array().expect("cases") {
        let id = case["id"].as_u64().expect("id");
        let ecef = bits3(&case["ecef_bits"]);
        let expected = bits3(&case["lonlatalt_bits"]);
        let actual =
            geodetic_from_ecef_proj(ecef[0], ecef[1], ecef[2]).expect("valid ECEF coordinates");
        assert_f64_slice_bits_eq(&format!("case {id} lon/lat/alt"), &actual, &expected);
        checked += 1;
    }
    assert!(checked > 0, "empty geodetic fixture");
}
