// SPDX-License-Identifier: AGPL-3.0-or-later
// Copyright (C) 2026 Vallés Puig, Ramon

//! Integration tests for the `affn::ellipsoidal` module.
//!
//! Covers:
//! - `Ellipsoid` constants and derived quantities
//! - `HasEllipsoid` association on ECEF / ITRF frames
//! - Ellipsoidal `Position` constructors, normalisation, equality
//! - Named getters generated by `#[frame(inherent, ellipsoid = "...")]`
//! - `to_cartesian` / `from_cartesian` geometry
//! - Round-trip accuracy at key geodetic locations
//! - Unit-conversion correctness (Meter ↔ Kilometer)
//! - Parametrised-center usage
//! - `Default`, `ORIGIN`, `Display`, `try_new`

use affn::ellipsoid::{Ellipsoid, Grs80, HasEllipsoid, Wgs84};
use affn::ellipsoidal::Position;
#[cfg(feature = "astro")]
use affn::frames::{ECEF, ITRF};
use affn::prelude::*;
#[allow(unused_imports)]
use qtty::angular::{Degrees, Radians};
#[allow(unused_imports)]
use qtty::length::{Kilometers, Meters};
use qtty::units::{Kilometer, Meter};
use qtty::{DEG, M};

// =============================================================================
// Helper types — local frame + center, no external dependencies
// =============================================================================

#[derive(Debug, Copy, Clone, ReferenceFrame)]
struct LocalFrame;

#[derive(Debug, Copy, Clone, ReferenceCenter)]
struct LocalCenter;

#[derive(Clone, Debug, Default, PartialEq)]
struct SiteParams {
    id: u32,
}

#[derive(Debug, Copy, Clone, ReferenceCenter)]
#[center(params = SiteParams)]
struct ParametrizedCenter;

// A frame that carries the WGS84 ellipsoid for geometry tests
#[derive(Debug, Copy, Clone, ReferenceFrame)]
#[frame(
    polar = "lat",
    azimuth = "lon",
    distance = "altitude",
    ellipsoid = "Wgs84"
)]
struct GeodFrame;

// =============================================================================
// Ellipsoid constant tests
// =============================================================================

#[test]
fn wgs84_semi_major_axis() {
    assert_eq!(Wgs84::A, 6_378_137.0);
}

#[test]
fn wgs84_flattening() {
    let expected = 1.0 / 298.257_223_563;
    assert!((Wgs84::F - expected).abs() < 1e-15);
}

#[test]
fn wgs84_eccentricity_squared() {
    // e² = 2f - f²
    let f = Wgs84::F;
    let e2 = 2.0 * f - f * f;
    assert!((Wgs84::e2() - e2).abs() < 1e-15);
}

#[test]
fn wgs84_semi_minor_axis() {
    let b = Wgs84::A * (1.0 - Wgs84::F);
    assert!((Wgs84::b() - b).abs() < 1e-6);
}

#[test]
fn grs80_semi_major_axis() {
    assert_eq!(Grs80::A, 6_378_137.0);
}

#[test]
fn grs80_flattening_differs_from_wgs84() {
    assert!(
        (Grs80::F - Wgs84::F).abs() > 1e-14,
        "GRS80 and WGS84 should have slightly different flattenings"
    );
}

#[test]
fn grs80_eccentricity_squared() {
    let f = Grs80::F;
    let e2 = 2.0 * f - f * f;
    assert!((Grs80::e2() - e2).abs() < 1e-15);
}

#[test]
fn grs80_semi_minor_axis() {
    let b = Grs80::A * (1.0 - Grs80::F);
    assert!((Grs80::b() - b).abs() < 1e-6);
}

// =============================================================================
// HasEllipsoid frame association
// =============================================================================

#[test]
#[cfg(feature = "astro")]
fn ecef_has_wgs84_ellipsoid() {
    assert_eq!(<ECEF as HasEllipsoid>::Ellipsoid::A, Wgs84::A);
    assert_eq!(<ECEF as HasEllipsoid>::Ellipsoid::F, Wgs84::F);
}

#[test]
#[cfg(feature = "astro")]
fn itrf_has_grs80_ellipsoid() {
    assert_eq!(<ITRF as HasEllipsoid>::Ellipsoid::A, Grs80::A);
    assert_eq!(<ITRF as HasEllipsoid>::Ellipsoid::F, Grs80::F);
}

#[test]
fn local_geod_frame_has_wgs84_ellipsoid() {
    assert_eq!(<GeodFrame as HasEllipsoid>::Ellipsoid::A, Wgs84::A);
}

// =============================================================================
// Constructor and normalisation
// =============================================================================

#[test]
fn new_stores_values_without_normalisation_change() {
    let p = Position::<LocalCenter, LocalFrame, Meter>::new(45.0 * DEG, -30.0 * DEG, 100.0 * M);
    assert_eq!(p.lon, 45.0 * DEG);
    assert_eq!(p.lat, -30.0 * DEG);
    assert_eq!(p.height, 100.0 * M);
}

#[test]
fn new_normalises_longitude_above_180() {
    let p = Position::<LocalCenter, LocalFrame, Meter>::new(270.0 * DEG, 0.0 * DEG, 0.0 * M);
    assert!(
        (p.lon.value() - (-90.0)).abs() < 1e-12,
        "lon={}",
        p.lon.value()
    );
}

#[test]
fn new_normalises_longitude_negative() {
    let p = Position::<LocalCenter, LocalFrame, Meter>::new(-200.0 * DEG, 0.0 * DEG, 0.0 * M);
    assert!(
        (p.lon.value() - 160.0).abs() < 1e-12,
        "lon={}",
        p.lon.value()
    );
}

#[test]
fn new_normalises_latitude_past_pole() {
    // lat=100° → reflected to 80° with lon flipped by 180°
    let p = Position::<LocalCenter, LocalFrame, Meter>::new(0.0 * DEG, 100.0 * DEG, 0.0 * M);
    assert!(
        (p.lat.value() - 80.0).abs() < 1e-12,
        "lat={}",
        p.lat.value()
    );
    assert!(
        (p.lon.value() - (-180.0)).abs() < 1e-12,
        "lon={}",
        p.lon.value()
    );
}

#[test]
fn new_normalises_latitude_negative_past_pole() {
    let p = Position::<LocalCenter, LocalFrame, Meter>::new(-20.0 * DEG, -100.0 * DEG, 0.0 * M);
    assert!(
        (p.lat.value() - (-80.0)).abs() < 1e-12,
        "lat={}",
        p.lat.value()
    );
    assert!(
        (p.lon.value() - 160.0).abs() < 1e-12,
        "lon={}",
        p.lon.value()
    );
}

#[test]
fn new_raw_does_not_normalise() {
    let p = Position::<LocalCenter, LocalFrame, Meter>::new_raw(540.0 * DEG, 200.0 * DEG, 1.0 * M);
    assert_eq!(p.lon, 540.0 * DEG);
    assert_eq!(p.lat, 200.0 * DEG);
}

#[test]
fn try_new_succeeds_and_normalises() {
    let p = Position::<LocalCenter, LocalFrame, Meter>::try_new(0.0 * DEG, 271.0 * DEG, 0.0 * M)
        .unwrap();
    assert!((p.lat.value() - (-89.0)).abs() < 1e-12);
}

#[test]
fn origin_constant_is_zero() {
    let origin = Position::<LocalCenter, LocalFrame, Meter>::ORIGIN;
    assert_eq!(origin.lon, 0.0 * DEG);
    assert_eq!(origin.lat, 0.0 * DEG);
    assert_eq!(origin.height, 0.0 * M);
}

#[test]
fn default_equals_origin() {
    let d = Position::<LocalCenter, LocalFrame, Meter>::default();
    let o = Position::<LocalCenter, LocalFrame, Meter>::ORIGIN;
    assert_eq!(d, o);
}

// =============================================================================
// Equality
// =============================================================================

#[test]
fn equality_same_values() {
    let a = Position::<LocalCenter, LocalFrame, Meter>::new(10.0 * DEG, 20.0 * DEG, 30.0 * M);
    let b = Position::<LocalCenter, LocalFrame, Meter>::new(10.0 * DEG, 20.0 * DEG, 30.0 * M);
    assert_eq!(a, b);
}

#[test]
fn inequality_different_lon() {
    let a = Position::<LocalCenter, LocalFrame, Meter>::new(10.0 * DEG, 20.0 * DEG, 30.0 * M);
    let b = Position::<LocalCenter, LocalFrame, Meter>::new(11.0 * DEG, 20.0 * DEG, 30.0 * M);
    assert_ne!(a, b);
}

#[test]
fn inequality_different_lat() {
    let a = Position::<LocalCenter, LocalFrame, Meter>::new(10.0 * DEG, 20.0 * DEG, 30.0 * M);
    let b = Position::<LocalCenter, LocalFrame, Meter>::new(10.0 * DEG, 21.0 * DEG, 30.0 * M);
    assert_ne!(a, b);
}

#[test]
fn inequality_different_height() {
    let a = Position::<LocalCenter, LocalFrame, Meter>::new(10.0 * DEG, 20.0 * DEG, 30.0 * M);
    let b = Position::<LocalCenter, LocalFrame, Meter>::new(10.0 * DEG, 20.0 * DEG, 31.0 * M);
    assert_ne!(a, b);
}

// =============================================================================
// Generic height unit
// =============================================================================

#[test]
fn kilometer_height_stored_correctly() {
    let p = Position::<LocalCenter, LocalFrame, Kilometer>::new(
        0.0 * DEG,
        0.0 * DEG,
        Kilometers::new(7.5),
    );
    assert_eq!(p.height, Kilometers::new(7.5));
}

// =============================================================================
// Parametrized center
// =============================================================================

#[test]
fn new_with_params_stores_center_params() {
    let params = SiteParams { id: 99 };
    let p = Position::<ParametrizedCenter, LocalFrame, Meter>::new_raw_with_params(
        params.clone(),
        15.0 * DEG,
        -5.0 * DEG,
        500.0 * M,
    );
    assert_eq!(p.center_params(), &params);
}

#[test]
fn new_with_params_normalises() {
    let params = SiteParams { id: 1 };
    let p = Position::<ParametrizedCenter, LocalFrame, Meter>::new_with_params(
        params,
        540.0 * DEG,
        100.0 * DEG,
        0.0 * M,
    );
    assert_eq!(p.lon, 0.0 * DEG);
    assert_eq!(p.lat, 80.0 * DEG);
}

// =============================================================================
// Named getters (generated by derive `inherent`) — only testable via
// frames defined inside the affn crate itself (ECEF / ITRF).
// =============================================================================

#[test]
#[cfg(feature = "astro")]
fn ecef_named_getter_lat() {
    let p = Position::<LocalCenter, ECEF, Meter>::new(12.0 * DEG, 48.0 * DEG, 200.0 * M);
    assert_eq!(p.lat(), p.lat);
}

#[test]
#[cfg(feature = "astro")]
fn ecef_named_getter_lon() {
    let p = Position::<LocalCenter, ECEF, Meter>::new(12.0 * DEG, 48.0 * DEG, 200.0 * M);
    assert_eq!(p.lon(), p.lon);
}

#[test]
#[cfg(feature = "astro")]
fn ecef_named_getter_altitude() {
    let p = Position::<LocalCenter, ECEF, Meter>::new(12.0 * DEG, 48.0 * DEG, 200.0 * M);
    assert_eq!(p.altitude(), p.height);
}

#[test]
#[cfg(feature = "astro")]
fn itrf_named_getter_lat() {
    let p = Position::<LocalCenter, ITRF, Meter>::new(5.0 * DEG, 51.0 * DEG, 10.0 * M);
    assert_eq!(p.lat(), p.lat);
}

#[test]
#[cfg(feature = "astro")]
fn itrf_named_getter_altitude() {
    let p = Position::<LocalCenter, ITRF, Meter>::new(5.0 * DEG, 51.0 * DEG, 10.0 * M);
    assert_eq!(p.altitude(), p.height);
}

// =============================================================================
// to_cartesian geometry
// =============================================================================

const TOL_M: f64 = 0.001; // 1 mm absolute tolerance

#[test]
fn to_cartesian_equator_prime_meridian_on_ellipsoid() {
    // At (0°, 0°, h=0), X = a, Y = 0, Z = 0
    let p = Position::<LocalCenter, GeodFrame, Meter>::new(0.0 * DEG, 0.0 * DEG, 0.0 * M);
    let cart = p.to_cartesian::<Meter>();
    assert!(
        (cart.x().value() - Wgs84::A).abs() < TOL_M,
        "x={}",
        cart.x().value()
    );
    assert!(cart.y().value().abs() < TOL_M, "y={}", cart.y().value());
    assert!(cart.z().value().abs() < TOL_M, "z={}", cart.z().value());
}

#[test]
fn to_cartesian_north_pole_on_ellipsoid() {
    // At (0°, 90°, h=0), X = 0, Y = 0, Z = b
    let p = Position::<LocalCenter, GeodFrame, Meter>::new(0.0 * DEG, 90.0 * DEG, 0.0 * M);
    let cart = p.to_cartesian::<Meter>();
    assert!(cart.x().value().abs() < TOL_M, "x={}", cart.x().value());
    assert!(cart.y().value().abs() < TOL_M, "y={}", cart.y().value());
    assert!(
        (cart.z().value() - Wgs84::b()).abs() < TOL_M,
        "z={}",
        cart.z().value()
    );
}

#[test]
fn to_cartesian_south_pole_on_ellipsoid() {
    let p = Position::<LocalCenter, GeodFrame, Meter>::new(0.0 * DEG, -90.0 * DEG, 0.0 * M);
    let cart = p.to_cartesian::<Meter>();
    assert!(cart.x().value().abs() < TOL_M, "x={}", cart.x().value());
    assert!(cart.y().value().abs() < TOL_M, "y={}", cart.y().value());
    assert!(
        (cart.z().value() - (-Wgs84::b())).abs() < TOL_M,
        "z={}",
        cart.z().value()
    );
}

#[test]
fn to_cartesian_90deg_meridian_equator() {
    // At (90°, 0°, h=0), X = 0, Y = a, Z = 0
    let p = Position::<LocalCenter, GeodFrame, Meter>::new(90.0 * DEG, 0.0 * DEG, 0.0 * M);
    let cart = p.to_cartesian::<Meter>();
    assert!(cart.x().value().abs() < TOL_M, "x={}", cart.x().value());
    assert!(
        (cart.y().value() - Wgs84::A).abs() < TOL_M,
        "y={}",
        cart.y().value()
    );
    assert!(cart.z().value().abs() < TOL_M, "z={}", cart.z().value());
}

#[test]
fn to_cartesian_height_adds_radially_at_equator() {
    let h = 1000.0_f64;
    let p = Position::<LocalCenter, GeodFrame, Meter>::new(0.0 * DEG, 0.0 * DEG, h * M);
    let cart = p.to_cartesian::<Meter>();
    // At equator/prime-meridian, X = a + h, Y = Z = 0
    assert!(
        (cart.x().value() - (Wgs84::A + h)).abs() < TOL_M,
        "x={}",
        cart.x().value()
    );
    assert!(cart.y().value().abs() < TOL_M, "y={}", cart.y().value());
    assert!(cart.z().value().abs() < TOL_M, "z={}", cart.z().value());
}

#[test]
fn to_cartesian_output_in_kilometers() {
    let p = Position::<LocalCenter, GeodFrame, Meter>::new(0.0 * DEG, 0.0 * DEG, 0.0 * M);
    let cart = p.to_cartesian::<Kilometer>();
    assert!(
        (cart.x().value() - Wgs84::A / 1000.0).abs() < 0.001,
        "x={}",
        cart.x().value()
    );
}

#[test]
fn to_cartesian_la_palma_observatory_approx() {
    // Roque de los Muchachos: lat≈28.75°, lon≈−17.89°, h≈2396 m
    let p =
        Position::<LocalCenter, GeodFrame, Meter>::new(-17.8925 * DEG, 28.7543 * DEG, 2396.0 * M);
    let cart = p.to_cartesian::<Meter>();
    // Expected WGS84 ECEF for Roque de los Muchachos
    // (verified against online converters)
    assert!(
        (cart.x().value() - 5_327_336.0).abs() < 50.0,
        "x={}",
        cart.x().value()
    );
    assert!(
        (cart.y().value() - (-1_719_913.0)).abs() < 50.0,
        "y={}",
        cart.y().value()
    );
    assert!(
        (cart.z().value() - 3_051_208.0).abs() < 50.0,
        "z={}",
        cart.z().value()
    );
}

// =============================================================================
// from_cartesian geometry
// =============================================================================

fn round_trip(lon_deg: f64, lat_deg: f64, h_m: f64) {
    let orig = Position::<LocalCenter, GeodFrame, Meter>::new(
        Degrees::new(lon_deg),
        Degrees::new(lat_deg),
        Meters::new(h_m),
    );
    let cart = orig.to_cartesian::<Meter>();
    let back = Position::<LocalCenter, GeodFrame, Meter>::from_cartesian::<Meter>(&cart);

    const ANG: f64 = 1e-10; // ~0.1 µas
    const H: f64 = 0.001; // 1 mm

    assert!(
        (back.lon.value() - orig.lon.value()).abs() < ANG,
        "lon: orig={} back={}",
        orig.lon.value(),
        back.lon.value()
    );
    assert!(
        (back.lat.value() - orig.lat.value()).abs() < ANG,
        "lat: orig={} back={}",
        orig.lat.value(),
        back.lat.value()
    );
    assert!(
        (back.height.value() - orig.height.value()).abs() < H,
        "h: orig={} back={}",
        orig.height.value(),
        back.height.value()
    );
}

#[test]
fn round_trip_equator_prime_meridian() {
    round_trip(0.0, 0.0, 0.0);
}

#[test]
fn round_trip_north_pole() {
    round_trip(0.0, 90.0, 0.0);
}

#[test]
fn round_trip_south_pole() {
    round_trip(0.0, -90.0, 0.0);
}

#[test]
fn round_trip_greenwich_uk() {
    round_trip(0.0, 51.4769, 65.0);
}

#[test]
fn round_trip_canary_islands() {
    round_trip(-17.8925, 28.7543, 2396.0);
}

#[test]
fn round_trip_mauna_kea() {
    round_trip(-155.4681, 19.8207, 4205.0);
}

#[test]
fn round_trip_negative_longitude() {
    round_trip(-120.5, -35.2, 500.0);
}

#[test]
fn round_trip_date_line() {
    round_trip(179.9999, 0.0, 0.0);
}

#[test]
fn round_trip_high_altitude() {
    round_trip(45.0, 30.0, 408_000.0);
}

#[test]
fn round_trip_antipodal() {
    round_trip(-180.0, -45.0, 100.0);
}

// =============================================================================
// Unit conversion in round-trips
// =============================================================================

#[test]
fn to_cartesian_km_then_from_cartesian_km() {
    let orig = Position::<LocalCenter, GeodFrame, Kilometer>::new(
        20.0 * DEG,
        55.0 * DEG,
        Kilometers::new(0.5),
    );
    let cart = orig.to_cartesian::<Kilometer>();
    let back = Position::<LocalCenter, GeodFrame, Kilometer>::from_cartesian::<Kilometer>(&cart);
    assert!((back.lat.value() - orig.lat.value()).abs() < 1e-9);
    assert!((back.lon.value() - orig.lon.value()).abs() < 1e-9);
    assert!((back.height.value() - orig.height.value()).abs() < 1e-6); // 1 mm in km
}

#[test]
fn meter_and_kilometer_input_produce_equal_cartesian_output() {
    let p_m = Position::<LocalCenter, GeodFrame, Meter>::new(0.0 * DEG, 45.0 * DEG, 1000.0 * M);
    let p_km = Position::<LocalCenter, GeodFrame, Kilometer>::new(
        0.0 * DEG,
        45.0 * DEG,
        Kilometers::new(1.0),
    );
    let c_m = p_m.to_cartesian::<Meter>();
    let c_km = p_km.to_cartesian::<Meter>();
    assert!((c_m.x().value() - c_km.x().value()).abs() < 0.01);
    assert!((c_m.y().value() - c_km.y().value()).abs() < 0.01);
    assert!((c_m.z().value() - c_km.z().value()).abs() < 0.01);
}

// =============================================================================
// Display
// =============================================================================

#[test]
fn display_contains_center_and_frame_names() {
    let p = Position::<LocalCenter, LocalFrame, Meter>::new(10.0 * DEG, 20.0 * DEG, 100.0 * M);
    let s = p.to_string();
    assert!(s.contains("LocalCenter"), "display: {s}");
    assert!(s.contains("LocalFrame"), "display: {s}");
}

#[test]
fn display_contains_coordinates() {
    let p = Position::<LocalCenter, LocalFrame, Meter>::new(10.0 * DEG, 20.0 * DEG, 100.0 * M);
    let s = p.to_string();
    assert!(s.contains("10"), "display: {s}");
    assert!(s.contains("20"), "display: {s}");
}

// =============================================================================
// ECEF / ITRF integration (astro feature)
// =============================================================================

#[test]
#[cfg(feature = "astro")]
fn ecef_equator_x_equals_wgs84_a() {
    let p = Position::<LocalCenter, ECEF, Meter>::new(0.0 * DEG, 0.0 * DEG, 0.0 * M);
    let c = p.to_cartesian::<Meter>();
    assert!(
        (c.x().value() - Wgs84::A).abs() < TOL_M,
        "x={}",
        c.x().value()
    );
}

#[test]
#[cfg(feature = "astro")]
fn itrf_equator_x_equals_grs80_a() {
    let p = Position::<LocalCenter, ITRF, Meter>::new(0.0 * DEG, 0.0 * DEG, 0.0 * M);
    let c = p.to_cartesian::<Meter>();
    assert!(
        (c.x().value() - Grs80::A).abs() < TOL_M,
        "x={}",
        c.x().value()
    );
}

#[test]
#[cfg(feature = "astro")]
fn ecef_and_itrf_differ_at_45deg_due_to_different_ellipsoids() {
    let pe = Position::<LocalCenter, ECEF, Meter>::new(0.0 * DEG, 45.0 * DEG, 0.0 * M);
    let pi = Position::<LocalCenter, ITRF, Meter>::new(0.0 * DEG, 45.0 * DEG, 0.0 * M);
    let ce = pe.to_cartesian::<Meter>();
    let ci = pi.to_cartesian::<Meter>();
    let diff = ((ce.x().value() - ci.x().value()).powi(2)
        + (ce.z().value() - ci.z().value()).powi(2))
    .sqrt();
    // WGS84 vs GRS80 differ in 9th decimal of f, difference is sub-mm
    assert!(diff < 0.1, "ECEF/ITRF ellipsoid diff = {diff} m");
    assert!(diff > 0.0, "ECEF and ITRF should not be identical");
}

#[test]
#[cfg(feature = "astro")]
fn ecef_greenwich_round_trip() {
    let p = Position::<LocalCenter, ECEF, Meter>::new(0.0 * DEG, 51.4769 * DEG, 65.0 * M);
    let cart = p.to_cartesian::<Meter>();
    let back = Position::<LocalCenter, ECEF, Meter>::from_cartesian::<Meter>(&cart);
    assert!((back.lat.value() - p.lat.value()).abs() < 1e-10);
    assert!((back.lon.value() - p.lon.value()).abs() < 1e-10);
    assert!((back.height.value() - p.height.value()).abs() < 0.001);
}
