//! # Cartesian Position (Affine Points)
//!
//! This module defines [`Position<C, F, U>`], an **affine point** in 3D space.
//!
//! ## Mathematical Model
//!
//! A position is a point in affine space, not a vector. It represents a location
//! relative to an origin (the reference center). Key properties:
//!
//! - **Center-dependent**: Position is measured from a specific origin `C`
//! - **Frame-dependent**: The orientation is relative to a reference frame `F`
//! - **Dimensioned**: Has a length unit `U`
//!
//! ## Affine Space Operations
//!
//! Positions do **not** form a vector space. The only valid operations are:
//!
//! | Operation | Result | Meaning |
//! |-----------|--------|---------|
//! | `Position - Position` | `Displacement` | Displacement between points |
//! | `Position + Displacement` | `Position` | Translate point by displacement |
//! | `Position - Displacement` | `Position` | Translate point backwards |
//!
//! ## Forbidden Operations
//!
//! The following operations are **mathematically invalid** and do not compile:
//!
//! - `Position + Position` — Adding points has no geometric meaning
//! - `Position * scalar` — Scaling a point makes no sense without an origin
//!
//! ## Example
//!
//! ```rust
//! use affn::cartesian::{Position, Displacement};
//! use affn::centers::ReferenceCenter;
//! use affn::frames::ReferenceFrame;
//! use qtty::units::Meter;
//!
//! // Define custom center and frame (astronomy types are in downstream crates)
//! #[derive(Debug, Copy, Clone)]
//! struct MyCenter;
//! impl ReferenceCenter for MyCenter {
//!     type Params = ();
//!     fn center_name() -> &'static str { "MyCenter" }
//! }
//!
//! #[derive(Debug, Copy, Clone)]
//! struct MyFrame;
//! impl ReferenceFrame for MyFrame {
//!     fn frame_name() -> &'static str { "MyFrame" }
//! }
//!
//! // Two positions in the custom coordinate system
//! let pos1 = Position::<MyCenter, MyFrame, Meter>::new(1.0, 0.0, 0.0);
//! let pos2 = Position::<MyCenter, MyFrame, Meter>::new(1.5, 0.0, 0.0);
//!
//! // Displacement between positions
//! let displacement: Displacement<MyFrame, Meter> = pos2 - pos1;
//! assert!((displacement.x().value() - 0.5).abs() < 1e-12);
//!
//! // Translate pos1 by the displacement to get pos2
//! let result = pos1 + displacement;
//! assert!((result.x().value() - 1.5).abs() < 1e-12);
//! ```

use super::vector::Displacement;
use super::xyz::XYZ;
use crate::centers::ReferenceCenter;
use crate::frames::ReferenceFrame;
use qtty::length::LengthUnit;
use qtty::Quantity;

use std::marker::PhantomData;
use std::ops::{Add, Sub};

// Serde implementations in separate module
#[cfg(feature = "serde")]
#[path = "position_serde.rs"]
mod position_serde;

// =============================================================================
// Error Types
// =============================================================================

/// Error returned when an operation requires matching center parameters
/// but the two positions have different ones.
///
/// This occurs with **parameterized centers** (e.g., `Topocentric` with `ObserverSite`)
/// when two positions reference different observer sites.
///
/// For centers with `Params = ()` (e.g., `Geocentric`, `Heliocentric`), this error
/// can never occur because all instances share the same (empty) parameters.
#[derive(Debug, Clone)]
pub struct CenterParamsMismatchError {
    /// The operation that detected the mismatch.
    pub operation: &'static str,
}

impl std::fmt::Display for CenterParamsMismatchError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(
            f,
            "center parameter mismatch in `{}`: \
             positions reference different parameterized centers \
             (e.g., different observer sites)",
            self.operation
        )
    }
}

impl std::error::Error for CenterParamsMismatchError {}

/// An affine point in 3D Cartesian coordinates.
///
/// Positions represent locations in space relative to a reference center (origin).
/// Unlike vectors, positions do not form a vector space.
///
/// # Type Parameters
/// - `C`: The reference center (e.g., `Heliocentric`, `Geocentric`, `Topocentric`)
/// - `F`: The reference frame (e.g., `ICRS`, `EclipticMeanJ2000`, `Equatorial`)
/// - `U`: The length unit (e.g., `AstronomicalUnit`, `Kilometer`)
///
/// # Center Parameters
///
/// Some centers (like `Topocentric`) require runtime parameters stored in `C::Params`.
/// For most centers, `Params = ()` (zero overhead).
#[derive(Debug, Clone, Copy)]
pub struct Position<C: ReferenceCenter, F: ReferenceFrame, U: LengthUnit> {
    xyz: XYZ<Quantity<U>>,
    center_params: C::Params,
    _frame: PhantomData<F>,
}

// =============================================================================
// Constructors with explicit center parameters
// =============================================================================

impl<C: ReferenceCenter, F: ReferenceFrame, U: LengthUnit> Position<C, F, U> {
    /// Creates a new position with explicit center parameters.
    ///
    /// # Arguments
    /// - `center_params`: Runtime parameters for the center (e.g., `ObserverSite` for Topocentric)
    /// - `x`, `y`, `z`: Component values (converted to `Quantity<U>`)
    #[inline]
    pub fn new_with_params<T: Into<Quantity<U>>>(
        center_params: C::Params,
        x: T,
        y: T,
        z: T,
    ) -> Self {
        Self {
            xyz: XYZ::new(x.into(), y.into(), z.into()),
            center_params,
            _frame: PhantomData,
        }
    }

    /// Creates a position from internal storage with explicit center parameters.
    #[inline]
    pub(crate) fn from_xyz_with_params(center_params: C::Params, xyz: XYZ<Quantity<U>>) -> Self {
        Self {
            xyz,
            center_params,
            _frame: PhantomData,
        }
    }

    /// Creates a position from a `[Quantity<U>; 3]` array with explicit center parameters.
    #[inline]
    pub fn from_array(center_params: C::Params, arr: [Quantity<U>; 3]) -> Self {
        Self {
            xyz: XYZ::from_array(arr),
            center_params,
            _frame: PhantomData,
        }
    }

    /// Const constructor for use in const contexts.
    #[inline]
    pub const fn new_const(
        center_params: C::Params,
        x: Quantity<U>,
        y: Quantity<U>,
        z: Quantity<U>,
    ) -> Self {
        Self {
            xyz: XYZ::new(x, y, z),
            center_params,
            _frame: PhantomData,
        }
    }

    /// Returns a reference to the center parameters.
    #[inline]
    pub fn center_params(&self) -> &C::Params {
        &self.center_params
    }
}

// =============================================================================
// Convenience constructors for centers with Params = ()
// =============================================================================

impl<C, F, U> Position<C, F, U>
where
    C: ReferenceCenter<Params = ()>,
    F: ReferenceFrame,
    U: LengthUnit,
{
    /// Creates a new position for centers with `Params = ()`.
    ///
    /// This is a convenience constructor that doesn't require passing `()` explicitly.
    ///
    /// # Example
    /// ```rust
    /// use affn::cartesian::Position;
    /// use affn::frames::ReferenceFrame;
    /// use affn::centers::ReferenceCenter;
    /// use qtty::units::Meter;
    ///
    /// #[derive(Debug, Copy, Clone)]
    /// struct WorldFrame;
    /// impl ReferenceFrame for WorldFrame {
    ///     fn frame_name() -> &'static str { "WorldFrame" }
    /// }
    ///
    /// #[derive(Debug, Copy, Clone)]
    /// struct WorldOrigin;
    /// impl ReferenceCenter for WorldOrigin {
    ///     type Params = ();
    ///     fn center_name() -> &'static str { "WorldOrigin" }
    /// }
    ///
    /// let pos = Position::<WorldOrigin, WorldFrame, Meter>::new(1.0, 0.0, 0.0);
    /// ```
    #[inline]
    pub fn new<T: Into<Quantity<U>>>(x: T, y: T, z: T) -> Self {
        Self::new_with_params((), x, y, z)
    }

    /// Creates a position from a `[Quantity<U>; 3]` array for centers with `Params = ()`.
    #[inline]
    pub fn from_array_origin(arr: [Quantity<U>; 3]) -> Self {
        Self::from_array((), arr)
    }

    /// The origin of this coordinate system (all coordinates zero).
    pub const CENTER: Self = Self::new_const(
        (),
        Quantity::<U>::new(0.0),
        Quantity::<U>::new(0.0),
        Quantity::<U>::new(0.0),
    );
}

// =============================================================================
// Component Access
// =============================================================================

impl<C: ReferenceCenter, F: ReferenceFrame, U: LengthUnit> Position<C, F, U> {
    /// Returns the x-component.
    #[inline]
    pub fn x(&self) -> Quantity<U> {
        self.xyz.x()
    }

    /// Returns the y-component.
    #[inline]
    pub fn y(&self) -> Quantity<U> {
        self.xyz.y()
    }

    /// Returns the z-component.
    #[inline]
    pub fn z(&self) -> Quantity<U> {
        self.xyz.z()
    }

    /// Returns a reference to the underlying `[Quantity<U>; 3]` array.
    #[inline]
    pub fn as_array(&self) -> &[Quantity<U>; 3] {
        self.xyz.as_array()
    }

    /// Converts this position to another length unit.
    ///
    /// The center and frame are preserved while each Cartesian component is
    /// converted independently via `qtty::Quantity::to`.
    #[inline]
    pub fn to_unit<U2: LengthUnit>(&self) -> Position<C, F, U2>
    where
        C::Params: Clone,
    {
        Position::<C, F, U2>::new_with_params(
            self.center_params.clone(),
            self.x().to::<U2>(),
            self.y().to::<U2>(),
            self.z().to::<U2>(),
        )
    }

    /// Reinterprets this position as belonging to a different reference frame.
    ///
    /// This is a **zero-cost** operation: the Cartesian components and center
    /// are preserved unchanged; only the compile-time frame tag `F` is replaced
    /// by `F2`.
    ///
    /// # When to use
    ///
    /// After applying a mathematical rotation (`Rotation3 * position`) whose
    /// result carries the *original* frame tag, call this method to assign the
    /// correct *target* frame tag.
    ///
    /// ```text
    /// // Rotate from EclipticMeanJ2000 into ICRS coordinates:
    /// let rotated = rot * pos_ecl;           // still tagged EclipticMeanJ2000
    /// let pos_icrs = rotated.reinterpret_frame::<ICRS>(); // now tagged ICRS
    /// ```
    #[inline]
    pub fn reinterpret_frame<F2: ReferenceFrame>(self) -> Position<C, F2, U>
    where
        C::Params: Clone,
    {
        Position::new_with_params(self.center_params.clone(), self.x(), self.y(), self.z())
    }
}

// =============================================================================
// Geometric Operations
// =============================================================================

impl<C: ReferenceCenter, F: ReferenceFrame, U: LengthUnit> Position<C, F, U> {
    /// Computes the distance from the reference center.
    #[inline]
    pub fn distance(&self) -> Quantity<U> {
        self.xyz.magnitude()
    }

    /// Computes the distance to another position in the same center and frame.
    ///
    /// # Panics
    ///
    /// Panics if the two positions carry different `center_params` values.
    /// The panic message includes the center type name and a debug print of
    /// both `center_params` to aid diagnosis. For a non-panicking alternative,
    /// use [`try_distance_to`](Self::try_distance_to), which returns a
    /// [`CenterParamsMismatchError`] instead.
    ///
    /// # Note on Parameterized Centers
    ///
    /// This method is only available when the center has no runtime parameters
    /// (`C::Params = ()`). For parameterized centers use the fallible
    /// [`try_distance_to`](Self::try_distance_to) instead.
    #[inline]
    pub fn distance_to(&self, other: &Self) -> Quantity<U>
    where
        C: ReferenceCenter<Params = ()>,
    {
        (self.xyz - other.xyz).magnitude()
    }

    /// Checked version of [`distance_to`](Self::distance_to) that returns `Err`
    /// instead of panicking when center parameters don't match.
    ///
    /// For centers with `Params = ()`, this always succeeds.
    #[inline]
    pub fn try_distance_to(&self, other: &Self) -> Result<Quantity<U>, CenterParamsMismatchError>
    where
        C::Params: PartialEq,
    {
        if self.center_params != other.center_params {
            return Err(CenterParamsMismatchError {
                operation: "distance_to",
            });
        }
        Ok((self.xyz - other.xyz).magnitude())
    }

    /// Returns the direction (unit vector) from the center to this position.
    ///
    /// Note: Directions are frame-only types (no center). This extracts the
    /// normalized direction of the position vector.
    ///
    /// Returns `None` if the position is at the origin.
    #[inline]
    pub fn direction(&self) -> Option<super::Direction<F>> {
        self.xyz
            .to_raw()
            .try_normalize()
            .map(super::Direction::from_xyz_unchecked)
    }

    /// Returns the direction, assuming non-zero distance from origin.
    ///
    /// # Panics
    /// May produce NaN if the position is at the origin.
    #[inline]
    pub fn direction_unchecked(&self) -> super::Direction<F> {
        super::Direction::from_xyz_unchecked(self.xyz.to_raw().normalize_unchecked())
    }

    /// Converts this Cartesian position to spherical coordinates.
    ///
    /// Returns a spherical position with the same center and frame,
    /// with (polar, azimuth, distance) computed from (x, y, z).
    #[must_use]
    #[inline]
    pub fn to_spherical(&self) -> crate::spherical::Position<C, F, U> {
        crate::spherical::Position::from_cartesian(self)
    }

    /// Constructs a Cartesian position from spherical coordinates.
    ///
    /// This is equivalent to `spherical_pos.to_cartesian()`.
    #[must_use]
    #[inline]
    pub fn from_spherical(sph: &crate::spherical::Position<C, F, U>) -> Self {
        sph.to_cartesian()
    }
}

// =============================================================================
// Affine Space Operations: Position - Position -> Vector
// =============================================================================

impl<C, F, U> Sub for Position<C, F, U>
where
    C: ReferenceCenter<Params = ()>,
    F: ReferenceFrame,
    U: LengthUnit,
{
    type Output = Displacement<F, U>;

    /// Computes the displacement vector from `other` to `self`.
    ///
    /// This operator is only available when the center has no runtime
    /// parameters (`C::Params = ()`), which is the case for the standard
    /// centers `Geocentric`, `Heliocentric`, `Barycentric`, etc.
    /// For parameterized centers (e.g., `Topocentric`) use the fallible
    /// [`Position::checked_sub`] instead.
    #[inline]
    fn sub(self, other: Self) -> Self::Output {
        Displacement::from_xyz(self.xyz - other.xyz)
    }
}

impl<C, F, U> Sub<&Position<C, F, U>> for &Position<C, F, U>
where
    C: ReferenceCenter<Params = ()>,
    F: ReferenceFrame,
    U: LengthUnit,
{
    type Output = Displacement<F, U>;

    /// Computes the displacement vector from `other` to `self`.
    ///
    /// Only available when `C::Params = ()`. Use [`Position::checked_sub`]
    /// for parameterized centers.
    #[inline]
    fn sub(self, other: &Position<C, F, U>) -> Self::Output {
        Displacement::from_xyz(self.xyz - other.xyz)
    }
}

// `&Position - &Position` is hand-written above (it does not require
// `C::Params: Copy`); the macro only supplies the remaining two reference
// variants, which forward to the by-value impl and therefore inherit its
// `Copy` requirement on `C::Params`.
forward_ref_binop_lhs! {
    impl[C, F, U] Sub, sub for Position<C, F, U>, Position<C, F, U>
    where (
        C: ReferenceCenter<Params = ()>,
        F: ReferenceFrame,
        U: LengthUnit,
    )
}
forward_ref_binop_rhs! {
    impl[C, F, U] Sub, sub for Position<C, F, U>, Position<C, F, U>
    where (
        C: ReferenceCenter<Params = ()>,
        F: ReferenceFrame,
        U: LengthUnit,
    )
}

impl<C: ReferenceCenter, F: ReferenceFrame, U: LengthUnit> Position<C, F, U> {
    /// Checked subtraction that returns `Err` instead of panicking when
    /// center parameters don't match.
    ///
    /// This is the safe alternative to the `Sub` operator (`pos_a - pos_b`).
    /// For centers with `Params = ()`, this always succeeds.
    #[inline]
    pub fn checked_sub(&self, other: &Self) -> Result<Displacement<F, U>, CenterParamsMismatchError>
    where
        C::Params: PartialEq,
    {
        if self.center_params != other.center_params {
            return Err(CenterParamsMismatchError {
                operation: "sub (Position - Position)",
            });
        }
        Ok(Displacement::from_xyz(self.xyz - other.xyz))
    }
}

// =============================================================================
// Affine Space Operations: Position + Vector -> Position
// =============================================================================

impl<C, F, U> Add<Displacement<F, U>> for Position<C, F, U>
where
    C: ReferenceCenter,
    F: ReferenceFrame,
    U: LengthUnit,
{
    type Output = Self;

    /// Translates the position by a displacement vector.
    #[inline]
    fn add(self, displacement: Displacement<F, U>) -> Self::Output {
        Self::from_xyz_with_params(
            self.center_params.clone(),
            self.xyz + XYZ::from_array(*displacement.as_array()),
        )
    }
}

impl<C, F, U> Sub<Displacement<F, U>> for Position<C, F, U>
where
    C: ReferenceCenter,
    F: ReferenceFrame,
    U: LengthUnit,
{
    type Output = Self;

    /// Translates the position backwards by a displacement vector.
    #[inline]
    fn sub(self, displacement: Displacement<F, U>) -> Self::Output {
        Self::from_xyz_with_params(
            self.center_params.clone(),
            self.xyz - XYZ::from_array(*displacement.as_array()),
        )
    }
}

forward_ref_binop! {
    impl[C, F, U] Add, add for Position<C, F, U>, Displacement<F, U>
    where (
        C: ReferenceCenter,
        F: ReferenceFrame,
        U: LengthUnit,
        C::Params: Copy,
    )
}

forward_ref_binop! {
    impl[C, F, U] Sub, sub for Position<C, F, U>, Displacement<F, U>
    where (
        C: ReferenceCenter,
        F: ReferenceFrame,
        U: LengthUnit,
        C::Params: Copy,
    )
}

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

impl_quantity_fmt_triplet! {
    impl[C, F, U] for Position<C, F, U>
    where {
        C: ReferenceCenter,
        F: ReferenceFrame,
        U: LengthUnit,
    },
    fmt_each: { Quantity<U>, },
    |this, f, FmtOne| {
        write!(
            f,
            "Center: {}, Frame: {}, X: ",
            C::center_name(),
            F::frame_name()
        )?;
        FmtOne::fmt(&this.x(), f)?;
        write!(f, ", Y: ")?;
        FmtOne::fmt(&this.y(), f)?;
        write!(f, ", Z: ")?;
        FmtOne::fmt(&this.z(), f)
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    // Import the derives
    use crate::{DeriveReferenceCenter as ReferenceCenter, DeriveReferenceFrame as ReferenceFrame};

    #[allow(unused_imports)]
    use qtty::angular::{Degrees, Radians};
    #[allow(unused_imports)]
    use qtty::length::{Kilometers, Meters};
    use qtty::units::{Kilometer, Meter};
    use qtty::M;
    // Define test-specific frame and center
    #[derive(Debug, Copy, Clone, ReferenceFrame)]
    struct TestFrame;
    #[derive(Debug, Copy, Clone, ReferenceCenter)]
    struct TestCenter;

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

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

    type TestPos = Position<TestCenter, TestFrame, Meter>;
    type TestDisp = Displacement<TestFrame, Meter>;

    #[test]
    fn test_position_minus_position_gives_vector() {
        let a = TestPos::new(1.0, 2.0, 3.0);
        let b = TestPos::new(4.0, 5.0, 6.0);

        let displacement: TestDisp = b - a;
        assert!((displacement.x().value() - 3.0).abs() < 1e-12);
        assert!((displacement.y().value() - 3.0).abs() < 1e-12);
        assert!((displacement.z().value() - 3.0).abs() < 1e-12);
    }

    #[test]
    fn test_position_plus_vector_gives_position() {
        let pos = TestPos::new(1.0, 2.0, 3.0);
        let vec = TestDisp::new(1.0, 1.0, 1.0);

        let result: TestPos = pos + vec;
        assert!((result.x().value() - 2.0).abs() < 1e-12);
        assert!((result.y().value() - 3.0).abs() < 1e-12);
        assert!((result.z().value() - 4.0).abs() < 1e-12);
    }

    #[test]
    fn test_position_roundtrip() {
        let a = TestPos::new(1.0, 2.0, 3.0);
        let b = TestPos::new(4.0, 5.0, 6.0);

        // a + (b - a) == b
        let displacement = b - a;
        let result = a + displacement;
        assert!((result.x().value() - b.x().value()).abs() < 1e-12);
        assert!((result.y().value() - b.y().value()).abs() < 1e-12);
        assert!((result.z().value() - b.z().value()).abs() < 1e-12);
    }

    #[test]
    fn test_position_distance() {
        let pos = TestPos::new(3.0, 4.0, 0.0);
        assert!((pos.distance().value() - 5.0).abs() < 1e-12);
    }

    #[test]
    fn test_position_direction() {
        let pos = TestPos::new(3.0, 4.0, 0.0);
        let dir = pos.direction().expect("non-zero position");
        let norm = (dir.x() * dir.x() + dir.y() * dir.y() + dir.z() * dir.z()).sqrt();
        assert!((norm - 1.0).abs() < 1e-12);
        assert!((dir.x() - 0.6).abs() < 1e-12);
        assert!((dir.y() - 0.8).abs() < 1e-12);
    }

    #[test]
    fn test_position_with_params_and_from_array() {
        let params = TestParams { id: 42 };
        let pos = Position::<ParamCenter, TestFrame, Meter>::new_with_params(
            params.clone(),
            1.0,
            2.0,
            3.0,
        );
        assert_eq!(pos.center_params(), &params);

        let arr = [1.0 * M, 2.0 * M, 3.0 * M];
        let pos_from_arr =
            Position::<ParamCenter, TestFrame, Meter>::from_array(params.clone(), arr);
        assert_eq!(pos_from_arr.center_params(), &params);
        assert!((pos_from_arr.z().value() - 3.0).abs() < 1e-12);
    }

    #[test]
    fn test_position_from_array_origin_and_center() {
        let arr = [1.0 * M, 2.0 * M, 3.0 * M];
        let pos = Position::<TestCenter, TestFrame, Meter>::from_array_origin(arr);
        assert!((pos.x().value() - 1.0).abs() < 1e-12);

        let origin = Position::<TestCenter, TestFrame, Meter>::CENTER;
        assert!(origin.distance().value().abs() < 1e-12);
    }

    #[test]
    fn test_position_distance_to_and_sub_methods() {
        let a = TestPos::new(0.0, 0.0, 0.0);
        let b = TestPos::new(0.0, 3.0, 4.0);
        let dist = a.distance_to(&b);
        assert!((dist.value() - 5.0).abs() < 1e-12);

        let disp: TestDisp = b - a;
        assert!((disp.y().value() - 3.0).abs() < 1e-12);
    }

    #[test]
    fn test_position_direction_unchecked_and_sub_displacement() {
        let pos = TestPos::new(0.0, 3.0, 4.0);
        let dir = pos.direction_unchecked();
        assert!((dir.y() - 0.6).abs() < 1e-12);
        assert!((dir.z() - 0.8).abs() < 1e-12);

        let disp = TestDisp::new(1.0, 1.0, 1.0);
        let moved = pos - disp;
        assert!((moved.y().value() - 2.0).abs() < 1e-12);
    }

    #[test]
    fn test_position_spherical_roundtrip() {
        let pos = TestPos::new(1.0, 1.0, 1.0);
        let sph = pos.to_spherical();
        let back = TestPos::from_spherical(&sph);
        assert!((back.x().value() - pos.x().value()).abs() < 1e-12);
        assert!((back.y().value() - pos.y().value()).abs() < 1e-12);
        assert!((back.z().value() - pos.z().value()).abs() < 1e-12);
    }

    #[test]
    fn test_position_const_vec3_and_display() {
        let pos =
            Position::<TestCenter, TestFrame, Meter>::new_const((), 1.0 * M, 2.0 * M, 3.0 * M);
        let vec3 = pos.as_array();
        assert!((vec3[0].value() - 1.0).abs() < 1e-12);
        assert!((vec3[1].value() - 2.0).abs() < 1e-12);
        assert!((vec3[2].value() - 3.0).abs() < 1e-12);

        let text = pos.to_string();
        assert!(text.contains("Center: TestCenter"));
        assert!(text.contains("Frame: TestFrame"));
    }

    #[test]
    fn test_position_display_respects_format_specifiers() {
        let pos = TestPos::new(1.234_567, -2.0, 3.5);

        let text_prec = format!("{:.2}", pos);
        let expected_x_prec = format!("{:.2}", pos.x());
        assert!(text_prec.contains(&format!("X: {expected_x_prec}")));

        let text_exp = format!("{:.3e}", pos);
        let expected_z_exp = format!("{:.3e}", pos.z());
        assert!(text_exp.contains(&format!("Z: {expected_z_exp}")));
    }

    #[test]
    fn test_position_sub_ref_ref() {
        let a = TestPos::new(1.0, 2.0, 3.0);
        let b = TestPos::new(4.0, 6.0, 9.0);
        let displacement: TestDisp = b - a;
        assert!((displacement.x().value() - 3.0).abs() < 1e-12);
        assert!((displacement.y().value() - 4.0).abs() < 1e-12);
        assert!((displacement.z().value() - 6.0).abs() < 1e-12);
    }

    // =========================================================================
    // Parameterized-center runtime invariant tests
    // =========================================================================

    type ParamPos = Position<ParamCenter, TestFrame, Meter>;

    #[test]
    fn test_distance_to_same_params_succeeds() {
        // Parameterized centers must use try_distance_to; distance_to requires Params = ().
        let params = TestParams { id: 1 };
        let a = ParamPos::new_with_params(params.clone(), 0.0, 0.0, 0.0);
        let b = ParamPos::new_with_params(params, 3.0, 4.0, 0.0);
        let d = a.try_distance_to(&b).unwrap();
        assert!((d.value() - 5.0).abs() < 1e-12);
    }

    #[test]
    fn test_distance_to_mismatched_params_err() {
        // Parameterized centers use the fallible try_distance_to; mismatching
        // params produce Err rather than a panic. Using Sub (operator -) for
        // a parameterized center is a compile-time error since affn 0.6.
        let a = ParamPos::new_with_params(TestParams { id: 1 }, 0.0, 0.0, 0.0);
        let b = ParamPos::new_with_params(TestParams { id: 2 }, 3.0, 4.0, 0.0);
        assert!(a.try_distance_to(&b).is_err());
    }

    #[test]
    fn test_try_distance_to_same_params_ok() {
        let params = TestParams { id: 1 };
        let a = ParamPos::new_with_params(params.clone(), 0.0, 0.0, 0.0);
        let b = ParamPos::new_with_params(params, 3.0, 4.0, 0.0);
        let result = a.try_distance_to(&b);
        assert!(result.is_ok());
        assert!((result.unwrap().value() - 5.0).abs() < 1e-12);
    }

    #[test]
    fn test_try_distance_to_mismatched_params_err() {
        let a = ParamPos::new_with_params(TestParams { id: 1 }, 0.0, 0.0, 0.0);
        let b = ParamPos::new_with_params(TestParams { id: 2 }, 3.0, 4.0, 0.0);
        let result = a.try_distance_to(&b);
        assert!(result.is_err());
        let err = result.unwrap_err();
        assert!(err.to_string().contains("center parameter mismatch"));
    }

    // NOTE: `test_sub_mismatched_params_panics` removed — since affn 0.6 the
    // `Sub` operator is only available when `C::Params = ()`, so attempting
    // `ParamPos - ParamPos` is a *compile-time* error. Mismatched-param
    // subtraction must go through `checked_sub` (see tests below).

    #[test]
    fn test_checked_sub_same_params_ok() {
        let params = TestParams { id: 1 };
        let a = ParamPos::new_with_params(params.clone(), 0.0, 0.0, 0.0);
        let b = ParamPos::new_with_params(params, 3.0, 4.0, 0.0);
        let result = b.checked_sub(&a);
        assert!(result.is_ok());
        let disp = result.unwrap();
        assert!((disp.x().value() - 3.0).abs() < 1e-12);
        assert!((disp.y().value() - 4.0).abs() < 1e-12);
    }

    #[test]
    fn test_checked_sub_mismatched_params_err() {
        let a = ParamPos::new_with_params(TestParams { id: 1 }, 0.0, 0.0, 0.0);
        let b = ParamPos::new_with_params(TestParams { id: 2 }, 3.0, 4.0, 0.0);
        let result = b.checked_sub(&a);
        assert!(result.is_err());
        let err = result.unwrap_err();
        assert!(err.to_string().contains("center parameter mismatch"));
    }

    #[test]
    fn test_unit_params_operations_always_succeed() {
        // For Params = (), operations are trivially valid (compile-time guarantee)
        let a = TestPos::new(0.0, 0.0, 0.0);
        let b = TestPos::new(3.0, 4.0, 0.0);
        assert!((a.distance_to(&b).value() - 5.0).abs() < 1e-12);
        assert!(a.try_distance_to(&b).is_ok());
        assert!(b.checked_sub(&a).is_ok());
    }

    #[test]
    fn test_center_params_mismatch_error_display() {
        let err = CenterParamsMismatchError {
            operation: "test_op",
        };
        let msg = err.to_string();
        assert!(msg.contains("test_op"));
        assert!(msg.contains("center parameter mismatch"));

        // Verify it implements std::error::Error
        let _: &dyn std::error::Error = &err;
    }

    #[test]
    fn test_position_to_unit_roundtrip() {
        let p_m = Position::<TestCenter, TestFrame, Meter>::new(1.0, -0.5, 2.25);
        let p_km: Position<TestCenter, TestFrame, Kilometer> = p_m.to_unit();
        let back: Position<TestCenter, TestFrame, Meter> = p_km.to_unit();

        assert!((back.x().value() - p_m.x().value()).abs() < 1e-12);
        assert!((back.y().value() - p_m.y().value()).abs() < 1e-12);
        assert!((back.z().value() - p_m.z().value()).abs() < 1e-12);
    }

    #[test]
    fn test_position_to_unit_preserves_center_params() {
        let p_m = ParamPos::new_with_params(TestParams { id: 7 }, 1.0, 2.0, 3.0);
        let p_km: Position<ParamCenter, TestFrame, Kilometer> = p_m.to_unit();

        assert_eq!(p_km.center_params().id, 7);
        assert!((p_km.x().value() - 0.001).abs() < 1e-12);
        assert!((p_km.y().value() - 0.002).abs() < 1e-12);
        assert!((p_km.z().value() - 0.003).abs() < 1e-12);
    }
}
