use std::{
  fmt::{Display, Formatter, Result as FmtResult},
  ops::{Add, Div, Mul, Sub},
};

#[cfg(feature = "space-cmy")]
use crate::space::Cmy;
#[cfg(feature = "space-cmyk")]
use crate::space::Cmyk;
#[cfg(feature = "space-hpluv")]
use crate::space::Hpluv;
#[cfg(feature = "space-hsi")]
use crate::space::Hsi;
#[cfg(feature = "space-hsl")]
use crate::space::Hsl;
#[cfg(feature = "space-hsluv")]
use crate::space::Hsluv;
#[cfg(feature = "space-hsv")]
use crate::space::Hsv;
#[cfg(feature = "space-hwb")]
use crate::space::Hwb;
#[cfg(feature = "space-lab")]
use crate::space::Lab;
#[cfg(feature = "space-lch")]
use crate::space::Lch;
#[cfg(feature = "space-okhsl")]
use crate::space::Okhsl;
#[cfg(feature = "space-okhsv")]
use crate::space::Okhsv;
#[cfg(feature = "space-okhwb")]
use crate::space::Okhwb;
#[cfg(feature = "space-oklab")]
use crate::space::Oklab;
#[cfg(feature = "space-oklch")]
use crate::space::Oklch;
#[cfg(feature = "space-xyy")]
use crate::space::Xyy;
use crate::{
  ColorimetricContext, Illuminant, Observer,
  component::Component,
  space::{ColorSpace, Lms, Luv, Rgb, RgbSpec, Srgb, Xyz},
};

/// Chroma threshold below which a color is considered achromatic (hueless).
const ACHROMATIC_THRESHOLD: f64 = 1e-4;

/// sRGB gamut boundary coefficients for the denominator (XYZ columns 0, 1, 2).
#[cfg(any(feature = "space-hsluv", feature = "space-hpluv"))]
const DENOM_COEFFS: [f64; 3] = [0.0, -126_452.0, 632_260.0];

/// CIE 1976 L\*u\*v\* threshold: (6/29)³.
#[cfg(any(feature = "space-hsluv", feature = "space-hpluv"))]
const EPSILON: f64 = 0.0088564516790356308;

/// sRGB gamut boundary coefficients for the intercept numerator (XYZ columns 0, 1, 2).
#[cfg(any(feature = "space-hsluv", feature = "space-hpluv"))]
const INTERCEPT_COEFFS: [f64; 3] = [731_718.0, 769_860.0, 838_422.0];

/// CIE 1976 L\*u\*v\* scaling factor: (29/3)³.
#[cfg(any(feature = "space-hsluv", feature = "space-hpluv"))]
const KAPPA: f64 = 903.2962962962963;

/// L\* formula offset: `L* = 116 * f(Y/Yn) - 16`.
#[cfg(any(feature = "space-hsluv", feature = "space-hpluv"))]
const L_STAR_OFFSET: f64 = 16.0;

/// Cube of the L\* scale factor: 116³ = 1,560,896.
#[cfg(any(feature = "space-hsluv", feature = "space-hpluv"))]
const L_STAR_SCALE_CUBED: f64 = 1_560_896.0;

/// Minimum denominator magnitude for the ray–line intersection in chroma computation.
#[cfg(feature = "space-hsluv")]
const RAY_INTERSECTION_THRESHOLD: f64 = 1e-15;

/// sRGB gamut boundary coefficients for the slope numerator (XYZ columns 0, 1, 2).
///
/// Derived from the algebraic inversion of CIE LUV chromaticity against sRGB channel limits.
/// See <https://www.hsluv.org/math/> for the full derivation.
#[cfg(any(feature = "space-hsluv", feature = "space-hpluv"))]
const SLOPE_COEFFS: [f64; 3] = [284_517.0, 0.0, -94_839.0];

/// CIE LCh(uv) color space (cylindrical form of CIE L*u*v*).
///
/// A cylindrical representation of the CIE L\*u\*v\* color space where L\* represents
/// lightness (0–100), C\*\_uv represents chroma (colorfulness), and H\_uv represents
/// hue stored internally as a 0.0–1.0 fraction (0–360°). Uses the same L\* lightness
/// axis as Luv but replaces the rectangular u\*/v\* axes with polar coordinates.
#[derive(Clone, Copy, Debug)]
pub struct Lchuv {
  alpha: Component,
  c: Component,
  context: ColorimetricContext,
  h: Component,
  l: Component,
}

impl Lchuv {
  /// The default viewing context for Lchuv (D65 illuminant, CIE 1931 2° observer).
  pub const DEFAULT_CONTEXT: ColorimetricContext = ColorimetricContext::new()
    .with_illuminant(Illuminant::D65)
    .with_observer(Observer::CIE_1931_2D);

  /// Creates a new Lchuv color from lightness (0–100), chroma, and hue (0–360°).
  pub fn new(l: impl Into<Component>, c: impl Into<Component>, h: impl Into<Component>) -> Self {
    Self {
      alpha: Component::new(1.0),
      c: c.into(),
      context: Self::DEFAULT_CONTEXT,
      h: Component::new((h.into().0 / 360.0).rem_euclid(1.0)),
      l: l.into(),
    }
  }

  /// Creates a new Lchuv color in a const context from lightness, chroma, and hue (0–360°).
  pub const fn new_const(l: f64, c: f64, h: f64) -> Self {
    let r = (h / 360.0) % 1.0;

    Self {
      alpha: Component::new_const(1.0),
      c: Component::new_const(c),
      context: Self::DEFAULT_CONTEXT,
      h: Component::new_const(if r < 0.0 { r + 1.0 } else { r }),
      l: Component::new_const(l),
    }
  }

  /// Adapts this color to a different viewing context via Luv and XYZ.
  pub fn adapt_to(&self, context: ColorimetricContext) -> Self {
    let reference_white = self.context.reference_white();
    let target_white = context.reference_white();

    if reference_white == target_white {
      return self.with_context(context);
    }

    Self::from(self.to_luv().adapt_to(context))
  }

  /// Returns the C\*\_uv (chroma) component.
  pub fn c(&self) -> f64 {
    self.c.0
  }

  /// Returns the chroma value (alias for [`Self::c`]).
  pub fn chroma(&self) -> f64 {
    self.c.0
  }

  /// Returns the [L\*, C\*\_uv, H\_uv] components as an array (hue normalized to 0.0–1.0).
  pub fn components(&self) -> [f64; 3] {
    [self.l.0, self.c.0, self.h.0]
  }

  /// Returns the viewing context for this color.
  pub fn context(&self) -> &ColorimetricContext {
    &self.context
  }

  /// Decreases the chroma by the given amount.
  pub fn decrement_c(&mut self, amount: impl Into<Component>) {
    self.c -= amount.into();
  }

  /// Alias for [`Self::decrement_c`].
  pub fn decrement_chroma(&mut self, amount: impl Into<Component>) {
    self.decrement_c(amount)
  }

  /// Decreases the normalized hue by the given amount (wraps around 0.0–1.0).
  pub fn decrement_h(&mut self, amount: impl Into<Component>) {
    self.h = Component::new((self.h.0 - amount.into().0).rem_euclid(1.0));
  }

  /// Decreases the hue by the given amount in degrees (wraps around 0–360°).
  pub fn decrement_hue(&mut self, amount: impl Into<Component>) {
    self.decrement_h(amount.into() / 360.0)
  }

  /// Decreases the L\* component by the given amount.
  pub fn decrement_l(&mut self, amount: impl Into<Component>) {
    self.l -= amount.into();
  }

  /// Generates a sequence of evenly-spaced colors between `self` and `other`.
  ///
  /// Returns `steps` colors including both endpoints, interpolated in the LCh(uv) color space.
  /// When `steps` is 0 the result is empty. When `steps` is 1 the result contains only `self`.
  ///
  /// Accepts any color type that can be converted to [`Xyz`].
  pub fn gradient(&self, other: impl Into<Xyz>, steps: usize) -> Vec<Self> {
    if steps == 0 {
      return Vec::new();
    }
    let other = other.into();
    if steps == 1 {
      return vec![self.mix(other, 0.0)];
    }
    let divisor = (steps - 1) as f64;
    (0..steps).map(|i| self.mix(other, i as f64 / divisor)).collect()
  }

  /// Returns the normalized hue component (0.0–1.0).
  pub fn h(&self) -> f64 {
    self.h.0
  }

  /// Returns the hue in degrees (0–360°).
  pub fn hue(&self) -> f64 {
    self.h.0 * 360.0
  }

  /// Increases the chroma by the given amount.
  pub fn increment_c(&mut self, amount: impl Into<Component>) {
    self.c += amount.into();
  }

  /// Alias for [`Self::increment_c`].
  pub fn increment_chroma(&mut self, amount: impl Into<Component>) {
    self.increment_c(amount)
  }

  /// Increases the normalized hue by the given amount (wraps around 0.0–1.0).
  pub fn increment_h(&mut self, amount: impl Into<Component>) {
    self.h = Component::new((self.h.0 + amount.into().0).rem_euclid(1.0));
  }

  /// Increases the hue by the given amount in degrees (wraps around 0–360°).
  pub fn increment_hue(&mut self, amount: impl Into<Component>) {
    self.increment_h(amount.into() / 360.0)
  }

  /// Increases the L\* component by the given amount.
  pub fn increment_l(&mut self, amount: impl Into<Component>) {
    self.l += amount.into();
  }

  /// Returns the L\* (lightness) component.
  pub fn l(&self) -> f64 {
    self.l.0
  }

  /// Interpolates between `self` and `other` at parameter `t`, returning a new color.
  ///
  /// When `t` is 0.0 the result matches `self`, when 1.0 it matches `other`.
  /// Values outside 0.0–1.0 extrapolate beyond the endpoints. Interpolation is
  /// performed in the LCh(uv) color space with shortest-arc hue and achromatic handling
  /// per the CSS Color Level 4 specification.
  ///
  /// Accepts any color type that can be converted to [`Xyz`].
  pub fn mix(&self, other: impl Into<Xyz>, t: f64) -> Self {
    let other = Self::from(other.into());

    let l = Component::new(self.l()).lerp(other.l(), t);
    let c = Component::new(self.c()).lerp(other.c(), t);
    let h = mix_hue(self.hue(), self.c(), other.hue(), other.c(), t);
    let alpha = Component::new(self.alpha()).lerp(other.alpha(), t);

    Self::new(l, c, h).with_alpha(alpha)
  }

  /// Interpolates `self` toward `other` at parameter `t`, mutating in place.
  ///
  /// See [`mix`](Self::mix) for details on the interpolation behavior.
  pub fn mixed_with(&mut self, other: impl Into<Xyz>, t: f64) {
    let result = self.mix(other, t);
    self.l = result.l;
    self.c = result.c;
    self.h = result.h;
    self.alpha = result.alpha;
  }

  /// Scales the chroma by the given factor.
  pub fn scale_c(&mut self, factor: impl Into<Component>) {
    self.c *= factor.into();
  }

  /// Alias for [`Self::scale_c`].
  pub fn scale_chroma(&mut self, factor: impl Into<Component>) {
    self.scale_c(factor)
  }

  /// Scales the normalized hue by the given factor (wraps around 0.0–1.0).
  pub fn scale_h(&mut self, factor: impl Into<Component>) {
    self.h = Component::new((self.h.0 * factor.into().0).rem_euclid(1.0));
  }

  /// Alias for [`Self::scale_h`].
  pub fn scale_hue(&mut self, factor: impl Into<Component>) {
    self.scale_h(factor)
  }

  /// Scales the L\* component by the given factor.
  pub fn scale_l(&mut self, factor: impl Into<Component>) {
    self.l *= factor.into();
  }

  /// Sets the C\*\_uv component.
  pub fn set_c(&mut self, c: impl Into<Component>) {
    self.c = c.into();
  }

  /// Alias for [`Self::set_c`].
  pub fn set_chroma(&mut self, chroma: impl Into<Component>) {
    self.set_c(chroma)
  }

  /// Sets the [L\*, C\*\_uv, H\_uv] components from an array.
  pub fn set_components(&mut self, components: [impl Into<Component> + Clone; 3]) {
    self.set_l(components[0].clone());
    self.set_c(components[1].clone());
    self.set_h(components[2].clone());
  }

  /// Sets the normalized hue component (0.0–1.0).
  pub fn set_h(&mut self, h: impl Into<Component>) {
    self.h = h.into();
  }

  /// Sets the hue from a value in degrees (0–360°).
  pub fn set_hue(&mut self, hue: impl Into<Component>) {
    self.h = Component::new((hue.into().0 / 360.0).rem_euclid(1.0));
  }

  /// Sets the L\* component.
  pub fn set_l(&mut self, l: impl Into<Component>) {
    self.l = l.into();
  }

  /// Converts to the HPLuv color space.
  #[cfg(feature = "space-hpluv")]
  pub fn to_hpluv(&self) -> Hpluv {
    let l = self.l();
    let c = self.chroma();
    let h = self.hue();

    if l > 99.9999999 {
      return Hpluv::new(h, 0.0, 100.0).with_alpha(self.alpha);
    }
    if l < 0.00000001 {
      return Hpluv::new(h, 0.0, 0.0).with_alpha(self.alpha);
    }

    let max_c = max_safe_chroma_for_l(l);
    let s = if max_c > 0.0 { c / max_c * 100.0 } else { 0.0 };

    Hpluv::new(h, s, l).with_alpha(self.alpha)
  }

  /// Converts to the HSLuv color space.
  #[cfg(feature = "space-hsluv")]
  pub fn to_hsluv(&self) -> Hsluv {
    let l = self.l();
    let c = self.chroma();
    let h = self.hue();

    if l > 99.9999999 {
      return Hsluv::new(h, 0.0, 100.0).with_alpha(self.alpha);
    }
    if l < 0.00000001 {
      return Hsluv::new(h, 0.0, 0.0).with_alpha(self.alpha);
    }

    let max_c = max_safe_chroma_for_lh(l, h);
    let s = if max_c > 0.0 { c / max_c * 100.0 } else { 0.0 };

    Hsluv::new(h, s, l).with_alpha(self.alpha)
  }

  /// Converts to the CIE L\*u\*v\* color space.
  pub fn to_luv(&self) -> Luv {
    let h_rad = self.h.0 * 2.0 * std::f64::consts::PI;
    let u = self.c.0 * h_rad.cos();
    let v = self.c.0 * h_rad.sin();

    Luv::new(self.l, u, v).with_context(self.context).with_alpha(self.alpha)
  }

  /// Converts to the specified RGB color space.
  pub fn to_rgb<S>(&self) -> Rgb<S>
  where
    S: RgbSpec,
  {
    self.to_luv().to_rgb::<S>()
  }

  /// Converts to the CIE XYZ color space.
  pub fn to_xyz(&self) -> Xyz {
    self.to_luv().to_xyz()
  }

  /// Returns a new color with the given C\*\_uv value.
  pub fn with_c(&self, c: impl Into<Component>) -> Self {
    Self {
      c: c.into(),
      ..*self
    }
  }

  /// Returns a new color with C\*\_uv decreased by the given amount.
  pub fn with_c_decremented_by(&self, amount: impl Into<Component>) -> Self {
    let mut lchuv = *self;
    lchuv.decrement_c(amount);
    lchuv
  }

  /// Returns a new color with C\*\_uv increased by the given amount.
  pub fn with_c_incremented_by(&self, amount: impl Into<Component>) -> Self {
    let mut lchuv = *self;
    lchuv.increment_c(amount);
    lchuv
  }

  /// Returns a new color with C\*\_uv scaled by the given factor.
  pub fn with_c_scaled_by(&self, factor: impl Into<Component>) -> Self {
    let mut lchuv = *self;
    lchuv.scale_c(factor);
    lchuv
  }

  /// Alias for [`Self::with_c`].
  pub fn with_chroma(&self, chroma: impl Into<Component>) -> Self {
    self.with_c(chroma)
  }

  /// Alias for [`Self::with_c_decremented_by`].
  pub fn with_chroma_decremented_by(&self, amount: impl Into<Component>) -> Self {
    self.with_c_decremented_by(amount)
  }

  /// Alias for [`Self::with_c_incremented_by`].
  pub fn with_chroma_incremented_by(&self, amount: impl Into<Component>) -> Self {
    self.with_c_incremented_by(amount)
  }

  /// Alias for [`Self::with_c_scaled_by`].
  pub fn with_chroma_scaled_by(&self, factor: impl Into<Component>) -> Self {
    self.with_c_scaled_by(factor)
  }

  /// Returns this color with a different viewing context (without adaptation).
  pub fn with_context(&self, context: ColorimetricContext) -> Self {
    Self {
      context,
      ..*self
    }
  }

  /// Returns a new color with the given normalized hue (0.0–1.0).
  pub fn with_h(&self, h: impl Into<Component>) -> Self {
    Self {
      h: h.into(),
      ..*self
    }
  }

  /// Returns a new color with normalized hue decreased by the given amount (wraps around 0.0–1.0).
  pub fn with_h_decremented_by(&self, amount: impl Into<Component>) -> Self {
    let mut lchuv = *self;
    lchuv.decrement_h(amount);
    lchuv
  }

  /// Returns a new color with normalized hue increased by the given amount (wraps around 0.0–1.0).
  pub fn with_h_incremented_by(&self, amount: impl Into<Component>) -> Self {
    let mut lchuv = *self;
    lchuv.increment_h(amount);
    lchuv
  }

  /// Returns a new color with normalized hue scaled by the given factor (wraps around 0.0–1.0).
  pub fn with_h_scaled_by(&self, factor: impl Into<Component>) -> Self {
    let mut lchuv = *self;
    lchuv.scale_h(factor);
    lchuv
  }

  /// Returns a new color with the given hue in degrees (0–360°).
  pub fn with_hue(&self, hue: impl Into<Component>) -> Self {
    Self {
      h: Component::new((hue.into().0 / 360.0).rem_euclid(1.0)),
      ..*self
    }
  }

  /// Returns a new color with hue decreased by the given amount in degrees.
  pub fn with_hue_decremented_by(&self, amount: impl Into<Component>) -> Self {
    let mut lchuv = *self;
    lchuv.decrement_hue(amount);
    lchuv
  }

  /// Returns a new color with hue increased by the given amount in degrees.
  pub fn with_hue_incremented_by(&self, amount: impl Into<Component>) -> Self {
    let mut lchuv = *self;
    lchuv.increment_hue(amount);
    lchuv
  }

  /// Returns a new color with hue scaled by the given factor.
  pub fn with_hue_scaled_by(&self, factor: impl Into<Component>) -> Self {
    let mut lchuv = *self;
    lchuv.scale_hue(factor);
    lchuv
  }

  /// Returns a new color with the given L\* value.
  pub fn with_l(&self, l: impl Into<Component>) -> Self {
    Self {
      l: l.into(),
      ..*self
    }
  }

  /// Returns a new color with L\* decreased by the given amount.
  pub fn with_l_decremented_by(&self, amount: impl Into<Component>) -> Self {
    let mut lchuv = *self;
    lchuv.decrement_l(amount);
    lchuv
  }

  /// Returns a new color with L\* increased by the given amount.
  pub fn with_l_incremented_by(&self, amount: impl Into<Component>) -> Self {
    let mut lchuv = *self;
    lchuv.increment_l(amount);
    lchuv
  }

  /// Returns a new color with L\* scaled by the given factor.
  pub fn with_l_scaled_by(&self, factor: impl Into<Component>) -> Self {
    let mut lchuv = *self;
    lchuv.scale_l(factor);
    lchuv
  }
}

impl<T> Add<T> for Lchuv
where
  T: Into<Self>,
{
  type Output = Self;

  fn add(self, rhs: T) -> Self::Output {
    Self::from(self.to_rgb::<Srgb>() + rhs.into().to_rgb::<Srgb>())
  }
}

impl ColorSpace<3> for Lchuv {
  fn alpha(&self) -> f64 {
    self.alpha.0
  }

  fn components(&self) -> [f64; 3] {
    self.components()
  }

  fn set_alpha(&mut self, alpha: impl Into<Component>) {
    self.alpha = alpha.into().clamp(0.0, 1.0);
  }

  fn set_components(&mut self, components: [impl Into<Component> + Clone; 3]) {
    self.set_components(components)
  }

  #[cfg(feature = "space-hsluv")]
  fn to_hsluv(&self) -> Hsluv {
    self.to_hsluv()
  }

  #[cfg(feature = "space-hpluv")]
  fn to_hpluv(&self) -> Hpluv {
    self.to_hpluv()
  }

  fn to_xyz(&self) -> Xyz {
    self.to_xyz()
  }
}

#[cfg(feature = "serde")]
impl<'de> serde::Deserialize<'de> for Lchuv {
  fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
    #[derive(serde::Deserialize)]
    struct LchuvData {
      l: Component,
      c: Component,
      h: Component,
      #[serde(default = "crate::component::default_alpha")]
      alpha: Component,
    }

    let data = LchuvData::deserialize(deserializer)?;
    Ok(Self {
      l: data.l,
      c: data.c,
      h: data.h,
      alpha: data.alpha,
      context: Self::DEFAULT_CONTEXT,
    })
  }
}

impl Display for Lchuv {
  fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
    let precision = f.precision().unwrap_or(4);
    if self.alpha.0 < 1.0 {
      write!(
        f,
        "LCh(uv)({:.precision$}, {:.precision$}, {:.precision$}°, {:.0}%)",
        self.l,
        self.c,
        self.hue(),
        self.opacity()
      )
    } else {
      write!(
        f,
        "LCh(uv)({:.precision$}, {:.precision$}, {:.precision$}°)",
        self.l,
        self.c,
        self.hue()
      )
    }
  }
}

impl<T> Div<T> for Lchuv
where
  T: Into<Self>,
{
  type Output = Self;

  fn div(self, rhs: T) -> Self::Output {
    Self::from(self.to_rgb::<Srgb>() / rhs.into().to_rgb::<Srgb>())
  }
}

impl<T> From<[T; 3]> for Lchuv
where
  T: Into<Component>,
{
  fn from([l, c, h]: [T; 3]) -> Self {
    Self::new(l, c, h)
  }
}

#[cfg(feature = "space-cmy")]
impl<S> From<Cmy<S>> for Lchuv
where
  S: RgbSpec,
{
  fn from(cmy: Cmy<S>) -> Self {
    cmy.to_lchuv()
  }
}

#[cfg(feature = "space-cmyk")]
impl<S> From<Cmyk<S>> for Lchuv
where
  S: RgbSpec,
{
  fn from(cmyk: Cmyk<S>) -> Self {
    cmyk.to_lchuv()
  }
}

#[cfg(feature = "space-hsi")]
impl<S> From<Hsi<S>> for Lchuv
where
  S: RgbSpec,
{
  fn from(hsi: Hsi<S>) -> Self {
    hsi.to_lchuv()
  }
}

#[cfg(feature = "space-hsl")]
impl<S> From<Hsl<S>> for Lchuv
where
  S: RgbSpec,
{
  fn from(hsl: Hsl<S>) -> Self {
    hsl.to_lchuv()
  }
}

#[cfg(feature = "space-hsluv")]
impl From<Hsluv> for Lchuv {
  fn from(hsluv: Hsluv) -> Self {
    hsluv.to_lchuv()
  }
}

#[cfg(feature = "space-hpluv")]
impl From<Hpluv> for Lchuv {
  fn from(hpluv: Hpluv) -> Self {
    hpluv.to_lchuv()
  }
}

#[cfg(feature = "space-hsv")]
impl<S> From<Hsv<S>> for Lchuv
where
  S: RgbSpec,
{
  fn from(hsv: Hsv<S>) -> Self {
    hsv.to_lchuv()
  }
}

#[cfg(feature = "space-hwb")]
impl<S> From<Hwb<S>> for Lchuv
where
  S: RgbSpec,
{
  fn from(hwb: Hwb<S>) -> Self {
    hwb.to_lchuv()
  }
}

#[cfg(feature = "space-lab")]
impl From<Lab> for Lchuv {
  fn from(lab: Lab) -> Self {
    lab.to_lchuv()
  }
}

#[cfg(feature = "space-lch")]
impl From<Lch> for Lchuv {
  fn from(lch: Lch) -> Self {
    lch.to_lchuv()
  }
}

impl From<Lms> for Lchuv {
  fn from(lms: Lms) -> Self {
    lms.to_lchuv()
  }
}

impl From<Luv> for Lchuv {
  fn from(luv: Luv) -> Self {
    luv.to_lchuv()
  }
}

#[cfg(feature = "space-okhsl")]
impl From<Okhsl> for Lchuv {
  fn from(okhsl: Okhsl) -> Self {
    okhsl.to_lchuv()
  }
}

#[cfg(feature = "space-okhsv")]
impl From<Okhsv> for Lchuv {
  fn from(okhsv: Okhsv) -> Self {
    okhsv.to_lchuv()
  }
}

#[cfg(feature = "space-okhwb")]
impl From<Okhwb> for Lchuv {
  fn from(okhwb: Okhwb) -> Self {
    okhwb.to_lchuv()
  }
}

#[cfg(feature = "space-oklab")]
impl From<Oklab> for Lchuv {
  fn from(oklab: Oklab) -> Self {
    oklab.to_lchuv()
  }
}

#[cfg(feature = "space-oklch")]
impl From<Oklch> for Lchuv {
  fn from(oklch: Oklch) -> Self {
    oklch.to_lchuv()
  }
}

impl<S> From<Rgb<S>> for Lchuv
where
  S: RgbSpec,
{
  fn from(rgb: Rgb<S>) -> Self {
    rgb.to_lchuv()
  }
}

#[cfg(feature = "space-xyy")]
impl From<Xyy> for Lchuv {
  fn from(xyy: Xyy) -> Self {
    xyy.to_lchuv()
  }
}

impl From<Xyz> for Lchuv {
  fn from(xyz: Xyz) -> Self {
    xyz.to_lchuv()
  }
}

impl<T> Mul<T> for Lchuv
where
  T: Into<Self>,
{
  type Output = Self;

  fn mul(self, rhs: T) -> Self::Output {
    Self::from(self.to_rgb::<Srgb>() * rhs.into().to_rgb::<Srgb>())
  }
}

impl<T> PartialEq<T> for Lchuv
where
  T: Into<Lchuv> + Copy,
{
  fn eq(&self, other: &T) -> bool {
    let other = (*other).into();
    self.alpha == other.alpha && self.l == other.l && self.c == other.c && self.h == other.h
  }
}

#[cfg(feature = "serde")]
impl serde::Serialize for Lchuv {
  fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
    use serde::ser::SerializeStruct;

    let field_count = if self.alpha.0 < 1.0 { 4 } else { 3 };
    let mut state = serializer.serialize_struct("Lchuv", field_count)?;
    state.serialize_field("l", &self.l)?;
    state.serialize_field("c", &self.c)?;
    state.serialize_field("h", &self.h)?;
    if self.alpha.0 < 1.0 {
      state.serialize_field("alpha", &self.alpha)?;
    }
    state.end()
  }
}

impl<T> Sub<T> for Lchuv
where
  T: Into<Self>,
{
  type Output = Self;

  fn sub(self, rhs: T) -> Self::Output {
    Self::from(self.to_rgb::<Srgb>() - rhs.into().to_rgb::<Srgb>())
  }
}

impl TryFrom<&str> for Lchuv {
  type Error = crate::Error;

  fn try_from(value: &str) -> Result<Self, Self::Error> {
    Ok(Self::from(Rgb::<Srgb>::try_from(value)?.to_xyz()))
  }
}

impl TryFrom<String> for Lchuv {
  type Error = crate::Error;

  fn try_from(value: String) -> Result<Self, Self::Error> {
    Ok(Self::from(Rgb::<Srgb>::try_from(value)?.to_xyz()))
  }
}

/// Interpolates hue along the shortest arc with achromatic handling.
///
/// When either color is achromatic (chroma below [`ACHROMATIC_THRESHOLD`]), its hue is
/// treated as "powerless" and the other color's hue is used. When both are achromatic,
/// hue is 0. This follows the CSS Color Level 4 specification for hue interpolation.
fn mix_hue(h1: f64, c1: f64, h2: f64, c2: f64, t: f64) -> f64 {
  let achromatic1 = c1 < ACHROMATIC_THRESHOLD;
  let achromatic2 = c2 < ACHROMATIC_THRESHOLD;

  if achromatic1 && achromatic2 {
    return 0.0;
  }
  if achromatic1 {
    return h2;
  }
  if achromatic2 {
    return h1;
  }

  let mut diff = h2 - h1;
  if diff > 180.0 {
    diff -= 360.0;
  } else if diff < -180.0 {
    diff += 360.0;
  }

  (h1 + diff * t).rem_euclid(360.0)
}

/// Computes the boundary lines of the sRGB gamut at lightness `l` in the CIE LCh(uv) plane.
///
/// Returns 6 `(slope, intercept)` pairs (one per RGB channel boundary × {0, 1}).
#[cfg(any(feature = "space-hsluv", feature = "space-hpluv"))]
fn get_bounds(l: f64) -> [(f64, f64); 6] {
  use crate::space::rgb::RgbSpec;

  let m = *Srgb::inversed_xyz_matrix();
  let sub1 = (l + L_STAR_OFFSET).powi(3) / L_STAR_SCALE_CUBED;
  let sub2 = if sub1 > EPSILON { sub1 } else { l / KAPPA };

  let top1_all = m * [SLOPE_COEFFS[0] * sub2, SLOPE_COEFFS[1] * sub2, SLOPE_COEFFS[2] * sub2];
  let top2_base = m
    * [
      INTERCEPT_COEFFS[0] * l * sub2,
      INTERCEPT_COEFFS[1] * l * sub2,
      INTERCEPT_COEFFS[2] * l * sub2,
    ];
  let bottom_base = m * [DENOM_COEFFS[0] * sub2, DENOM_COEFFS[1] * sub2, DENOM_COEFFS[2] * sub2];

  let mut bounds = [(0.0, 0.0); 6];
  let mut idx = 0;

  for channel in 0..3 {
    for t in [0.0_f64, 1.0] {
      let top2 = top2_base[channel] - INTERCEPT_COEFFS[1] * t * l;
      let bottom = bottom_base[channel] - DENOM_COEFFS[1] * t;
      bounds[idx] = (top1_all[channel] / bottom, top2 / bottom);
      idx += 1;
    }
  }

  bounds
}

/// Returns the maximum chroma achievable within the sRGB gamut at lightness `l` and hue `h` (degrees).
#[cfg(feature = "space-hsluv")]
pub(crate) fn max_safe_chroma_for_lh(l: f64, h: f64) -> f64 {
  let h_rad = h.to_radians();
  let sin_h = h_rad.sin();
  let cos_h = h_rad.cos();
  let mut min_length = f64::MAX;

  for (slope, intercept) in get_bounds(l) {
    let denom = sin_h - slope * cos_h;
    if denom.abs() < RAY_INTERSECTION_THRESHOLD {
      continue;
    }
    let length = intercept / denom;
    if length >= 0.0 {
      min_length = min_length.min(length);
    }
  }

  min_length
}

/// Returns the maximum chroma achievable within the sRGB gamut at lightness `l` across all hues.
///
/// This computes the perpendicular distance from the origin to each gamut boundary line,
/// returning the minimum — the radius of the largest circle centered at the origin that
/// fits entirely within the sRGB gamut at the given lightness.
#[cfg(feature = "space-hpluv")]
pub(crate) fn max_safe_chroma_for_l(l: f64) -> f64 {
  let mut min_length = f64::MAX;

  for (slope, intercept) in get_bounds(l) {
    let length = intercept.abs() / (slope * slope + 1.0).sqrt();
    if length >= 0.0 {
      min_length = min_length.min(length);
    }
  }

  min_length
}

#[cfg(test)]
mod test {
  use super::*;

  mod add {
    use super::*;

    #[test]
    fn it_adds_two_lchuv_colors() {
      let a = Lchuv::new(50.0, 30.0, 180.0);
      let b = Lchuv::new(30.0, 20.0, 90.0);
      let result = a + b;

      assert!(result.l() > 0.0);
    }
  }

  mod adapt_to {
    use super::*;
    use crate::{Illuminant, illuminant::IlluminantType, spectral::Spd};

    static TEST_SPD_A: &[(u32, f64)] = &[
      (380, 9.80),
      (400, 14.71),
      (420, 20.99),
      (440, 28.70),
      (460, 37.81),
      (480, 48.24),
      (500, 59.86),
      (520, 72.50),
      (540, 85.95),
      (560, 100.00),
      (580, 114.44),
      (600, 129.04),
      (620, 143.62),
      (640, 157.98),
      (660, 171.96),
      (680, 185.43),
      (700, 198.26),
      (720, 210.36),
      (740, 221.67),
      (760, 232.12),
      (780, 241.68),
    ];

    #[test]
    fn it_returns_same_values_when_white_points_match() {
      let lchuv = Lchuv::new(50.0, 30.0, 180.0);
      let same_context = Lchuv::DEFAULT_CONTEXT;
      let adapted = lchuv.adapt_to(same_context);

      assert!((adapted.l() - lchuv.l()).abs() < 1e-10);
      assert!((adapted.c() - lchuv.c()).abs() < 1e-10);
      assert!((adapted.h() - lchuv.h()).abs() < 1e-10);
    }

    #[test]
    fn it_changes_values_for_non_d65_source() {
      let illuminant_a = Illuminant::new("Test A", IlluminantType::Custom, Spd::new(TEST_SPD_A));
      let context_a = ColorimetricContext::new().with_illuminant(illuminant_a);
      let lchuv = Lchuv::new(50.0, 30.0, 180.0).with_context(context_a);
      let adapted = lchuv.adapt_to(Lchuv::DEFAULT_CONTEXT);

      assert!(
        (adapted.l() - lchuv.l()).abs() > 0.01
          || (adapted.c() - lchuv.c()).abs() > 0.01
          || (adapted.h() - lchuv.h()).abs() > 0.001
      );
    }

    #[test]
    fn it_preserves_alpha() {
      let illuminant_a = Illuminant::new("Test A", IlluminantType::Custom, Spd::new(TEST_SPD_A));
      let target_context = ColorimetricContext::new().with_illuminant(illuminant_a);
      let lchuv = Lchuv::new(50.0, 30.0, 180.0).with_alpha(0.5);
      let adapted = lchuv.adapt_to(target_context);

      assert!((adapted.alpha() - 0.5).abs() < 1e-10);
    }
  }

  mod c {
    use super::*;

    #[test]
    fn it_returns_c_component() {
      let lchuv = Lchuv::new(50.0, 30.0, 180.0);

      assert!((lchuv.c() - 30.0).abs() < 1e-10);
    }
  }

  mod chroma {
    use super::*;

    #[test]
    fn it_returns_chroma_as_alias() {
      let lchuv = Lchuv::new(50.0, 30.0, 180.0);

      assert!((lchuv.chroma() - 30.0).abs() < 1e-10);
    }
  }

  mod components {
    use pretty_assertions::assert_eq;

    use super::*;

    #[test]
    fn it_returns_components_as_array() {
      let lchuv = Lchuv::new(50.0, 30.0, 180.0);
      let [l, c, h] = lchuv.components();

      assert_eq!(l, 50.0);
      assert_eq!(c, 30.0);
      assert_eq!(h, 0.5);
    }
  }

  mod decrement_c {
    use super::*;

    #[test]
    fn it_decreases_c_component() {
      let mut lchuv = Lchuv::new(50.0, 30.0, 180.0);
      lchuv.decrement_c(10.0);

      assert!((lchuv.c() - 20.0).abs() < 1e-10);
    }
  }

  mod decrement_h {
    use super::*;

    #[test]
    fn it_decreases_h_with_wrapping() {
      let mut lchuv = Lchuv::new(50.0, 30.0, 36.0);
      lchuv.decrement_h(0.2);

      assert!((lchuv.h() - 0.9).abs() < 1e-10);
    }
  }

  mod decrement_hue {
    use super::*;

    #[test]
    fn it_decreases_hue_in_degrees() {
      let mut lchuv = Lchuv::new(50.0, 30.0, 180.0);
      lchuv.decrement_hue(90.0);

      assert!((lchuv.hue() - 90.0).abs() < 1e-10);
    }
  }

  mod decrement_l {
    use super::*;

    #[test]
    fn it_decreases_l_component() {
      let mut lchuv = Lchuv::new(50.0, 30.0, 180.0);
      lchuv.decrement_l(10.0);

      assert!((lchuv.l() - 40.0).abs() < 1e-10);
    }
  }

  mod display {
    use pretty_assertions::assert_eq;

    use super::*;

    #[test]
    fn it_formats_with_default_precision() {
      let lchuv = Lchuv::new(50.0, 30.0, 180.0);

      assert_eq!(format!("{}", lchuv), "LCh(uv)(50.0000, 30.0000, 180.0000°)");
    }

    #[test]
    fn it_formats_with_custom_precision() {
      let lchuv = Lchuv::new(50.0, 30.0, 180.0);

      assert_eq!(format!("{:.2}", lchuv), "LCh(uv)(50.00, 30.00, 180.00°)");
    }

    #[test]
    fn it_includes_opacity_when_alpha_below_one() {
      let lchuv = Lchuv::new(50.0, 30.0, 180.0).with_alpha(0.5);

      assert_eq!(format!("{}", lchuv), "LCh(uv)(50.0000, 30.0000, 180.0000°, 50%)");
    }

    #[test]
    fn it_omits_opacity_when_fully_opaque() {
      let lchuv = Lchuv::new(50.0, 30.0, 180.0);

      assert_eq!(format!("{}", lchuv), "LCh(uv)(50.0000, 30.0000, 180.0000°)");
    }
  }

  mod from_array {
    use super::*;

    #[test]
    fn it_creates_from_f64_array() {
      let lchuv = Lchuv::from([50.0, 30.0, 180.0]);

      assert!((lchuv.l() - 50.0).abs() < 1e-10);
      assert!((lchuv.c() - 30.0).abs() < 1e-10);
      assert!((lchuv.hue() - 180.0).abs() < 1e-10);
    }
  }

  mod from_luv {
    use super::*;

    #[test]
    fn it_converts_from_luv() {
      let luv = Luv::new(50.0, 0.0, 30.0);
      let lchuv = Lchuv::from(luv);

      assert!((lchuv.l() - 50.0).abs() < 1e-10);
      assert!((lchuv.c() - 30.0).abs() < 1e-10);
      assert!((lchuv.hue() - 90.0).abs() < 1e-10);
    }

    #[test]
    fn it_preserves_alpha() {
      let luv = Luv::new(50.0, 20.0, -30.0).with_alpha(0.5);
      let lchuv = Lchuv::from(luv);

      assert!((lchuv.alpha() - 0.5).abs() < 1e-10);
    }
  }

  mod from_rgb {
    use super::*;

    #[test]
    fn it_converts_white_correctly() {
      let rgb = Rgb::<Srgb>::new(255, 255, 255);
      let lchuv = Lchuv::from(rgb);

      assert!((lchuv.l() - 100.0).abs() < 0.01);
      assert!(lchuv.c().abs() < 0.01);
    }

    #[test]
    fn it_converts_black_correctly() {
      let rgb = Rgb::<Srgb>::new(0, 0, 0);
      let lchuv = Lchuv::from(rgb);

      assert!(lchuv.l().abs() < 1e-10);
      assert!(lchuv.c().abs() < 1e-10);
    }

    #[test]
    fn it_preserves_alpha() {
      let rgb = Rgb::<Srgb>::new(128, 64, 32).with_alpha(0.5);
      let lchuv = Lchuv::from(rgb);

      assert!((lchuv.alpha() - 0.5).abs() < 1e-10);
    }
  }

  mod from_xyz {
    use super::*;

    #[test]
    fn it_converts_from_xyz() {
      let xyz = Xyz::new(0.5, 0.5, 0.5);
      let lchuv = Lchuv::from(xyz);

      assert!(lchuv.l() > 0.0);
    }

    #[test]
    fn it_preserves_alpha() {
      let xyz = Xyz::new(0.5, 0.5, 0.5).with_alpha(0.3);
      let lchuv = Lchuv::from(xyz);

      assert!((lchuv.alpha() - 0.3).abs() < 1e-10);
    }
  }

  mod gradient {
    use super::*;

    #[test]
    fn zero_steps_is_empty() {
      let c1 = Lchuv::new(50.0, 30.0, 180.0);
      let c2 = Lchuv::new(80.0, 10.0, 90.0);
      assert!(c1.gradient(c2.to_xyz(), 0).is_empty());
    }

    #[test]
    fn one_step_returns_self() {
      let c1 = Lchuv::new(50.0, 30.0, 180.0);
      let c2 = Lchuv::new(80.0, 10.0, 90.0);
      let steps = c1.gradient(c2.to_xyz(), 1);
      assert_eq!(steps.len(), 1);
      assert!((steps[0].l() - c1.l()).abs() < 1e-4);
    }

    #[test]
    fn two_steps_returns_endpoints() {
      let c1 = Lchuv::new(50.0, 30.0, 180.0);
      let c2 = Lchuv::new(80.0, 10.0, 90.0);
      let steps = c1.gradient(c2.to_xyz(), 2);
      assert_eq!(steps.len(), 2);
      assert!((steps[0].l() - c1.l()).abs() < 1e-4);
      assert!((steps[1].l() - c2.l()).abs() < 1e-4);
    }

    #[test]
    fn five_steps_correct_count() {
      let c1 = Lchuv::new(20.0, 10.0, 0.0);
      let c2 = Lchuv::new(90.0, 5.0, 180.0);
      assert_eq!(c1.gradient(c2.to_xyz(), 5).len(), 5);
    }

    #[test]
    fn monotonic_lightness_dark_to_light() {
      let dark = Lchuv::new(10.0, 0.0, 0.0);
      let light = Lchuv::new(90.0, 0.0, 0.0);
      let steps = dark.gradient(light.to_xyz(), 5);
      let lightnesses: Vec<f64> = steps.iter().map(|c| c.l()).collect();
      for i in 1..lightnesses.len() {
        assert!(
          lightnesses[i] >= lightnesses[i - 1],
          "Lightness should be monotonically increasing: {lightnesses:?}"
        );
      }
    }
  }

  mod h {
    use super::*;

    #[test]
    fn it_returns_normalized_hue() {
      let lchuv = Lchuv::new(50.0, 30.0, 180.0);

      assert!((lchuv.h() - 0.5).abs() < 1e-10);
    }
  }

  mod hue {
    use super::*;

    #[test]
    fn it_returns_hue_in_degrees() {
      let lchuv = Lchuv::new(50.0, 30.0, 180.0);

      assert!((lchuv.hue() - 180.0).abs() < 1e-10);
    }
  }

  mod increment_c {
    use super::*;

    #[test]
    fn it_increases_c_component() {
      let mut lchuv = Lchuv::new(50.0, 30.0, 180.0);
      lchuv.increment_c(10.0);

      assert!((lchuv.c() - 40.0).abs() < 1e-10);
    }
  }

  mod increment_h {
    use super::*;

    #[test]
    fn it_increases_h_with_wrapping() {
      let mut lchuv = Lchuv::new(50.0, 30.0, 324.0);
      lchuv.increment_h(0.2);

      assert!((lchuv.h() - 0.1).abs() < 1e-10);
    }
  }

  mod increment_hue {
    use super::*;

    #[test]
    fn it_increases_hue_in_degrees() {
      let mut lchuv = Lchuv::new(50.0, 30.0, 180.0);
      lchuv.increment_hue(90.0);

      assert!((lchuv.hue() - 270.0).abs() < 1e-10);
    }
  }

  mod increment_l {
    use super::*;

    #[test]
    fn it_increases_l_component() {
      let mut lchuv = Lchuv::new(50.0, 30.0, 180.0);
      lchuv.increment_l(10.0);

      assert!((lchuv.l() - 60.0).abs() < 1e-10);
    }
  }

  mod l {
    use super::*;

    #[test]
    fn it_returns_l_component() {
      let lchuv = Lchuv::new(50.0, 30.0, 180.0);

      assert!((lchuv.l() - 50.0).abs() < 1e-10);
    }
  }

  mod mix {
    use super::*;

    const EPSILON: f64 = 1e-4;

    #[test]
    fn at_zero_returns_self() {
      let c1 = Lchuv::new(60.0, 40.0, 30.0);
      let c2 = Lchuv::new(40.0, 20.0, 270.0);
      let result = c1.mix(c2.to_xyz(), 0.0);
      assert!((result.l() - c1.l()).abs() < EPSILON);
      assert!((result.c() - c1.c()).abs() < EPSILON);
    }

    #[test]
    fn at_one_returns_other() {
      let c1 = Lchuv::new(60.0, 40.0, 30.0);
      let c2 = Lchuv::new(40.0, 20.0, 270.0);
      let result = c1.mix(c2.to_xyz(), 1.0);
      assert!((result.l() - c2.l()).abs() < EPSILON);
      assert!((result.c() - c2.c()).abs() < EPSILON);
    }

    #[test]
    fn midpoint_is_between() {
      let c1 = Lchuv::new(20.0, 0.0, 0.0);
      let c2 = Lchuv::new(80.0, 0.0, 0.0);
      let mid = c1.mix(c2.to_xyz(), 0.5);
      assert!(mid.l() > 30.0 && mid.l() < 70.0);
    }

    #[test]
    fn alpha_interpolation() {
      let c1 = Lchuv::new(50.0, 10.0, 180.0).with_alpha(0.0);
      let c2 = Lchuv::new(50.0, 10.0, 180.0).with_alpha(1.0);
      let mid = c1.mix(c2.to_xyz(), 0.5);
      assert!((mid.alpha() - 0.5).abs() < EPSILON);
    }

    #[test]
    fn extrapolation() {
      let c1 = Lchuv::new(20.0, 0.0, 0.0);
      let c2 = Lchuv::new(80.0, 0.0, 0.0);
      let beyond = c1.mix(c2.to_xyz(), 1.5);
      assert!(beyond.l() > c2.l());
    }

    #[test]
    fn cross_type() {
      let lchuv = Lchuv::new(60.0, 40.0, 30.0);
      let xyz = Xyz::new(0.18048, 0.07219, 0.95030);
      let _result = lchuv.mix(xyz, 0.5);
    }

    #[test]
    fn shortest_arc_hue() {
      let c1 = Lchuv::new(60.0, 40.0, 350.0);
      let c2 = Lchuv::new(60.0, 40.0, 10.0);
      let mid = c1.mix(c2.to_xyz(), 0.5);
      let hue = mid.hue();
      assert!(hue < 20.0 || hue > 340.0, "Hue {hue} should be near 0°/360°");
    }

    #[test]
    fn both_achromatic() {
      let c1 = Lchuv::new(20.0, 0.0, 0.0);
      let c2 = Lchuv::new(80.0, 0.0, 0.0);
      let mid = c1.mix(c2.to_xyz(), 0.5);
      assert!(mid.c() < 0.01);
    }

    #[test]
    fn one_achromatic() {
      let grey = Lchuv::new(50.0, 0.0, 0.0);
      let red = Lchuv::new(60.0, 40.0, 30.0);
      let result = grey.mix(red.to_xyz(), 0.5);
      let result_hue = result.hue();
      assert!((result_hue - 30.0).abs() < 5.0);
    }
  }

  mod mix_hue_fn {
    use super::super::mix_hue;

    const EPSILON: f64 = 1e-6;

    #[test]
    fn shortest_arc_forward() {
      let h = mix_hue(10.0, 0.1, 50.0, 0.1, 0.5);
      assert!((h - 30.0).abs() < EPSILON);
    }

    #[test]
    fn shortest_arc_crosses_zero() {
      let h = mix_hue(350.0, 0.1, 10.0, 0.1, 0.5);
      assert!((h - 0.0).abs() < EPSILON);
    }

    #[test]
    fn shortest_arc_backward() {
      let h = mix_hue(10.0, 0.1, 350.0, 0.1, 0.5);
      assert!((h - 0.0).abs() < EPSILON);
    }

    #[test]
    fn both_achromatic() {
      let h = mix_hue(90.0, 0.0, 270.0, 0.0, 0.5);
      assert!((h - 0.0).abs() < EPSILON);
    }

    #[test]
    fn first_achromatic_uses_second() {
      let h = mix_hue(90.0, 0.0, 200.0, 0.1, 0.5);
      assert!((h - 200.0).abs() < EPSILON);
    }

    #[test]
    fn second_achromatic_uses_first() {
      let h = mix_hue(90.0, 0.1, 200.0, 0.0, 0.5);
      assert!((h - 90.0).abs() < EPSILON);
    }

    #[test]
    fn same_hemisphere() {
      let h = mix_hue(100.0, 0.1, 140.0, 0.1, 0.5);
      assert!((h - 120.0).abs() < EPSILON);
    }
  }

  mod mixed_with {
    use super::*;

    #[test]
    fn it_mutates_in_place() {
      let c1 = Lchuv::new(60.0, 40.0, 30.0);
      let c2 = Lchuv::new(40.0, 20.0, 270.0);
      let expected = c1.mix(c2.to_xyz(), 0.5);
      let mut color = c1;
      color.mixed_with(c2.to_xyz(), 0.5);
      assert!((color.l() - expected.l()).abs() < 1e-10);
      assert!((color.c() - expected.c()).abs() < 1e-10);
      assert!((color.h() - expected.h()).abs() < 1e-10);
      assert!((color.alpha() - expected.alpha()).abs() < 1e-10);
    }
  }

  mod new {
    use super::*;

    #[test]
    fn it_creates_with_default_alpha() {
      let lchuv = Lchuv::new(50.0, 30.0, 180.0);

      assert!((lchuv.alpha() - 1.0).abs() < 1e-10);
    }

    #[test]
    fn it_creates_with_default_context() {
      let lchuv = Lchuv::new(50.0, 30.0, 180.0);

      assert_eq!(lchuv.context().illuminant().name(), "D65");
    }

    #[test]
    fn it_normalizes_hue_to_zero_one() {
      let lchuv = Lchuv::new(50.0, 30.0, 450.0);

      assert!((lchuv.h() - 0.25).abs() < 1e-10);
    }

    #[test]
    fn it_normalizes_negative_hue() {
      let lchuv = Lchuv::new(50.0, 30.0, -90.0);

      assert!((lchuv.h() - 0.75).abs() < 1e-10);
    }
  }

  mod partial_eq {
    use super::*;

    #[test]
    fn it_compares_equal_colors() {
      let a = Lchuv::new(50.0, 30.0, 180.0);
      let b = Lchuv::new(50.0, 30.0, 180.0);

      assert!(a == b);
    }

    #[test]
    fn it_compares_unequal_colors() {
      let a = Lchuv::new(50.0, 30.0, 180.0);
      let b = Lchuv::new(60.0, 30.0, 180.0);

      assert!(a != b);
    }
  }

  mod scale_c {
    use super::*;

    #[test]
    fn it_scales_c_component() {
      let mut lchuv = Lchuv::new(50.0, 30.0, 180.0);
      lchuv.scale_c(2.0);

      assert!((lchuv.c() - 60.0).abs() < 1e-10);
    }
  }

  mod scale_h {
    use super::*;

    #[test]
    fn it_scales_h_with_wrapping() {
      let mut lchuv = Lchuv::new(50.0, 30.0, 270.0);
      lchuv.scale_h(2.0);

      assert!((lchuv.h() - 0.5).abs() < 1e-10);
    }
  }

  mod scale_l {
    use super::*;

    #[test]
    fn it_scales_l_component() {
      let mut lchuv = Lchuv::new(50.0, 30.0, 180.0);
      lchuv.scale_l(2.0);

      assert!((lchuv.l() - 100.0).abs() < 1e-10);
    }
  }

  mod to_luv {
    use super::*;

    #[test]
    fn it_converts_to_luv() {
      let lchuv = Lchuv::new(50.0, 30.0, 90.0);
      let luv = lchuv.to_luv();

      assert!((luv.l() - 50.0).abs() < 1e-10);
      assert!(luv.u().abs() < 1e-10);
      assert!((luv.v() - 30.0).abs() < 1e-10);
    }

    #[test]
    fn it_roundtrips_through_luv() {
      let original = Lchuv::new(50.0, 30.0, 180.0);
      let roundtrip = Lchuv::from(original.to_luv());

      assert!((original.l() - roundtrip.l()).abs() < 1e-10);
      assert!((original.c() - roundtrip.c()).abs() < 1e-10);
      assert!((original.h() - roundtrip.h()).abs() < 1e-10);
    }

    #[test]
    fn it_preserves_alpha() {
      let lchuv = Lchuv::new(50.0, 30.0, 180.0).with_alpha(0.7);
      let luv = lchuv.to_luv();

      assert!((luv.alpha() - 0.7).abs() < 1e-10);
    }

    #[test]
    fn it_preserves_context() {
      let lchuv = Lchuv::new(50.0, 30.0, 180.0);
      let luv = lchuv.to_luv();

      assert_eq!(luv.context().illuminant().name(), "D65");
    }
  }

  mod to_rgb {
    use super::*;

    #[test]
    fn it_converts_to_srgb() {
      let lchuv = Lchuv::new(50.0, 0.0, 0.0);
      let rgb = lchuv.to_rgb::<Srgb>();

      assert!(rgb.red() > 0);
    }

    #[test]
    fn it_roundtrips_through_rgb() {
      let original = Lchuv::from(Rgb::<Srgb>::new(128, 64, 200));
      let roundtrip = Lchuv::from(original.to_rgb::<Srgb>());

      assert!((original.l() - roundtrip.l()).abs() < 0.5);
      assert!((original.c() - roundtrip.c()).abs() < 0.5);
      assert!((original.h() - roundtrip.h()).abs() < 0.01);
    }

    #[test]
    fn it_preserves_alpha() {
      let lchuv = Lchuv::new(50.0, 30.0, 180.0).with_alpha(0.7);
      let rgb = lchuv.to_rgb::<Srgb>();

      assert!((rgb.alpha() - 0.7).abs() < 1e-10);
    }
  }

  mod to_xyz {
    use super::*;

    #[test]
    fn it_converts_to_xyz() {
      let lchuv = Lchuv::new(50.0, 0.0, 0.0);
      let xyz = lchuv.to_xyz();

      assert!(xyz.y() > 0.0);
    }

    #[test]
    fn it_roundtrips_through_xyz() {
      let original = Lchuv::new(50.0, 30.0, 180.0);
      let roundtrip = Lchuv::from(original.to_xyz());

      assert!((original.l() - roundtrip.l()).abs() < 1e-10);
      assert!((original.c() - roundtrip.c()).abs() < 1e-10);
      assert!((original.h() - roundtrip.h()).abs() < 1e-10);
    }

    #[test]
    fn it_preserves_alpha() {
      let lchuv = Lchuv::new(50.0, 30.0, 180.0).with_alpha(0.3);
      let xyz = lchuv.to_xyz();

      assert!((xyz.alpha() - 0.3).abs() < 1e-10);
    }
  }

  mod try_from_str {
    use super::*;

    #[test]
    fn it_parses_hex_string() {
      let lchuv = Lchuv::try_from("#FF5733").unwrap();

      assert!(lchuv.l() > 0.0);
    }

    #[test]
    fn it_returns_error_for_invalid_hex() {
      let result = Lchuv::try_from("not_a_color");

      assert!(result.is_err());
    }
  }

  mod with_alpha {
    use super::*;

    #[test]
    fn it_returns_new_color_with_alpha() {
      let lchuv = Lchuv::new(50.0, 30.0, 180.0);
      let result = lchuv.with_alpha(0.5);

      assert!((result.alpha() - 0.5).abs() < 1e-10);
      assert!((result.l() - 50.0).abs() < 1e-10);
    }
  }

  mod with_c {
    use super::*;

    #[test]
    fn it_returns_new_color_with_c() {
      let lchuv = Lchuv::new(50.0, 30.0, 180.0);
      let result = lchuv.with_c(40.0);

      assert!((result.c() - 40.0).abs() < 1e-10);
      assert!((result.l() - 50.0).abs() < 1e-10);
      assert!((result.h() - 0.5).abs() < 1e-10);
    }
  }

  mod with_c_decremented_by {
    use super::*;

    #[test]
    fn it_returns_new_color_with_c_decremented() {
      let lchuv = Lchuv::new(50.0, 30.0, 180.0);
      let result = lchuv.with_c_decremented_by(10.0);

      assert!((result.c() - 20.0).abs() < 1e-10);
    }
  }

  mod with_c_incremented_by {
    use super::*;

    #[test]
    fn it_returns_new_color_with_c_incremented() {
      let lchuv = Lchuv::new(50.0, 30.0, 180.0);
      let result = lchuv.with_c_incremented_by(10.0);

      assert!((result.c() - 40.0).abs() < 1e-10);
    }
  }

  mod with_c_scaled_by {
    use super::*;

    #[test]
    fn it_returns_new_color_with_c_scaled() {
      let lchuv = Lchuv::new(50.0, 30.0, 180.0);
      let result = lchuv.with_c_scaled_by(2.0);

      assert!((result.c() - 60.0).abs() < 1e-10);
    }
  }

  mod with_context {
    use super::*;

    #[test]
    fn it_returns_new_color_with_context() {
      let lchuv = Lchuv::new(50.0, 30.0, 180.0);
      let context = ColorimetricContext::default();
      let result = lchuv.with_context(context);

      assert!((result.l() - 50.0).abs() < 1e-10);
    }
  }

  mod with_h {
    use super::*;

    #[test]
    fn it_returns_new_color_with_h() {
      let lchuv = Lchuv::new(50.0, 30.0, 180.0);
      let result = lchuv.with_h(0.75);

      assert!((result.h() - 0.75).abs() < 1e-10);
      assert!((result.l() - 50.0).abs() < 1e-10);
      assert!((result.c() - 30.0).abs() < 1e-10);
    }
  }

  mod with_h_decremented_by {
    use super::*;

    #[test]
    fn it_returns_new_color_with_h_decremented() {
      let lchuv = Lchuv::new(50.0, 30.0, 36.0);
      let result = lchuv.with_h_decremented_by(0.2);

      assert!((result.h() - 0.9).abs() < 1e-10);
    }
  }

  mod with_h_incremented_by {
    use super::*;

    #[test]
    fn it_returns_new_color_with_h_incremented() {
      let lchuv = Lchuv::new(50.0, 30.0, 324.0);
      let result = lchuv.with_h_incremented_by(0.2);

      assert!((result.h() - 0.1).abs() < 1e-10);
    }
  }

  mod with_h_scaled_by {
    use super::*;

    #[test]
    fn it_returns_new_color_with_h_scaled() {
      let lchuv = Lchuv::new(50.0, 30.0, 270.0);
      let result = lchuv.with_h_scaled_by(2.0);

      assert!((result.h() - 0.5).abs() < 1e-10);
    }
  }

  mod with_hue {
    use super::*;

    #[test]
    fn it_returns_new_color_with_hue_in_degrees() {
      let lchuv = Lchuv::new(50.0, 30.0, 180.0);
      let result = lchuv.with_hue(270.0);

      assert!((result.hue() - 270.0).abs() < 1e-10);
    }
  }

  mod with_hue_decremented_by {
    use super::*;

    #[test]
    fn it_returns_new_color_with_hue_decremented() {
      let lchuv = Lchuv::new(50.0, 30.0, 180.0);
      let result = lchuv.with_hue_decremented_by(90.0);

      assert!((result.hue() - 90.0).abs() < 1e-10);
    }
  }

  mod with_hue_incremented_by {
    use super::*;

    #[test]
    fn it_returns_new_color_with_hue_incremented() {
      let lchuv = Lchuv::new(50.0, 30.0, 180.0);
      let result = lchuv.with_hue_incremented_by(90.0);

      assert!((result.hue() - 270.0).abs() < 1e-10);
    }
  }

  mod with_l {
    use super::*;

    #[test]
    fn it_returns_new_color_with_l() {
      let lchuv = Lchuv::new(50.0, 30.0, 180.0);
      let result = lchuv.with_l(80.0);

      assert!((result.l() - 80.0).abs() < 1e-10);
      assert!((result.c() - 30.0).abs() < 1e-10);
      assert!((result.h() - 0.5).abs() < 1e-10);
    }
  }

  mod with_l_decremented_by {
    use super::*;

    #[test]
    fn it_returns_new_color_with_l_decremented() {
      let lchuv = Lchuv::new(50.0, 30.0, 180.0);
      let result = lchuv.with_l_decremented_by(10.0);

      assert!((result.l() - 40.0).abs() < 1e-10);
    }
  }

  mod with_l_incremented_by {
    use super::*;

    #[test]
    fn it_returns_new_color_with_l_incremented() {
      let lchuv = Lchuv::new(50.0, 30.0, 180.0);
      let result = lchuv.with_l_incremented_by(10.0);

      assert!((result.l() - 60.0).abs() < 1e-10);
    }
  }

  mod with_l_scaled_by {
    use super::*;

    #[test]
    fn it_returns_new_color_with_l_scaled() {
      let lchuv = Lchuv::new(50.0, 30.0, 180.0);
      let result = lchuv.with_l_scaled_by(2.0);

      assert!((result.l() - 100.0).abs() < 1e-10);
    }
  }

  #[cfg(feature = "space-hsluv")]
  mod max_safe_chroma_for_lh {
    use super::super::max_safe_chroma_for_lh;

    #[test]
    fn it_returns_positive_chroma() {
      let max_c = max_safe_chroma_for_lh(50.0, 0.0);
      assert!(max_c > 0.0);
      assert!(max_c.is_finite());
    }

    #[test]
    fn it_returns_finite_for_all_hues() {
      for h in (0..360).step_by(15) {
        let max_c = max_safe_chroma_for_lh(50.0, h as f64);
        assert!(max_c > 0.0, "max_chroma should be positive at hue {h}");
        assert!(max_c.is_finite(), "max_chroma should be finite at hue {h}");
      }
    }

    #[test]
    fn it_returns_finite_for_all_lightness_values() {
      for l in (1..100).step_by(10) {
        let max_c = max_safe_chroma_for_lh(l as f64, 0.0);
        assert!(max_c > 0.0, "max_chroma should be positive at L={l}");
        assert!(max_c.is_finite(), "max_chroma should be finite at L={l}");
      }
    }
  }

  #[cfg(feature = "space-hpluv")]
  mod max_safe_chroma_for_l {
    use super::super::max_safe_chroma_for_l;

    #[test]
    fn it_returns_positive_chroma() {
      let max_c = max_safe_chroma_for_l(50.0);
      assert!(max_c > 0.0);
      assert!(max_c.is_finite());
    }

    #[test]
    fn it_returns_finite_for_all_lightness_values() {
      for l in (1..100).step_by(10) {
        let max_c = max_safe_chroma_for_l(l as f64);
        assert!(max_c > 0.0, "max_chroma should be positive at L={l}");
        assert!(max_c.is_finite(), "max_chroma should be finite at L={l}");
      }
    }

    #[test]
    fn it_returns_smaller_chroma_than_per_hue_maximum() {
      let max_c = max_safe_chroma_for_l(50.0);
      assert!(max_c > 0.0);
      assert!(max_c < 100.0);
    }
  }
}
