//! Generalized performance curve evaluation engine.
//!
//! Supports 9+ equation types (polynomial, exponential, logarithmic, Colburn,
//! power law, rational, effectiveness, bivariate polynomial) with bounded
//! validity checking. Used for vendor performance data and heat transfer
//! correlations evaluated uniformly across all components.

use crate::polynomials::{Polynomial1D, Polynomial2D};
use crate::ComponentError;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;

// ---------------------------------------------------------------------------
// Result types
// ---------------------------------------------------------------------------

/// Result of evaluating a curve at an operating point.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct CurveResult {
    /// Evaluated curve value.
    pub value: f64,
    /// Analytical derivative at the evaluation point.
    pub derivative: f64,
    /// Validity warning for the evaluation.
    pub warning: CurveWarning,
}

/// Warning about curve evaluation validity relative to bounds.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum CurveWarning {
    /// Operating point is within the curve's validity bounds.
    WithinBounds,
    /// Operating point is below the minimum validity bound.
    BelowMinBound,
    /// Operating point is above the maximum validity bound.
    AboveMaxBound,
    /// Evaluation produced NaN or infinite value (domain violation).
    DomainViolation,
}

impl CurveWarning {
    fn check(x: f64, bounds: Option<(f64, f64)>) -> Self {
        if x.is_nan() {
            return Self::DomainViolation;
        }
        if let Some((min, max)) = bounds {
            if x < min {
                return Self::BelowMinBound;
            }
            if x > max {
                return Self::AboveMaxBound;
            }
        }
        Self::WithinBounds
    }
}

// ---------------------------------------------------------------------------
// CurveEval trait
// ---------------------------------------------------------------------------

/// Trait for evaluating a performance curve at an operating point.
///
/// Implementations provide evaluation, analytical derivative (for Jacobian
/// computation), and coefficient validation.
pub trait CurveEval {
    /// Evaluates the curve at operating point `x`.
    fn evaluate(&self, x: f64) -> f64;
    /// Computes the analytical derivative at operating point `x`.
    fn derivative(&self, x: f64) -> f64;
    /// Validates that all coefficients are finite and well-formed.
    fn validate_coefficients(&self) -> Result<(), ComponentError>;
}

// ---------------------------------------------------------------------------
// Individual curve types
// ---------------------------------------------------------------------------

/// Polynomial curve: y = c0 + c1*x + c2*x² + ...
///
/// Wraps the existing [`Polynomial1D`] implementation (Horner's method).
#[derive(Debug, Clone, PartialEq)]
pub struct PolynomialCurve {
    inner: Polynomial1D,
}

impl PolynomialCurve {
    /// Creates a new polynomial curve from coefficients `[c0, c1, c2, ...]`.
    pub fn new(coefficients: Vec<f64>) -> Self {
        Self {
            inner: Polynomial1D::new(coefficients),
        }
    }
}

impl Serialize for PolynomialCurve {
    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
        self.inner.coefficients().serialize(serializer)
    }
}

impl<'de> Deserialize<'de> for PolynomialCurve {
    fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
        let coeffs = Vec::<f64>::deserialize(deserializer)?;
        Ok(Self {
            inner: Polynomial1D::new(coeffs),
        })
    }
}

impl CurveEval for PolynomialCurve {
    fn evaluate(&self, x: f64) -> f64 {
        self.inner.evaluate(x)
    }

    fn derivative(&self, x: f64) -> f64 {
        self.inner.derivative(x)
    }

    fn validate_coefficients(&self) -> Result<(), ComponentError> {
        self.inner.validate()
    }
}

/// Exponential curve: y = a * exp(b * x) + c
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ExponentialCurve {
    /// Scale factor.
    pub a: f64,
    /// Exponential rate.
    pub b: f64,
    /// Vertical offset.
    pub c: f64,
}

impl CurveEval for ExponentialCurve {
    fn evaluate(&self, x: f64) -> f64 {
        self.a * (self.b * x).exp() + self.c
    }

    fn derivative(&self, x: f64) -> f64 {
        self.a * self.b * (self.b * x).exp()
    }

    fn validate_coefficients(&self) -> Result<(), ComponentError> {
        validate_finite(&[self.a, self.b, self.c], "Exponential")
    }
}

/// Logarithmic curve: y = a * ln(x) + b
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct LogarithmicCurve {
    /// Scale factor.
    pub a: f64,
    /// Offset.
    pub b: f64,
}

impl CurveEval for LogarithmicCurve {
    fn evaluate(&self, x: f64) -> f64 {
        if x <= 0.0 {
            tracing::warn!(
                x,
                "LogarithmicCurve evaluate at non-positive x, returning NaN"
            );
            f64::NAN
        } else {
            self.a * x.ln() + self.b
        }
    }

    fn derivative(&self, x: f64) -> f64 {
        if x <= 0.0 {
            tracing::warn!(
                x,
                "LogarithmicCurve derivative at non-positive x, returning NaN"
            );
            f64::NAN
        } else {
            self.a / x
        }
    }

    fn validate_coefficients(&self) -> Result<(), ComponentError> {
        validate_finite(&[self.a, self.b], "Logarithmic")
    }
}

/// Inverse curve: y = a / (x + b) + c
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct InverseCurve {
    /// Numerator scale.
    pub a: f64,
    /// Denominator offset.
    pub b: f64,
    /// Vertical offset.
    pub c: f64,
}

impl CurveEval for InverseCurve {
    fn evaluate(&self, x: f64) -> f64 {
        self.a / (x + self.b) + self.c
    }

    fn derivative(&self, x: f64) -> f64 {
        -self.a / (x + self.b).powi(2)
    }

    fn validate_coefficients(&self) -> Result<(), ComponentError> {
        validate_finite(&[self.a, self.b, self.c], "Inverse")
    }
}

/// Colburn j-factor curve: y = a * x^b
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ColburnCurve {
    /// Scale factor.
    pub a: f64,
    /// Exponent.
    pub b: f64,
}

impl CurveEval for ColburnCurve {
    fn evaluate(&self, x: f64) -> f64 {
        self.a * x.powf(self.b)
    }

    fn derivative(&self, x: f64) -> f64 {
        self.a * self.b * x.powf(self.b - 1.0)
    }

    fn validate_coefficients(&self) -> Result<(), ComponentError> {
        validate_finite(&[self.a, self.b], "Colburn")
    }
}

/// Power law curve: y = a * x^b
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct PowerLawCurve {
    /// Scale factor.
    pub a: f64,
    /// Exponent.
    pub b: f64,
}

impl CurveEval for PowerLawCurve {
    fn evaluate(&self, x: f64) -> f64 {
        self.a * x.powf(self.b)
    }

    fn derivative(&self, x: f64) -> f64 {
        self.a * self.b * x.powf(self.b - 1.0)
    }

    fn validate_coefficients(&self) -> Result<(), ComponentError> {
        validate_finite(&[self.a, self.b], "PowerLaw")
    }
}

/// Rational curve: y = (a0 + a1*x) / (1 + b1*x + b2*x² + ...)
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct RationalCurve {
    /// Numerator coefficients `[a0, a1]`.
    pub a: [f64; 2],
    /// Denominator coefficients `[b1, b2, ...]` (constant term is always 1).
    pub b: Vec<f64>,
}

impl RationalCurve {
    fn denominator(&self, x: f64) -> f64 {
        1.0 + self
            .b
            .iter()
            .enumerate()
            .map(|(i, &bi)| bi * x.powi((i + 1) as i32))
            .sum::<f64>()
    }

    fn denominator_deriv(&self, x: f64) -> f64 {
        self.b
            .iter()
            .enumerate()
            .map(|(i, &bi)| bi * (i + 1) as f64 * x.powi(i as i32))
            .sum()
    }
}

impl CurveEval for RationalCurve {
    fn evaluate(&self, x: f64) -> f64 {
        (self.a[0] + self.a[1] * x) / self.denominator(x)
    }

    fn derivative(&self, x: f64) -> f64 {
        let u = self.a[0] + self.a[1] * x;
        let v = self.denominator(x);
        (self.a[1] * v - u * self.denominator_deriv(x)) / (v * v)
    }

    fn validate_coefficients(&self) -> Result<(), ComponentError> {
        validate_finite(&self.a, "Rational.a")?;
        validate_finite(&self.b, "Rational.b")
    }
}

/// Counter-flow heat exchanger effectiveness: ε = f(NTU, Cr).
///
/// Stores `cr` (capacity ratio) as a fixed coefficient; `x` = NTU.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct EffectivenessCurve {
    /// Capacity ratio Cr (fixed).
    pub cr: f64,
}

impl CurveEval for EffectivenessCurve {
    fn evaluate(&self, x: f64) -> f64 {
        let ntu = x;
        let cr = self.cr;
        if (cr - 1.0).abs() < 1e-10 {
            // Balanced counter-flow: ε = NTU / (1 + NTU)
            ntu / (1.0 + ntu)
        } else {
            let exp_val = (-ntu * (1.0 - cr)).exp();
            (1.0 - exp_val) / (1.0 - cr * exp_val)
        }
    }

    fn derivative(&self, x: f64) -> f64 {
        let ntu = x;
        let cr = self.cr;
        if (cr - 1.0).abs() < 1e-10 {
            1.0 / (1.0 + ntu).powi(2)
        } else {
            let exp_val = (-ntu * (1.0 - cr)).exp();
            let denom = 1.0 - cr * exp_val;
            (1.0 - cr).powi(2) * exp_val / (denom * denom)
        }
    }

    fn validate_coefficients(&self) -> Result<(), ComponentError> {
        validate_finite(&[self.cr], "Effectiveness.cr")
    }
}

/// Bivariate polynomial curve: z = Σ a_ij * x^i * y^j.
///
/// Delegates to [`Polynomial2D`]. Stores `y` as a fixed parameter;
/// `evaluate(x)` uses the stored `y`. Use [`evaluate_xy`](Self::evaluate_xy)
/// for callers that need both variables.
#[derive(Debug, Clone, PartialEq)]
pub struct BivariatePolyCurve {
    inner: Polynomial2D,
    /// Fixed y parameter for univariate evaluation. Defaults to 0.0.
    pub y: f64,
}

impl BivariatePolyCurve {
    /// Creates a new bivariate polynomial from a coefficient matrix.
    pub fn new(coefficients: Vec<Vec<f64>>) -> Self {
        Self {
            inner: Polynomial2D::new(coefficients),
            y: 0.0,
        }
    }

    /// Evaluates the bivariate polynomial at `(x, y)` (bivariate).
    pub fn evaluate_xy(&self, x: f64, y: f64) -> f64 {
        self.inner.evaluate(x, y)
    }
}

impl Serialize for BivariatePolyCurve {
    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
        use serde::ser::SerializeStruct;
        let mut s = serializer.serialize_struct("BivariatePolyCurve", 2)?;
        s.serialize_field("coefficients", self.inner.coefficients())?;
        s.serialize_field("y", &self.y)?;
        s.end()
    }
}

impl<'de> Deserialize<'de> for BivariatePolyCurve {
    fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
        #[derive(Deserialize)]
        struct Helper {
            coefficients: Vec<Vec<f64>>,
            #[serde(default)]
            y: f64,
        }
        let h = Helper::deserialize(deserializer)?;
        Ok(Self {
            inner: Polynomial2D::new(h.coefficients),
            y: h.y,
        })
    }
}

impl CurveEval for BivariatePolyCurve {
    fn evaluate(&self, x: f64) -> f64 {
        self.inner.evaluate(x, self.y)
    }

    fn derivative(&self, x: f64) -> f64 {
        self.inner.partial_x(x, self.y)
    }

    fn validate_coefficients(&self) -> Result<(), ComponentError> {
        self.inner.validate()
    }
}

// ---------------------------------------------------------------------------
// Validation helper
// ---------------------------------------------------------------------------

fn validate_finite(vals: &[f64], name: &str) -> Result<(), ComponentError> {
    for (i, &v) in vals.iter().enumerate() {
        if v.is_nan() {
            return Err(ComponentError::InvalidState(format!(
                "{} coefficient {} is NaN",
                name, i
            )));
        }
        if v.is_infinite() {
            return Err(ComponentError::InvalidState(format!(
                "{} coefficient {} is infinite",
                name, i
            )));
        }
    }
    Ok(())
}

// ---------------------------------------------------------------------------
// CurveEngine — enum dispatch
// ---------------------------------------------------------------------------

/// Zero-cost enum dispatch over all curve types.
///
/// Uses adjacently-tagged serde: `{"type": "Variant", "coefficients": {...}}`.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(tag = "type", content = "coefficients")]
pub enum CurveEngine {
    /// Polynomial: y = c0 + c1*x + c2*x² + ... (Horner's method)
    Polynomial(PolynomialCurve),
    /// Exponential: y = a * exp(b*x) + c
    Exponential(ExponentialCurve),
    /// Logarithmic: y = a * ln(x) + b
    Logarithmic(LogarithmicCurve),
    /// Inverse: y = a / (x + b) + c
    Inverse(InverseCurve),
    /// Colburn j-factor: y = a * x^b
    Colburn(ColburnCurve),
    /// Power law: y = a * x^b
    PowerLaw(PowerLawCurve),
    /// Rational: y = (a0 + a1*x) / (1 + b1*x + b2*x²)
    Rational(RationalCurve),
    /// Counter-flow ε-NTU effectiveness: ε = f(NTU, Cr)
    Effectiveness(EffectivenessCurve),
    /// Bivariate polynomial: z = Σ a_ij * x^i * y^j
    BivariatePoly(BivariatePolyCurve),
}

impl CurveEval for CurveEngine {
    fn evaluate(&self, x: f64) -> f64 {
        match self {
            Self::Polynomial(c) => c.evaluate(x),
            Self::Exponential(c) => c.evaluate(x),
            Self::Logarithmic(c) => c.evaluate(x),
            Self::Inverse(c) => c.evaluate(x),
            Self::Colburn(c) => c.evaluate(x),
            Self::PowerLaw(c) => c.evaluate(x),
            Self::Rational(c) => c.evaluate(x),
            Self::Effectiveness(c) => c.evaluate(x),
            Self::BivariatePoly(c) => c.evaluate(x),
        }
    }

    fn derivative(&self, x: f64) -> f64 {
        match self {
            Self::Polynomial(c) => c.derivative(x),
            Self::Exponential(c) => c.derivative(x),
            Self::Logarithmic(c) => c.derivative(x),
            Self::Inverse(c) => c.derivative(x),
            Self::Colburn(c) => c.derivative(x),
            Self::PowerLaw(c) => c.derivative(x),
            Self::Rational(c) => c.derivative(x),
            Self::Effectiveness(c) => c.derivative(x),
            Self::BivariatePoly(c) => c.derivative(x),
        }
    }

    fn validate_coefficients(&self) -> Result<(), ComponentError> {
        match self {
            Self::Polynomial(c) => c.validate_coefficients(),
            Self::Exponential(c) => c.validate_coefficients(),
            Self::Logarithmic(c) => c.validate_coefficients(),
            Self::Inverse(c) => c.validate_coefficients(),
            Self::Colburn(c) => c.validate_coefficients(),
            Self::PowerLaw(c) => c.validate_coefficients(),
            Self::Rational(c) => c.validate_coefficients(),
            Self::Effectiveness(c) => c.validate_coefficients(),
            Self::BivariatePoly(c) => c.validate_coefficients(),
        }
    }
}

impl CurveEngine {
    /// Returns the curve type name as a string.
    pub fn curve_type_name(&self) -> &'static str {
        match self {
            Self::Polynomial(_) => "Polynomial",
            Self::Exponential(_) => "Exponential",
            Self::Logarithmic(_) => "Logarithmic",
            Self::Inverse(_) => "Inverse",
            Self::Colburn(_) => "Colburn",
            Self::PowerLaw(_) => "PowerLaw",
            Self::Rational(_) => "Rational",
            Self::Effectiveness(_) => "Effectiveness",
            Self::BivariatePoly(_) => "BivariatePoly",
        }
    }
}

impl From<Polynomial1D> for CurveEngine {
    fn from(p: Polynomial1D) -> Self {
        Self::Polynomial(PolynomialCurve::new(p.coefficients().to_vec()))
    }
}

impl From<Polynomial2D> for CurveEngine {
    fn from(p: Polynomial2D) -> Self {
        Self::BivariatePoly(BivariatePolyCurve { inner: p, y: 0.0 })
    }
}

// ---------------------------------------------------------------------------
// BoundedCurve
// ---------------------------------------------------------------------------

/// A curve with optional validity bounds and metadata.
///
/// Wraps a [`CurveEngine`] and emits warnings when evaluation occurs outside
/// the declared validity range.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct BoundedCurve {
    /// Curve name (e.g. "compressor_capacity").
    pub name: String,
    /// The underlying curve engine.
    pub inner: CurveEngine,
    /// Optional `(x_min, x_max)` validity bounds.
    pub bounds: Option<(f64, f64)>,
    /// Optional human-readable description.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub description: Option<String>,
}

impl BoundedCurve {
    /// Creates a new bounded curve.
    pub fn new(name: impl Into<String>, inner: CurveEngine) -> Self {
        Self {
            name: name.into(),
            inner,
            bounds: None,
            description: None,
        }
    }

    /// Sets validity bounds `(x_min, x_max)`.
    ///
    /// # Panics
    ///
    /// Panics if `min > max` or if either bound is NaN.
    pub fn with_bounds(mut self, min: f64, max: f64) -> Self {
        assert!(
            min <= max,
            "BoundedCurve::with_bounds: min ({min}) must be <= max ({max})"
        );
        assert!(
            !min.is_nan() && !max.is_nan(),
            "BoundedCurve::with_bounds: bounds must not be NaN"
        );
        self.bounds = Some((min, max));
        self
    }

    /// Sets a human-readable description.
    pub fn with_description(mut self, desc: impl Into<String>) -> Self {
        self.description = Some(desc.into());
        self
    }

    /// Evaluates the curve at `x`, returning the value, derivative, and
    /// a bound-warning. Emits a `tracing::warn!` if outside bounds or if
    /// the result is NaN/infinite (domain violation).
    pub fn evaluate(&self, x: f64) -> CurveResult {
        let value = self.inner.evaluate(x);
        let derivative = self.inner.derivative(x);

        // Check for domain violations (NaN/inf in result)
        if !value.is_finite() || !derivative.is_finite() {
            tracing::warn!(
                curve = %self.name,
                x,
                value,
                derivative,
                "Curve evaluation produced non-finite result (domain violation)"
            );
            return CurveResult {
                value,
                derivative,
                warning: CurveWarning::DomainViolation,
            };
        }

        let warning = CurveWarning::check(x, self.bounds);
        if !matches!(warning, CurveWarning::WithinBounds) {
            let bound_info = match self.bounds {
                Some((min, max)) => format!("[{}, {}]", min, max),
                None => "none".to_string(),
            };
            tracing::warn!(
                curve = %self.name,
                x,
                bounds = %bound_info,
                warning = ?warning,
                "Operating point outside curve validity bounds"
            );
        }
        CurveResult {
            value,
            derivative,
            warning,
        }
    }

    /// Validates the inner curve coefficients.
    pub fn validate(&self) -> Result<(), ComponentError> {
        self.inner.validate_coefficients()
    }
}

// ---------------------------------------------------------------------------
// CurveSet
// ---------------------------------------------------------------------------

/// Named collection of [`BoundedCurve`]s for multi-curve evaluation.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct CurveSet {
    curves: HashMap<String, BoundedCurve>,
}

impl CurveSet {
    /// Creates an empty curve set.
    pub fn new() -> Self {
        Self {
            curves: HashMap::new(),
        }
    }

    /// Inserts a curve into the set.
    pub fn insert(&mut self, curve: BoundedCurve) {
        self.curves.insert(curve.name.clone(), curve);
    }

    /// Returns a reference to the curve with the given name, if it exists.
    pub fn get(&self, name: &str) -> Option<&BoundedCurve> {
        self.curves.get(name)
    }

    /// Evaluates the named curve at `x`.
    ///
    /// Returns `None` if no curve with that name exists.
    pub fn evaluate(&self, name: &str, x: f64) -> Option<CurveResult> {
        self.curves.get(name).map(|c| c.evaluate(x))
    }

    /// Returns the number of curves in the set.
    pub fn len(&self) -> usize {
        self.curves.len()
    }

    /// Returns `true` if the set contains no curves.
    pub fn is_empty(&self) -> bool {
        self.curves.is_empty()
    }

    /// Returns an iterator over all curve names.
    pub fn names(&self) -> impl Iterator<Item = &str> {
        self.curves.keys().map(|s| s.as_str())
    }

    /// Validates all curves in the set.
    pub fn validate(&self) -> Result<(), ComponentError> {
        for curve in self.curves.values() {
            curve.validate()?;
        }
        Ok(())
    }
}

impl Default for CurveSet {
    fn default() -> Self {
        Self::new()
    }
}

// ---------------------------------------------------------------------------
// Tests
// ---------------------------------------------------------------------------

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

    // === PolynomialCurve ===

    #[test]
    fn test_polynomial_evaluate() {
        let c = PolynomialCurve::new(vec![1.0, 2.0, 3.0]); // y = 1 + 2x + 3x²
        assert_relative_eq!(c.evaluate(0.0), 1.0);
        assert_relative_eq!(c.evaluate(1.0), 6.0); // 1 + 2 + 3
        assert_relative_eq!(c.evaluate(2.0), 17.0); // 1 + 4 + 12
    }

    #[test]
    fn test_polynomial_derivative() {
        let c = PolynomialCurve::new(vec![1.0, 2.0, 3.0]); // dy/dx = 2 + 6x
        assert_relative_eq!(c.derivative(0.0), 2.0);
        assert_relative_eq!(c.derivative(2.0), 14.0); // 2 + 12
    }

    #[test]
    fn test_polynomial_validate() {
        assert!(PolynomialCurve::new(vec![1.0, 2.0])
            .validate_coefficients()
            .is_ok());
        assert!(PolynomialCurve::new(vec![f64::NAN])
            .validate_coefficients()
            .is_err());
        assert!(PolynomialCurve::new(vec![f64::INFINITY])
            .validate_coefficients()
            .is_err());
    }

    // === ExponentialCurve ===

    #[test]
    fn test_exponential_evaluate() {
        let c = ExponentialCurve {
            a: 2.0,
            b: 0.5,
            c: 1.0,
        }; // y = 2*exp(0.5x) + 1
        assert_relative_eq!(c.evaluate(0.0), 3.0); // 2*1 + 1
        assert_relative_eq!(c.evaluate(1.0), 2.0 * (0.5_f64).exp() + 1.0);
    }

    #[test]
    fn test_exponential_derivative() {
        let c = ExponentialCurve {
            a: 2.0,
            b: 0.5,
            c: 1.0,
        };
        // dy/dx = 2*0.5*exp(0.5x) = exp(0.5x)
        assert_relative_eq!(c.derivative(0.0), 1.0);
        assert_relative_eq!(c.derivative(1.0), (0.5_f64).exp());
    }

    #[test]
    fn test_exponential_validate() {
        assert!(ExponentialCurve {
            a: 1.0,
            b: 2.0,
            c: 0.0
        }
        .validate_coefficients()
        .is_ok());
        assert!(ExponentialCurve {
            a: f64::NAN,
            b: 1.0,
            c: 0.0
        }
        .validate_coefficients()
        .is_err());
    }

    // === LogarithmicCurve ===

    #[test]
    fn test_logarithmic_evaluate() {
        let c = LogarithmicCurve { a: 3.0, b: 2.0 }; // y = 3*ln(x) + 2
        assert_relative_eq!(c.evaluate(1.0), 2.0);
        assert_relative_eq!(c.evaluate(std::f64::consts::E), 5.0); // 3*1 + 2
    }

    #[test]
    fn test_logarithmic_derivative() {
        let c = LogarithmicCurve { a: 3.0, b: 2.0 }; // dy/dx = 3/x
        assert_relative_eq!(c.derivative(1.0), 3.0);
        assert_relative_eq!(c.derivative(3.0), 1.0);
    }

    #[test]
    fn test_logarithmic_derivative_negative_x() {
        let c = LogarithmicCurve { a: 3.0, b: 2.0 };
        assert!(c.derivative(-1.0).is_nan());
        assert!(c.derivative(0.0).is_nan());
    }

    // === InverseCurve ===

    #[test]
    fn test_inverse_evaluate() {
        let c = InverseCurve {
            a: 6.0,
            b: 2.0,
            c: 1.0,
        }; // y = 6/(x+2) + 1
        assert_relative_eq!(c.evaluate(0.0), 4.0); // 6/2 + 1
        assert_relative_eq!(c.evaluate(4.0), 2.0); // 6/6 + 1
    }

    #[test]
    fn test_inverse_derivative() {
        let c = InverseCurve {
            a: 6.0,
            b: 2.0,
            c: 1.0,
        }; // dy/dx = -6/(x+2)²
        assert_relative_eq!(c.derivative(0.0), -1.5); // -6/4
        assert_relative_eq!(c.derivative(4.0), -0.166_666_666_666_666_66); // -6/36
    }

    // === ColburnCurve ===

    #[test]
    fn test_colburn_evaluate() {
        let c = ColburnCurve { a: 0.023, b: 0.8 }; // y = 0.023 * x^0.8
        let x: f64 = 10000.0;
        let expected = 0.023 * x.powf(0.8);
        assert_relative_eq!(c.evaluate(x), expected);
    }

    #[test]
    fn test_colburn_derivative() {
        let c = ColburnCurve { a: 0.023, b: 0.8 };
        let x: f64 = 10000.0;
        let expected = 0.023 * 0.8 * x.powf(-0.2);
        assert_relative_eq!(c.derivative(x), expected);
    }

    // === PowerLawCurve ===

    #[test]
    fn test_powerlaw_evaluate() {
        let c = PowerLawCurve { a: 2.0, b: 3.0 }; // y = 2*x³
        assert_relative_eq!(c.evaluate(0.0), 0.0);
        assert_relative_eq!(c.evaluate(1.0), 2.0);
        assert_relative_eq!(c.evaluate(2.0), 16.0); // 2*8
    }

    #[test]
    fn test_powerlaw_derivative() {
        let c = PowerLawCurve { a: 2.0, b: 3.0 }; // dy/dx = 6*x²
        assert_relative_eq!(c.derivative(1.0), 6.0);
        assert_relative_eq!(c.derivative(2.0), 24.0);
    }

    // === RationalCurve ===

    #[test]
    fn test_rational_evaluate() {
        // y = (1 + 2x) / (1 + 0.5x + 0.1x²)
        let c = RationalCurve {
            a: [1.0, 2.0],
            b: vec![0.5, 0.1],
        };
        assert_relative_eq!(c.evaluate(0.0), 1.0); // (1+0)/(1+0+0)
        let at_1 = (1.0 + 2.0) / (1.0 + 0.5 + 0.1); // 3/1.6
        assert_relative_eq!(c.evaluate(1.0), at_1);
    }

    #[test]
    fn test_rational_derivative() {
        let c = RationalCurve {
            a: [1.0, 2.0],
            b: vec![0.5, 0.1],
        };
        // Numerical check at x=1
        let h = 1e-7;
        let numerical = (c.evaluate(1.0 + h) - c.evaluate(1.0 - h)) / (2.0 * h);
        assert_relative_eq!(c.derivative(1.0), numerical, epsilon = 1e-5);
    }

    // === EffectivenessCurve ===

    #[test]
    fn test_effectiveness_evaluate() {
        // Cr=0.5, NTU=3 → ε = (1-exp(-3*0.5))/(1-0.5*exp(-3*0.5))
        let c = EffectivenessCurve { cr: 0.5 };
        let ntu = 3.0;
        let exp_val: f64 = (-(ntu * 0.5_f64)).exp();
        let expected = (1.0 - exp_val) / (1.0 - 0.5 * exp_val);
        assert_relative_eq!(c.evaluate(ntu), expected, epsilon = 1e-10);
    }

    #[test]
    fn test_effectiveness_balanced() {
        // Cr=1 (balanced counter-flow): ε = NTU/(1+NTU)
        let c = EffectivenessCurve { cr: 1.0 };
        assert_relative_eq!(c.evaluate(3.0), 3.0 / 4.0);
        assert_relative_eq!(c.evaluate(10.0), 10.0 / 11.0);
    }

    #[test]
    fn test_effectiveness_derivative() {
        let c = EffectivenessCurve { cr: 0.5 };
        // Numerical check
        let h = 1e-7;
        let ntu = 3.0;
        let numerical = (c.evaluate(ntu + h) - c.evaluate(ntu - h)) / (2.0 * h);
        assert_relative_eq!(c.derivative(ntu), numerical, epsilon = 1e-5);
    }

    #[test]
    fn test_effectiveness_derivative_balanced() {
        let c = EffectivenessCurve { cr: 1.0 };
        // d/dNTU [NTU/(1+NTU)] = 1/(1+NTU)²
        assert_relative_eq!(c.derivative(3.0), 1.0 / 16.0);
    }

    // === BivariatePolyCurve ===

    #[test]
    fn test_bivariate_poly_evaluate() {
        // z = 1 + 2*x + 3*y + 0.5*x*y, y fixed at 2.0
        let c = BivariatePolyCurve {
            inner: Polynomial2D::new(vec![vec![1.0, 3.0], vec![2.0, 0.5]]),
            y: 2.0,
        };
        // evaluate(3.0) with y=2.0: 1 + 6 + 6 + 3 = 16
        assert_relative_eq!(c.evaluate(3.0), 16.0);
    }

    #[test]
    fn test_bivariate_poly_evaluate_xy() {
        let c = BivariatePolyCurve {
            inner: Polynomial2D::new(vec![vec![1.0, 3.0], vec![2.0, 0.5]]),
            y: 0.0, // y field ignored for evaluate_xy
        };
        // z(2,3) = 1 + 4 + 9 + 3 = 17
        assert_relative_eq!(c.evaluate_xy(2.0, 3.0), 17.0);
    }

    #[test]
    fn test_bivariate_poly_derivative() {
        let c = BivariatePolyCurve {
            inner: Polynomial2D::new(vec![vec![1.0, 3.0], vec![2.0, 0.5]]),
            y: 2.0,
        };
        // ∂z/∂x = 2 + 0.5*y = 2 + 1 = 3 (at y=2)
        assert_relative_eq!(c.derivative(5.0), 3.0);
    }

    // === BoundedCurve ===

    #[test]
    fn test_bounded_within_bounds() {
        let curve = BoundedCurve::new(
            "test",
            CurveEngine::Polynomial(PolynomialCurve::new(vec![1.0, 2.0])),
        )
        .with_bounds(0.0, 100.0);
        let result = curve.evaluate(50.0);
        assert_relative_eq!(result.value, 101.0);
        assert_relative_eq!(result.derivative, 2.0);
        assert_eq!(result.warning, CurveWarning::WithinBounds);
    }

    #[test]
    fn test_bounded_below_min() {
        let curve = BoundedCurve::new(
            "test",
            CurveEngine::Polynomial(PolynomialCurve::new(vec![0.0, 1.0])),
        )
        .with_bounds(10.0, 100.0);
        let result = curve.evaluate(5.0);
        assert_relative_eq!(result.value, 5.0); // Still evaluates
        assert_eq!(result.warning, CurveWarning::BelowMinBound);
    }

    #[test]
    fn test_bounded_above_max() {
        let curve = BoundedCurve::new(
            "test",
            CurveEngine::Polynomial(PolynomialCurve::new(vec![0.0, 1.0])),
        )
        .with_bounds(0.0, 100.0);
        let result = curve.evaluate(150.0);
        assert_relative_eq!(result.value, 150.0);
        assert_eq!(result.warning, CurveWarning::AboveMaxBound);
    }

    #[test]
    fn test_bounded_no_bounds() {
        let curve = BoundedCurve::new(
            "test",
            CurveEngine::Polynomial(PolynomialCurve::new(vec![1.0])),
        );
        let result = curve.evaluate(9999.0);
        assert_eq!(result.warning, CurveWarning::WithinBounds);
    }

    // === CurveEngine dispatch ===

    #[test]
    fn test_curve_engine_polynomial() {
        let engine = CurveEngine::Polynomial(PolynomialCurve::new(vec![1.0, 2.0, 3.0]));
        assert_relative_eq!(engine.evaluate(2.0), 17.0);
        assert_relative_eq!(engine.derivative(2.0), 14.0);
    }

    #[test]
    fn test_curve_engine_type_name() {
        assert_eq!(
            CurveEngine::Polynomial(PolynomialCurve::new(vec![1.0])).curve_type_name(),
            "Polynomial"
        );
        assert_eq!(
            CurveEngine::Exponential(ExponentialCurve {
                a: 1.0,
                b: 1.0,
                c: 0.0
            })
            .curve_type_name(),
            "Exponential"
        );
    }

    #[test]
    fn test_curve_engine_from_polynomial1d() {
        let p1d = Polynomial1D::quadratic(1.0, 2.0, 3.0);
        let engine = CurveEngine::from(p1d);
        assert_relative_eq!(engine.evaluate(2.0), 17.0);
    }

    #[test]
    fn test_curve_engine_from_polynomial2d() {
        let p2d = Polynomial2D::bilinear(1.0, 2.0, 3.0, 0.5);
        let engine = CurveEngine::from(p2d);
        // evaluate(x) uses stored y=0.0
        assert_relative_eq!(engine.evaluate(3.0), 7.0); // 1 + 6 + 0 + 0
    }

    #[test]
    fn test_curve_engine_validate() {
        let valid = CurveEngine::Polynomial(PolynomialCurve::new(vec![1.0, 2.0]));
        assert!(valid.validate_coefficients().is_ok());

        let invalid = CurveEngine::Exponential(ExponentialCurve {
            a: f64::NAN,
            b: 1.0,
            c: 0.0,
        });
        assert!(invalid.validate_coefficients().is_err());
    }

    // === JSON round-trip ===

    #[test]
    fn test_json_roundtrip_polynomial() {
        let engine = CurveEngine::Polynomial(PolynomialCurve::new(vec![1.0, -0.5, 0.01]));
        let json = serde_json::to_string(&engine).unwrap();
        assert!(json.contains("\"type\":\"Polynomial\""));
        let restored: CurveEngine = serde_json::from_str(&json).unwrap();
        assert_eq!(engine, restored);
    }

    #[test]
    fn test_json_roundtrip_exponential() {
        let engine = CurveEngine::Exponential(ExponentialCurve {
            a: 1.5,
            b: -0.02,
            c: 0.1,
        });
        let json = serde_json::to_string(&engine).unwrap();
        assert!(json.contains("\"type\":\"Exponential\""));
        let restored: CurveEngine = serde_json::from_str(&json).unwrap();
        assert_eq!(engine, restored);
    }

    #[test]
    fn test_json_roundtrip_logarithmic() {
        let engine = CurveEngine::Logarithmic(LogarithmicCurve { a: 2.0, b: -1.0 });
        let json = serde_json::to_string(&engine).unwrap();
        let restored: CurveEngine = serde_json::from_str(&json).unwrap();
        assert_eq!(engine, restored);
    }

    #[test]
    fn test_json_roundtrip_inverse() {
        let engine = CurveEngine::Inverse(InverseCurve {
            a: 10.0,
            b: 5.0,
            c: -2.0,
        });
        let json = serde_json::to_string(&engine).unwrap();
        let restored: CurveEngine = serde_json::from_str(&json).unwrap();
        assert_eq!(engine, restored);
    }

    #[test]
    fn test_json_roundtrip_colburn() {
        let engine = CurveEngine::Colburn(ColburnCurve { a: 0.023, b: 0.8 });
        let json = serde_json::to_string(&engine).unwrap();
        let restored: CurveEngine = serde_json::from_str(&json).unwrap();
        assert_eq!(engine, restored);
    }

    #[test]
    fn test_json_roundtrip_powerlaw() {
        let engine = CurveEngine::PowerLaw(PowerLawCurve { a: 3.0, b: 1.5 });
        let json = serde_json::to_string(&engine).unwrap();
        let restored: CurveEngine = serde_json::from_str(&json).unwrap();
        assert_eq!(engine, restored);
    }

    #[test]
    fn test_json_roundtrip_rational() {
        let engine = CurveEngine::Rational(RationalCurve {
            a: [1.0, 2.0],
            b: vec![0.5, 0.1],
        });
        let json = serde_json::to_string(&engine).unwrap();
        let restored: CurveEngine = serde_json::from_str(&json).unwrap();
        assert_eq!(engine, restored);
    }

    #[test]
    fn test_json_roundtrip_effectiveness() {
        let engine = CurveEngine::Effectiveness(EffectivenessCurve { cr: 0.7 });
        let json = serde_json::to_string(&engine).unwrap();
        let restored: CurveEngine = serde_json::from_str(&json).unwrap();
        assert_eq!(engine, restored);
    }

    #[test]
    fn test_json_roundtrip_bivariate() {
        let engine = CurveEngine::BivariatePoly(BivariatePolyCurve {
            inner: Polynomial2D::new(vec![vec![1.0, 2.0], vec![3.0, 0.5]]),
            y: 0.0,
        });
        let json = serde_json::to_string(&engine).unwrap();
        let restored: CurveEngine = serde_json::from_str(&json).unwrap();
        assert_relative_eq!(engine.evaluate(2.0), restored.evaluate(2.0));
    }

    #[test]
    fn test_json_roundtrip_bounded_curve() {
        let curve = BoundedCurve::new(
            "capacity",
            CurveEngine::Exponential(ExponentialCurve {
                a: 1.0,
                b: 0.5,
                c: 0.0,
            }),
        )
        .with_bounds(-10.0, 50.0)
        .with_description("Compressor capacity curve");
        let json = serde_json::to_string(&curve).unwrap();
        let restored: BoundedCurve = serde_json::from_str(&json).unwrap();
        assert_eq!(curve.name, restored.name);
        assert_eq!(curve.bounds, restored.bounds);
        assert_eq!(curve.description, restored.description);
        assert_relative_eq!(curve.inner.evaluate(5.0), restored.inner.evaluate(5.0));
    }

    #[test]
    fn test_json_roundtrip_curve_set() {
        let mut set = CurveSet::new();
        set.insert(BoundedCurve::new(
            "head",
            CurveEngine::Polynomial(PolynomialCurve::new(vec![50.0, -0.1, -0.001])),
        ));
        set.insert(
            BoundedCurve::new(
                "efficiency",
                CurveEngine::Polynomial(PolynomialCurve::new(vec![0.4, 0.02, -0.0001])),
            )
            .with_bounds(0.0, 200.0),
        );

        let json = serde_json::to_string(&set).unwrap();
        let restored: CurveSet = serde_json::from_str(&json).unwrap();
        assert_eq!(set.len(), restored.len());
        assert!(restored.get("head").is_some());
        assert!(restored.get("efficiency").is_some());
    }

    // === CurveSet ===

    #[test]
    fn test_curve_set_basic() {
        let mut set = CurveSet::new();
        assert!(set.is_empty());

        set.insert(BoundedCurve::new(
            "cap",
            CurveEngine::Polynomial(PolynomialCurve::new(vec![1.0, 2.0])),
        ));
        assert_eq!(set.len(), 1);

        let result = set.evaluate("cap", 3.0).unwrap();
        assert_relative_eq!(result.value, 7.0);

        assert!(set.evaluate("nonexistent", 1.0).is_none());
    }

    #[test]
    fn test_curve_set_validate() {
        let mut set = CurveSet::new();
        set.insert(BoundedCurve::new(
            "good",
            CurveEngine::Polynomial(PolynomialCurve::new(vec![1.0, 2.0])),
        ));
        assert!(set.validate().is_ok());

        let mut bad_set = CurveSet::new();
        bad_set.insert(BoundedCurve::new(
            "bad",
            CurveEngine::Polynomial(PolynomialCurve::new(vec![f64::NAN])),
        ));
        assert!(bad_set.validate().is_err());
    }

    // === Edge cases ===

    #[test]
    fn test_empty_polynomial() {
        let c = PolynomialCurve::new(vec![]);
        assert_relative_eq!(c.evaluate(5.0), 0.0);
        assert_relative_eq!(c.derivative(5.0), 0.0);
    }

    #[test]
    fn test_constant_polynomial() {
        let c = PolynomialCurve::new(vec![42.0]);
        assert_relative_eq!(c.evaluate(100.0), 42.0);
        assert_relative_eq!(c.derivative(100.0), 0.0);
    }

    #[test]
    fn test_logarithmic_at_x_equals_1() {
        let c = LogarithmicCurve { a: 5.0, b: 3.0 };
        assert_relative_eq!(c.evaluate(1.0), 3.0); // 5*ln(1) + 3 = 3
    }

    #[test]
    fn test_inverse_zero_denominator() {
        let c = InverseCurve {
            a: 1.0,
            b: -5.0,
            c: 0.0,
        }; // y = 1/(x-5)
           // At x=5, denominator is 0 → infinity
        let result = c.evaluate(5.0);
        assert!(result.is_infinite());
    }

    #[test]
    fn test_bounded_curve_no_description_serialized() {
        let curve = BoundedCurve::new(
            "test",
            CurveEngine::Polynomial(PolynomialCurve::new(vec![1.0])),
        );
        let json = serde_json::to_string(&curve).unwrap();
        assert!(!json.contains("description")); // skip_serializing_if
    }
}
