//! Compressor Component Implementation (AHRI 540 Standard + SST/SDT Curves)
//!
//! This module provides a compressor component implementation based on the
//! AHRI 540 standard for performance rating of positive displacement
//! refrigerant compressors and compressor units.
//!
//! ## AHRI 540 Standard Equations
//!
//! The compressor is modeled using 10 coefficients (M1-M10):
//!
//! **Mass Flow Rate:**
//! ```text
//! ṁ = M1 × (1 - (P_suction/P_discharge)^(1/M2)) × ρ_suction × V_disp × N/60
//! ```
//!
//! **Power Consumption (Cooling):**
//! ```text
//! Ẇ = M3 + M4 × (P_discharge/P_suction) + M5 × T_suction + M6 × T_discharge
//! ```
//!
//! **Power Consumption (Heating):**
//! ```text
//! Ẇ = M7 + M8 × (P_discharge/P_suction) + M9 × T_suction + M10 × T_discharge
//! ```
//!
//! ## SST/SDT Polynomial Model
//!
//! Alternative model based on saturated temperatures:
//!
//! **Mass Flow Rate:**
//! ```text
//! ṁ = Σ a_ij × SST^i × SDT^j
//! ```
//!
//! **Power Consumption:**
//! ```text
//! Ẇ = Σ b_ij × SST^i × SDT^j
//! ```
//!
//! Where:
//! - SST = Saturated Suction Temperature (K)
//! - SDT = Saturated Discharge Temperature (K)

use crate::polynomials::Polynomial2D;
use crate::port::{Connected, Disconnected, FluidId, Port};
use crate::{
    CircuitId, Component, ComponentError, ConnectedPort, JacobianBuilder, OperationalState,
    ResidualVector, StateSlice,
};
use entropyk_core::{Calib, Enthalpy, MassFlow, Temperature};
use serde::{Deserialize, Serialize};
use std::marker::PhantomData;

/// Coefficients for AHRI 540 compressor performance model.
///
/// The AHRI 540 standard defines 10 coefficients (M1-M10) that characterize
/// compressor performance across operating conditions.
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
pub struct Ahri540Coefficients {
    /// Flow coefficient (M1)
    pub m1: f64,
    /// Pressure ratio exponent (M2)
    pub m2: f64,
    /// Power coefficient - constant term for cooling (M3)
    pub m3: f64,
    /// Power coefficient - pressure ratio term for cooling (M4)
    pub m4: f64,
    /// Power coefficient - suction temperature term for cooling (M5)
    pub m5: f64,
    /// Power coefficient - discharge temperature term for cooling (M6)
    pub m6: f64,
    /// Power coefficient - constant term for heating (M7)
    pub m7: f64,
    /// Power coefficient - pressure ratio term for heating (M8)
    pub m8: f64,
    /// Power coefficient - suction temperature term for heating (M9)
    pub m9: f64,
    /// Power coefficient - discharge temperature term for heating (M10)
    pub m10: f64,
}

impl Ahri540Coefficients {
    /// Creates a new set of AHRI 540 coefficients.
    #[allow(clippy::too_many_arguments)]
    ///
    /// # Arguments
    ///
    /// * `m1` - Flow coefficient
    /// * `m2` - Pressure ratio exponent
    /// * `m3` - Power constant (cooling)
    /// * `m4` - Power pressure ratio coefficient (cooling)
    /// * `m5` - Power suction temperature coefficient (cooling)
    /// * `m6` - Power discharge temperature coefficient (cooling)
    /// * `m7` - Power constant (heating)
    /// * `m8` - Power pressure ratio coefficient (heating)
    /// * `m9` - Power suction temperature coefficient (heating)
    /// * `m10` - Power discharge temperature coefficient (heating)
    ///
    /// # Example
    ///
    /// ```
    /// use entropyk_components::compressor::Ahri540Coefficients;
    ///
    /// let coeffs = Ahri540Coefficients::new(
    ///     0.85, 2.5,      // M1, M2 (flow)
    ///     500.0, 1500.0, -2.5, 1.8,  // M3-M6 (cooling)
    ///     600.0, 1600.0, -3.0, 2.0   // M7-M10 (heating)
    /// );
    /// ```
    pub fn new(
        m1: f64,
        m2: f64,
        m3: f64,
        m4: f64,
        m5: f64,
        m6: f64,
        m7: f64,
        m8: f64,
        m9: f64,
        m10: f64,
    ) -> Self {
        Self {
            m1,
            m2,
            m3,
            m4,
            m5,
            m6,
            m7,
            m8,
            m9,
            m10,
        }
    }

    /// Validates that coefficients are within reasonable ranges.
    ///
    /// Returns an error if any coefficient is NaN or infinite,
    /// or if critical coefficients are outside expected ranges.
    pub fn validate(&self) -> Result<(), ComponentError> {
        // Check for NaN or infinite values
        let coefficients = [
            ("M1", self.m1),
            ("M2", self.m2),
            ("M3", self.m3),
            ("M4", self.m4),
            ("M5", self.m5),
            ("M6", self.m6),
            ("M7", self.m7),
            ("M8", self.m8),
            ("M9", self.m9),
            ("M10", self.m10),
        ];

        for (name, value) in coefficients.iter() {
            if value.is_nan() {
                return Err(ComponentError::InvalidState(format!(
                    "Coefficient {} is NaN",
                    name
                )));
            }
            if value.is_infinite() {
                return Err(ComponentError::InvalidState(format!(
                    "Coefficient {} is infinite",
                    name
                )));
            }
        }

        // M2 should be positive (pressure ratio exponent)
        if self.m2 <= 0.0 {
            return Err(ComponentError::InvalidState(
                "Coefficient M2 (pressure ratio exponent) must be positive".to_string(),
            ));
        }

        Ok(())
    }
}

/// Polynomial coefficients for SST/SDT based compressor model.
///
/// This model characterizes compressor performance as a function of
/// saturated suction temperature (SST) and saturated discharge temperature (SDT).
///
/// ## Model Equations
///
/// **Mass Flow Rate:**
/// ```text
/// ṁ = Σ a_ij × SST^i × SDT^j  (kg/s)
/// ```
///
/// **Power Consumption:**
/// ```text
/// Ẇ = Σ b_ij × SST^i × SDT^j  (W)
/// ```
///
/// # Example
///
/// ```
/// use entropyk_components::compressor::SstSdtCoefficients;
///
/// // Simple bilinear model: m_dot = a00 + a10*SST + a01*SDT + a11*SST*SDT
/// let coeffs = SstSdtCoefficients::bilinear(
///     0.05, 0.001, 0.0005, 0.00001,  // mass flow coefficients
///     1000.0, 50.0, 30.0, 0.5         // power coefficients
/// );
/// ```
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct SstSdtCoefficients {
    /// Mass flow rate polynomial: ṁ = f(SST, SDT) in kg/s
    pub mass_flow_curve: Polynomial2D,
    /// Power consumption polynomial: Ẇ = f(SST, SDT) in W
    pub power_curve: Polynomial2D,
}

impl SstSdtCoefficients {
    /// Creates new SST/SDT coefficients from 2D polynomials.
    pub fn new(mass_flow_curve: Polynomial2D, power_curve: Polynomial2D) -> Self {
        Self {
            mass_flow_curve,
            power_curve,
        }
    }

    /// Creates a bilinear SST/SDT model.
    ///
    /// mass_flow = a00 + a10*SST + a01*SDT + a11*SST*SDT
    /// power = b00 + b10*SST + b01*SDT + b11*SST*SDT
    #[allow(clippy::too_many_arguments)]
    pub fn bilinear(
        mass_a00: f64,
        mass_a10: f64,
        mass_a01: f64,
        mass_a11: f64,
        power_b00: f64,
        power_b10: f64,
        power_b01: f64,
        power_b11: f64,
    ) -> Self {
        Self {
            mass_flow_curve: Polynomial2D::bilinear(mass_a00, mass_a10, mass_a01, mass_a11),
            power_curve: Polynomial2D::bilinear(power_b00, power_b10, power_b01, power_b11),
        }
    }

    /// Creates a biquadratic SST/SDT model (degree 2 in both variables).
    pub fn biquadratic(mass_coeffs: [[f64; 3]; 3], power_coeffs: [[f64; 3]; 3]) -> Self {
        let mass_vec: Vec<Vec<f64>> = mass_coeffs.iter().map(|row| row.to_vec()).collect();
        let power_vec: Vec<Vec<f64>> = power_coeffs.iter().map(|row| row.to_vec()).collect();

        Self {
            mass_flow_curve: Polynomial2D::new(mass_vec),
            power_curve: Polynomial2D::new(power_vec),
        }
    }

    /// Calculates mass flow rate at given SST and SDT.
    ///
    /// # Arguments
    ///
    /// * `sst_k` - Saturated suction temperature in Kelvin
    /// * `sdt_k` - Saturated discharge temperature in Kelvin
    ///
    /// # Returns
    ///
    /// Mass flow rate in kg/s
    pub fn mass_flow_at(&self, sst_k: f64, sdt_k: f64) -> f64 {
        self.mass_flow_curve.evaluate(sst_k, sdt_k)
    }

    /// Calculates power consumption at given SST and SDT.
    ///
    /// # Arguments
    ///
    /// * `sst_k` - Saturated suction temperature in Kelvin
    /// * `sdt_k` - Saturated discharge temperature in Kelvin
    ///
    /// # Returns
    ///
    /// Power consumption in Watts
    pub fn power_at(&self, sst_k: f64, sdt_k: f64) -> f64 {
        self.power_curve.evaluate(sst_k, sdt_k)
    }

    /// Validates that coefficients are within reasonable ranges.
    pub fn validate(&self) -> Result<(), ComponentError> {
        self.mass_flow_curve.validate()?;
        self.power_curve.validate()?;
        Ok(())
    }
}

/// Compressor performance model selection.
///
/// Allows switching between AHRI 540 coefficients and SST/SDT polynomial model.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum CompressorModel {
    /// AHRI 540 standard model with 10 coefficients
    Ahri540(Ahri540Coefficients),
    /// SST/SDT polynomial model
    SstSdt(SstSdtCoefficients),
}

impl Default for CompressorModel {
    fn default() -> Self {
        CompressorModel::Ahri540(Ahri540Coefficients::new(
            0.85, 2.5, 500.0, 1500.0, -2.5, 1.8, 600.0, 1600.0, -3.0, 2.0,
        ))
    }
}

impl From<Ahri540Coefficients> for CompressorModel {
    fn from(coeffs: Ahri540Coefficients) -> Self {
        CompressorModel::Ahri540(coeffs)
    }
}

impl From<SstSdtCoefficients> for CompressorModel {
    fn from(coeffs: SstSdtCoefficients) -> Self {
        CompressorModel::SstSdt(coeffs)
    }
}
///
/// The compressor uses the Type-State pattern to ensure ports are connected
/// before use in simulations. It implements the [`Component`] trait for
/// integration with the solver.
///
/// # Type Parameters
///
/// * `State` - Either `Disconnected` or `Connected`, tracking connection state
///
/// # Example
///
/// ```
/// use entropyk_components::compressor::{Compressor, Ahri540Coefficients};
/// use entropyk_components::port::{FluidId, Port};
/// use entropyk_core::{Pressure, Enthalpy};
///
/// // Create coefficients
/// let coeffs = Ahri540Coefficients::new(
///     0.85, 2.5,
///     500.0, 1500.0, -2.5, 1.8,
///     600.0, 1600.0, -3.0, 2.0
/// );
///
/// // Create disconnected ports (Compressor::new accepts Disconnected ports)
/// let suction = Port::new(
///     FluidId::new("R134a"),
///     Pressure::from_bar(3.5),
///     Enthalpy::from_joules_per_kg(400000.0)
/// );
/// let discharge = Port::new(
///     FluidId::new("R134a"),
///     Pressure::from_bar(3.5),  // Same pressure for validation
///     Enthalpy::from_joules_per_kg(400000.0)  // Same enthalpy for validation
/// );
///
/// // Create compressor with disconnected ports
/// let compressor = Compressor::new(
///     coeffs,
///     suction,
///     discharge,
///     2900.0,    // RPM
///     0.0001,    // Displacement volume (m³/rev)
///     0.85       // Mechanical efficiency
/// ).unwrap();
/// ```
#[derive(Debug, Clone)]
pub struct Compressor<State> {
    /// Compressor performance model (AHRI 540 or SST/SDT polynomial)
    model: CompressorModel,
    /// Suction port (inlet)
    port_suction: Port<State>,
    /// Discharge port (outlet)
    port_discharge: Port<State>,
    /// Rotational speed in RPM
    speed_rpm: f64,
    /// Displacement volume in m³/revolution
    displacement_m3_per_rev: f64,
    /// Mechanical efficiency (0.0 to 1.0)
    mechanical_efficiency: f64,
    /// Calibration factors: ṁ_eff = f_m × ṁ_nominal, Ẇ_eff = f_power × Ẇ_nominal, etc.
    calib: Calib,
    /// Calibration indices to extract factors dynamically from SystemState
    calib_indices: entropyk_core::CalibIndices,
    /// Fluid identifier for density lookups
    fluid_id: FluidId,
    /// Circuit identifier for multi-circuit machines (FR9)
    circuit_id: CircuitId,
    /// Operational state: On, Off, or Bypass (FR6-FR8)
    operational_state: OperationalState,
    /// State-vector index: suction mass flow (incoming edge, CM1.3)
    suction_m_idx: Option<usize>,
    /// State-vector index: suction enthalpy (incoming edge, CM1.3)
    suction_h_idx: Option<usize>,
    /// State-vector index: discharge mass flow (outgoing edge, CM1.3)
    discharge_m_idx: Option<usize>,
    /// State-vector index: discharge enthalpy (outgoing edge, CM1.3)
    discharge_h_idx: Option<usize>,
    /// True when suction and discharge share the same ṁ state index (same
    /// series branch). In this case the mass-conservation residual
    /// `ṁ_dis − ṁ_suc = 0` is trivially satisfied and must be dropped to
    /// keep the system square (CM1.4).
    same_branch_m: bool,
    /// Phantom data for type state
    _state: PhantomData<State>,
}

impl Compressor<Connected> {
    /// Returns both ports as a slice for solver topology.
    ///
    /// # Note
    ///
    /// This creates a temporary array on each call. For better performance
    /// in hot loops, cache the ports directly via `port_suction()` and
    /// `port_discharge()` methods.
    pub fn get_ports_slice(&self) -> [&Port<Connected>; 2] {
        [&self.port_suction, &self.port_discharge]
    }
}

impl Compressor<Disconnected> {
    /// Creates a new disconnected compressor with AHRI 540 model.
    ///
    /// The compressor must have its ports connected before use in simulations.
    ///
    /// # Arguments
    ///
    /// * `coefficients` - AHRI 540 performance coefficients
    /// * `port_suction` - Suction port (disconnected)
    /// * `port_discharge` - Discharge port (disconnected)
    /// * `speed_rpm` - Rotational speed in RPM
    /// * `displacement_m3_per_rev` - Displacement volume in m³/rev
    /// * `mechanical_efficiency` - Mechanical efficiency (0.0 to 1.0)
    ///
    /// # Errors
    ///
    /// Returns an error if:
    /// - Coefficients are invalid
    /// - Speed is negative or zero
    /// - Displacement is negative or zero
    /// - Mechanical efficiency is outside [0, 1]
    /// - Ports have different fluid types
    pub fn new(
        coefficients: Ahri540Coefficients,
        port_suction: Port<Disconnected>,
        port_discharge: Port<Disconnected>,
        speed_rpm: f64,
        displacement_m3_per_rev: f64,
        mechanical_efficiency: f64,
    ) -> Result<Self, ComponentError> {
        Self::with_model(
            CompressorModel::Ahri540(coefficients),
            port_suction,
            port_discharge,
            speed_rpm,
            displacement_m3_per_rev,
            mechanical_efficiency,
        )
    }

    /// Creates a new disconnected compressor with a specified model.
    ///
    /// Use this constructor to select between AHRI 540 and SST/SDT polynomial models.
    ///
    /// # Arguments
    ///
    /// * `model` - Compressor performance model (AHRI 540 or SST/SDT)
    /// * `port_suction` - Suction port (disconnected)
    /// * `port_discharge` - Discharge port (disconnected)
    /// * `speed_rpm` - Rotational speed in RPM
    /// * `displacement_m3_per_rev` - Displacement volume in m³/rev
    /// * `mechanical_efficiency` - Mechanical efficiency (0.0 to 1.0)
    ///
    /// # Errors
    ///
    /// Returns an error if:
    /// - Model coefficients are invalid
    /// - Speed is negative or zero
    /// - Displacement is negative or zero
    /// - Mechanical efficiency is outside [0, 1]
    /// - Ports have different fluid types
    ///
    /// # Example
    ///
    /// ```
    /// use entropyk_components::compressor::{Compressor, CompressorModel, SstSdtCoefficients};
    /// use entropyk_components::port::{FluidId, Port};
    /// use entropyk_core::{Pressure, Enthalpy};
    ///
    /// // Create SST/SDT polynomial model
    /// let sst_sdt = SstSdtCoefficients::bilinear(
    ///     0.05, 0.001, 0.0005, 0.00001,   // mass flow coefficients
    ///     1000.0, 50.0, 30.0, 0.5         // power coefficients
    /// );
    ///
    /// let suction = Port::new(
    ///     FluidId::new("R134a"),
    ///     Pressure::from_bar(3.5),
    ///     Enthalpy::from_joules_per_kg(400000.0)
    /// );
    /// let discharge = Port::new(
    ///     FluidId::new("R134a"),
    ///     Pressure::from_bar(10.0),
    ///     Enthalpy::from_joules_per_kg(450000.0)
    /// );
    ///
    /// // Create compressor with SST/SDT model
    /// let compressor = Compressor::with_model(
    ///     CompressorModel::SstSdt(sst_sdt),
    ///     suction,
    ///     discharge,
    ///     2900.0,    // RPM
    ///     0.0001,    // Displacement volume (m³/rev)
    ///     0.85       // Mechanical efficiency
    /// ).unwrap();
    /// ```
    pub fn with_model(
        model: CompressorModel,
        port_suction: Port<Disconnected>,
        port_discharge: Port<Disconnected>,
        speed_rpm: f64,
        displacement_m3_per_rev: f64,
        mechanical_efficiency: f64,
    ) -> Result<Self, ComponentError> {
        // Validate model coefficients
        match &model {
            CompressorModel::Ahri540(coeffs) => coeffs.validate()?,
            CompressorModel::SstSdt(coeffs) => coeffs.validate()?,
        }

        // Validate speed
        if speed_rpm <= 0.0 {
            return Err(ComponentError::InvalidState(
                "Compressor speed must be positive".to_string(),
            ));
        }

        // Validate displacement
        if displacement_m3_per_rev <= 0.0 {
            return Err(ComponentError::InvalidState(
                "Displacement volume must be positive".to_string(),
            ));
        }

        // Validate efficiency
        if !(0.0..=1.0).contains(&mechanical_efficiency) {
            return Err(ComponentError::InvalidState(
                "Mechanical efficiency must be between 0.0 and 1.0".to_string(),
            ));
        }

        // Validate fluid compatibility
        if port_suction.fluid_id() != port_discharge.fluid_id() {
            return Err(ComponentError::InvalidState(
                "Suction and discharge ports must have the same fluid type".to_string(),
            ));
        }

        let fluid_id = port_suction.fluid_id().clone();

        Ok(Self {
            model,
            port_suction,
            port_discharge,
            speed_rpm,
            displacement_m3_per_rev,
            mechanical_efficiency,
            calib: Calib::default(),
            calib_indices: entropyk_core::CalibIndices::default(),
            fluid_id,

            circuit_id: CircuitId::default(), // Default circuit
            operational_state: OperationalState::default(), // Default to On
            suction_m_idx: None,
            suction_h_idx: None,
            discharge_m_idx: None,
            discharge_h_idx: None,
            same_branch_m: false,
            _state: PhantomData,
        })
    }

    /// Returns the fluid identifier.
    pub fn fluid_id(&self) -> &FluidId {
        &self.fluid_id
    }

    /// Returns the suction port.
    pub fn port_suction(&self) -> &Port<Disconnected> {
        &self.port_suction
    }

    /// Returns the discharge port.
    pub fn port_discharge(&self) -> &Port<Disconnected> {
        &self.port_discharge
    }

    /// Returns the rotational speed in RPM.
    pub fn speed_rpm(&self) -> f64 {
        self.speed_rpm
    }

    /// Returns the displacement volume in m³/rev.
    pub fn displacement_m3_per_rev(&self) -> f64 {
        self.displacement_m3_per_rev
    }

    /// Returns the mechanical efficiency.
    pub fn mechanical_efficiency(&self) -> f64 {
        self.mechanical_efficiency
    }

    /// Returns the compressor model (AHRI 540 or SST/SDT).
    pub fn model(&self) -> &CompressorModel {
        &self.model
    }

    /// Returns the AHRI 540 coefficients if using AHRI 540 model.
    ///
    /// # Returns
    ///
    /// `Some(&Ahri540Coefficients)` if the model is AHRI 540, `None` otherwise.
    pub fn ahri540_coefficients(&self) -> Option<&Ahri540Coefficients> {
        match &self.model {
            CompressorModel::Ahri540(coeffs) => Some(coeffs),
            _ => None,
        }
    }

    /// Returns the SST/SDT coefficients if using SST/SDT polynomial model.
    ///
    /// # Returns
    ///
    /// `Some(&SstSdtCoefficients)` if the model is SST/SDT, `None` otherwise.
    pub fn sst_sdt_coefficients(&self) -> Option<&SstSdtCoefficients> {
        match &self.model {
            CompressorModel::SstSdt(coeffs) => Some(coeffs),
            _ => None,
        }
    }

    /// Returns calibration factors (f_m, f_power, etc.).
    pub fn calib(&self) -> &Calib {
        &self.calib
    }

    /// Sets calibration factors.
    pub fn set_calib(&mut self, calib: Calib) {
        self.calib = calib;
    }

    /// Returns the circuit identifier.
    pub fn circuit_id(&self) -> &CircuitId {
        &self.circuit_id
    }

    /// Sets the circuit identifier.
    pub fn set_circuit_id(&mut self, circuit_id: CircuitId) {
        self.circuit_id = circuit_id;
    }

    /// Returns the operational state.
    pub fn operational_state(&self) -> OperationalState {
        self.operational_state
    }

    /// Sets the operational state.
    pub fn set_operational_state(&mut self, state: OperationalState) {
        self.operational_state = state;
    }
    /// Connects the compressor to suction and discharge ports.
    ///
    /// This consumes the disconnected compressor and returns a connected one,
    /// transitioning the state at compile time.
    pub fn connect(
        self,
        suction: Port<Disconnected>,
        discharge: Port<Disconnected>,
    ) -> Result<Compressor<Connected>, ComponentError> {
        let (p_suction, _) = self
            .port_suction
            .connect(suction)
            .map_err(|e| ComponentError::InvalidState(e.to_string()))?;
        let (p_discharge, _) = self
            .port_discharge
            .connect(discharge)
            .map_err(|e| ComponentError::InvalidState(e.to_string()))?;

        Ok(Compressor {
            model: self.model,
            port_suction: p_suction,
            port_discharge: p_discharge,
            speed_rpm: self.speed_rpm,
            displacement_m3_per_rev: self.displacement_m3_per_rev,
            mechanical_efficiency: self.mechanical_efficiency,
            calib: self.calib,
            calib_indices: self.calib_indices,
            fluid_id: self.fluid_id,
            circuit_id: self.circuit_id,
            operational_state: self.operational_state,
            suction_m_idx: None,
            suction_h_idx: None,
            discharge_m_idx: None,
            discharge_h_idx: None,
            same_branch_m: false,
            _state: PhantomData,
        })
    }
}

impl Compressor<Connected> {
    /// Returns the suction port.
    pub fn port_suction(&self) -> &Port<Connected> {
        &self.port_suction
    }

    /// Returns the discharge port.
    pub fn port_discharge(&self) -> &Port<Connected> {
        &self.port_discharge
    }

    /// Computes the full thermodynamic state at the suction port.
    pub fn suction_state(
        &self,
        backend: &impl entropyk_fluids::FluidBackend,
    ) -> Result<entropyk_fluids::ThermoState, ComponentError> {
        backend
            .full_state(
                entropyk_fluids::FluidId::new(self.port_suction.fluid_id().as_str()),
                self.port_suction.pressure(),
                self.port_suction.enthalpy(),
            )
            .map_err(|e| {
                ComponentError::from_fluid_error_context("Failed to compute suction state", e)
            })
    }

    /// Computes the full thermodynamic state at the discharge port.
    pub fn discharge_state(
        &self,
        backend: &impl entropyk_fluids::FluidBackend,
    ) -> Result<entropyk_fluids::ThermoState, ComponentError> {
        backend
            .full_state(
                entropyk_fluids::FluidId::new(self.port_discharge.fluid_id().as_str()),
                self.port_discharge.pressure(),
                self.port_discharge.enthalpy(),
            )
            .map_err(|e| {
                ComponentError::from_fluid_error_context("Failed to compute discharge state", e)
            })
    }

    /// Calculates the mass flow rate through the compressor.
    ///
    /// Uses the selected model (AHRI 540 or SST/SDT):
    /// - AHRI 540: ṁ = M1 × (1 - (P_suction/P_discharge)^(1/M2)) × ρ_suction × V_disp × N/60
    /// - SST/SDT: ṁ = Σ a_ij × SST^i × SDT^j
    ///
    /// # Arguments
    ///
    /// * `density_suction` - Suction gas density in kg/m³ (used for AHRI 540 model)
    /// * `sst_k` - Saturated suction temperature in Kelvin (used for SST/SDT model)
    /// * `sdt_k` - Saturated discharge temperature in Kelvin (used for SST/SDT model)
    ///
    /// # Returns
    ///
    /// Returns the mass flow rate as [`MassFlow`].
    ///
    /// # Errors
    ///
    /// Returns an error if the calculation results in numerical errors
    /// (e.g., negative pressure ratio, division by zero).
    pub fn mass_flow_rate(
        &self,
        density_suction: f64,
        sst_k: f64,
        sdt_k: f64,
        state: Option<&StateSlice>,
    ) -> Result<MassFlow, ComponentError> {
        if density_suction < 0.0 {
            return Err(ComponentError::InvalidState(
                "Suction density cannot be negative".to_string(),
            ));
        }

        let p_suction = self.port_suction.pressure().to_pascals();
        let p_discharge = self.port_discharge.pressure().to_pascals();

        // Validate pressures
        if p_suction <= 0.0 {
            return Err(ComponentError::NumericalError(
                "Suction pressure must be positive".to_string(),
            ));
        }

        if p_discharge <= 0.0 {
            return Err(ComponentError::NumericalError(
                "Discharge pressure must be positive".to_string(),
            ));
        }

        let mass_flow_kg_per_s = match &self.model {
            CompressorModel::Ahri540(coeffs) => {
                // Calculate volumetric efficiency using inverse pressure ratio
                // η_vol = f_etav × (1 - (P_suction/P_discharge)^(1/M2))
                // f_etav is read dynamically from the state vector when wired
                // as a calibration unknown (Story 5.5), like f_m / f_power.
                let f_etav = state
                    .and_then(|st| self.calib_indices.z_etav.map(|idx| st[idx]))
                    .unwrap_or(self.calib.z_etav);
                let inverse_pressure_ratio = p_suction / p_discharge;
                let volumetric_efficiency =
                    (1.0 - inverse_pressure_ratio.powf(1.0 / coeffs.m2)) * f_etav;

                if volumetric_efficiency < 0.0 {
                    return Err(ComponentError::NumericalError(
                        "Volumetric efficiency is negative - check pressure ratio and M2 coefficient"
                            .to_string(),
                    ));
                }

                // Convert RPM to rev/s
                let speed_rev_per_s = self.speed_rpm / 60.0;

                // Calculate mass flow (AHRI 540 nominal)
                coeffs.m1
                    * volumetric_efficiency
                    * density_suction
                    * self.displacement_m3_per_rev
                    * speed_rev_per_s
            }
            CompressorModel::SstSdt(coeffs) => {
                // SST/SDT polynomial model
                coeffs.mass_flow_at(sst_k, sdt_k)
            }
        };

        // Apply calibration: ṁ_eff = f_m × ṁ_nominal
        let f_m = if let Some(st) = state {
            self.calib_indices
                .z_flow
                .map(|idx| st[idx])
                .unwrap_or(self.calib.z_flow)
        } else {
            self.calib.z_flow
        };
        Ok(MassFlow::from_kg_per_s(mass_flow_kg_per_s * f_m))
    }

    /// Calculates the power consumption (cooling mode).
    ///
    /// Uses the selected model:
    /// - AHRI 540: Ẇ = M3 + M4 × (P_discharge/P_suction) + M5 × T_suction + M6 × T_discharge
    /// - SST/SDT: Ẇ = Σ b_ij × SST^i × SDT^j
    ///
    /// # Arguments
    ///
    /// * `t_suction` - Suction temperature
    /// * `t_discharge` - Discharge temperature
    ///
    /// # Returns
    ///
    /// Returns the power consumption in Watts.
    pub fn power_consumption_cooling(
        &self,
        t_suction: Temperature,
        t_discharge: Temperature,
        state: Option<&StateSlice>,
    ) -> f64 {
        let power_nominal = match &self.model {
            CompressorModel::Ahri540(coeffs) => {
                let pressure_ratio = self.port_discharge.pressure().to_pascals()
                    / self.port_suction.pressure().to_pascals();
                coeffs.m3
                    + coeffs.m4 * pressure_ratio
                    + coeffs.m5 * t_suction.to_kelvin()
                    + coeffs.m6 * t_discharge.to_kelvin()
            }
            CompressorModel::SstSdt(coeffs) => {
                coeffs.power_at(t_suction.to_kelvin(), t_discharge.to_kelvin())
            }
        };
        // Ẇ_eff = f_power × Ẇ_nominal
        let f_power = if let Some(st) = state {
            self.calib_indices
                .z_power
                .map(|idx| st[idx])
                .unwrap_or(self.calib.z_power)
        } else {
            self.calib.z_power
        };
        power_nominal * f_power
    }

    /// Calculates the power consumption (heating mode).
    ///
    /// Uses the selected model:
    /// - AHRI 540: Ẇ = M7 + M8 × (P_discharge/P_suction) + M9 × T_suction + M10 × T_discharge
    /// - SST/SDT: Same as cooling mode (SST/SDT model doesn't distinguish modes)
    ///
    /// # Arguments
    ///
    /// * `t_suction` - Suction temperature
    /// * `t_discharge` - Discharge temperature
    ///
    /// # Returns
    ///
    /// Returns the power consumption in Watts.
    pub fn power_consumption_heating(
        &self,
        t_suction: Temperature,
        t_discharge: Temperature,
        state: Option<&StateSlice>,
    ) -> f64 {
        let power_nominal = match &self.model {
            CompressorModel::Ahri540(coeffs) => {
                let pressure_ratio = self.port_discharge.pressure().to_pascals()
                    / self.port_suction.pressure().to_pascals();
                coeffs.m7
                    + coeffs.m8 * pressure_ratio
                    + coeffs.m9 * t_suction.to_kelvin()
                    + coeffs.m10 * t_discharge.to_kelvin()
            }
            CompressorModel::SstSdt(coeffs) => {
                // SST/SDT model doesn't distinguish between cooling and heating
                coeffs.power_at(t_suction.to_kelvin(), t_discharge.to_kelvin())
            }
        };
        // Ẇ_eff = f_power × Ẇ_nominal
        let f_power = if let Some(st) = state {
            self.calib_indices
                .z_power
                .map(|idx| st[idx])
                .unwrap_or(self.calib.z_power)
        } else {
            self.calib.z_power
        };
        power_nominal * f_power
    }

    /// Calculates the cooling capacity.
    ///
    /// Q̇_cool = ṁ × (h_evap_out - h_evap_in)
    ///
    /// # Arguments
    ///
    /// * `mass_flow` - Mass flow rate
    /// * `h_evap_in` - Evaporator inlet enthalpy
    /// * `h_evap_out` - Evaporator outlet enthalpy
    ///
    /// # Returns
    ///
    /// Returns the cooling capacity in Watts.
    pub fn cooling_capacity(
        &self,
        mass_flow: MassFlow,
        h_evap_in: Enthalpy,
        h_evap_out: Enthalpy,
    ) -> f64 {
        mass_flow.to_kg_per_s() * (h_evap_out.to_joules_per_kg() - h_evap_in.to_joules_per_kg())
    }

    /// Calculates the heating capacity.
    ///
    /// Q̇_heat = ṁ × (h_cond_out - h_cond_in)
    ///
    /// # Arguments
    ///
    /// * `mass_flow` - Mass flow rate
    /// * `h_cond_in` - Condenser inlet enthalpy
    /// * `h_cond_out` - Condenser outlet enthalpy
    ///
    /// # Returns
    ///
    /// Returns the heating capacity in Watts.
    pub fn heating_capacity(
        &self,
        mass_flow: MassFlow,
        h_cond_in: Enthalpy,
        h_cond_out: Enthalpy,
    ) -> f64 {
        mass_flow.to_kg_per_s() * (h_cond_out.to_joules_per_kg() - h_cond_in.to_joules_per_kg())
    }

    /// Calculates the Coefficient of Performance (COP).
    ///
    /// COP = Q̇ / Ẇ
    ///
    /// # Arguments
    ///
    /// * `capacity` - Cooling or heating capacity in Watts
    /// * `power` - Power consumption in Watts
    ///
    /// # Returns
    ///
    /// Returns the COP. Returns an error if power is zero or negative.
    pub fn coefficient_of_performance(
        &self,
        capacity: f64,
        power: f64,
    ) -> Result<f64, ComponentError> {
        if power <= 0.0 {
            return Err(ComponentError::NumericalError(
                "Power must be positive for COP calculation".to_string(),
            ));
        }
        Ok(capacity / power)
    }

    /// Returns the rotational speed in RPM.
    pub fn speed_rpm(&self) -> f64 {
        self.speed_rpm
    }

    /// Returns the displacement volume in m³/rev.
    pub fn displacement_m3_per_rev(&self) -> f64 {
        self.displacement_m3_per_rev
    }

    /// Returns the mechanical efficiency.
    pub fn mechanical_efficiency(&self) -> f64 {
        self.mechanical_efficiency
    }

    /// Returns the compressor model (AHRI 540 or SST/SDT).
    pub fn model(&self) -> &CompressorModel {
        &self.model
    }

    /// Returns the AHRI 540 coefficients if using AHRI 540 model.
    ///
    /// # Returns
    ///
    /// `Some(&Ahri540Coefficients)` if the model is AHRI 540, `None` otherwise.
    pub fn ahri540_coefficients(&self) -> Option<&Ahri540Coefficients> {
        match &self.model {
            CompressorModel::Ahri540(coeffs) => Some(coeffs),
            _ => None,
        }
    }

    /// Returns the SST/SDT coefficients if using SST/SDT polynomial model.
    ///
    /// # Returns
    ///
    /// `Some(&SstSdtCoefficients)` if the model is SST/SDT, `None` otherwise.
    pub fn sst_sdt_coefficients(&self) -> Option<&SstSdtCoefficients> {
        match &self.model {
            CompressorModel::SstSdt(coeffs) => Some(coeffs),
            _ => None,
        }
    }

    /// Returns calibration factors (f_m, f_power, etc.).
    pub fn calib(&self) -> &Calib {
        &self.calib
    }

    /// Sets calibration factors.
    pub fn set_calib(&mut self, calib: Calib) {
        self.calib = calib;
    }

    /// Returns the circuit identifier.
    pub fn circuit_id(&self) -> &CircuitId {
        &self.circuit_id
    }

    /// Sets the circuit identifier.
    pub fn set_circuit_id(&mut self, circuit_id: CircuitId) {
        self.circuit_id = circuit_id;
    }

    /// Returns the operational state.
    pub fn operational_state(&self) -> OperationalState {
        self.operational_state
    }

    /// Sets the operational state.
    pub fn set_operational_state(&mut self, state: OperationalState) {
        self.operational_state = state;
    }

    /// Live energy-retention factor \(f_w\): fraction of shaft work kept in the
    /// refrigerant (`1` = adiabatic, `0` = all work lost to ambient).
    ///
    /// Reads `state[calib_indices.f_w]` when free; otherwise stored calib.
    /// Clamped to [0, 1].
    fn live_f_w(&self, state: Option<&StateSlice>) -> f64 {
        let raw = if let Some(st) = state {
            self.calib_indices
                .f_w
                .map(|idx| st.get(idx).copied().unwrap_or(self.calib.f_w))
                .unwrap_or(self.calib.f_w)
        } else {
            self.calib.f_w
        };
        raw.clamp(0.0, 1.0)
    }
}

impl Component for Compressor<Connected> {
    fn set_system_context(
        &mut self,
        _state_offset: usize,
        external_edge_state_indices: &[(usize, usize, usize)],
    ) {
        // Layout: [0] = incoming suction edge, [1] = outgoing discharge edge.
        // Triple: (m_idx, p_idx, h_idx)
        if !external_edge_state_indices.is_empty() {
            self.suction_m_idx = Some(external_edge_state_indices[0].0);
            self.suction_h_idx = Some(external_edge_state_indices[0].2);
        }
        if external_edge_state_indices.len() >= 2 {
            self.discharge_m_idx = Some(external_edge_state_indices[1].0);
            self.discharge_h_idx = Some(external_edge_state_indices[1].2);
        }
        // CM1.4: detect same-branch topology → conservation residual becomes trivial.
        self.same_branch_m = matches!(
            (self.suction_m_idx, self.discharge_m_idx),
            (Some(suc), Some(dis)) if suc == dis
        );
    }

    fn compute_residuals(
        &self,
        state: &StateSlice,
        residuals: &mut ResidualVector,
    ) -> Result<(), ComponentError> {
        // Validate residual vector length
        if residuals.len() != self.n_equations() {
            return Err(ComponentError::InvalidResidualDimensions {
                expected: self.n_equations(),
                actual: residuals.len(),
            });
        }

        let (suc_m_idx, suc_h_idx, dis_m_idx, dis_h_idx) = match (
            self.suction_m_idx,
            self.suction_h_idx,
            self.discharge_m_idx,
            self.discharge_h_idx,
        ) {
            (Some(sm), Some(sh), Some(dm), Some(dh)) => (sm, sh, dm, dh),
            _ => {
                return Err(ComponentError::InvalidState(
                    "Compressor requires live suction/discharge mass-flow and enthalpy indices"
                        .to_string(),
                ));
            }
        };

        // Handle operational states (FR6-FR8)
        match self.operational_state {
            OperationalState::Off => {
                // In Off state, mass flow is zero (FR7)
                residuals[0] = state[suc_m_idx]; // ṁ_suction = 0
                residuals[1] = 0.0; // No energy transfer
                residuals[2] = state[dis_m_idx] - state[suc_m_idx]; // mass conservation
                return Ok(());
            }
            OperationalState::Bypass => {
                // In Bypass state, behaves as adiabatic pipe (FR8)
                let p_suction = self.port_suction.pressure().to_pascals();
                let p_discharge = self.port_discharge.pressure().to_pascals();
                let h_suction = self.port_suction.enthalpy().to_joules_per_kg();
                let h_discharge = self.port_discharge.enthalpy().to_joules_per_kg();

                residuals[0] = p_suction - p_discharge; // Pressure continuity
                residuals[1] = h_suction - h_discharge; // Enthalpy continuity (adiabatic)
                residuals[2] = state[dis_m_idx] - state[suc_m_idx]; // mass conservation
                return Ok(());
            }
            OperationalState::On => {
                // Normal operation - continue with AHRI 540 calculations
            }
        }

        // Extract state variables using stored edge indices (CM1.3)
        let mass_flow_state = state[suc_m_idx]; // kg/s — ṁ at suction edge
        let h_suction = state[suc_h_idx]; // J/kg
        let h_discharge = state[dis_h_idx]; // J/kg

        // Get port values
        let p_suction = self.port_suction.pressure().to_pascals();
        let p_discharge = self.port_discharge.pressure().to_pascals();

        // Calculate temperatures for SST/SDT model
        let t_suction_k = estimate_temperature(self.fluid_id.as_str(), p_suction, h_suction)?;
        let t_discharge_k = estimate_temperature(self.fluid_id.as_str(), p_discharge, h_discharge)?;

        // Calculate mass flow from selected model
        // For now, we use a simplified density calculation
        // In the future, this will come from the fluid property backend
        let density_suction = estimate_density(self.fluid_id.as_str(), p_suction, h_suction)?;
        let mass_flow_calc = self
            .mass_flow_rate(density_suction, t_suction_k, t_discharge_k, Some(state))?
            .to_kg_per_s();

        // Calculate power consumption
        let power_calc = self.power_consumption_cooling(
            Temperature::from_kelvin(t_suction_k),
            Temperature::from_kelvin(t_discharge_k),
            Some(state),
        );

        // Residual 0: Mass flow continuity
        // ṁ_calc - ṁ_state = 0
        residuals[0] = mass_flow_calc - mass_flow_state;

        // Residual 1: Energy balance with retention factor f_w
        // (fraction of shaft work kept in the refrigerant):
        //   ṁ·Δh = Ẇ·f_w / η_mech  ⇒  Ẇ·f_w − ṁ·Δh/η_mech = 0
        // so h_dis ≈ h_suc + f_w·Ẇ/(ṁ·η_mech).
        // f_w = 1 → adiabatic (default); f_w = 0 → all work lost to ambient.
        let enthalpy_change = h_discharge - h_suction;
        let f_w = self.live_f_w(Some(state));

        // Prevent division by zero
        if self.mechanical_efficiency.abs() < 1e-10 {
            return Err(ComponentError::InvalidState(
                "Mechanical efficiency is too close to zero".to_string(),
            ));
        }

        residuals[1] =
            power_calc * f_w - mass_flow_state * enthalpy_change / self.mechanical_efficiency;

        // r2: ṁ_discharge − ṁ_suction = 0  (mass conservation, CM1.3)
        // CM1.4: skip when same_branch_m — ṁ_dis == ṁ_suc (same state index),
        // so the residual would be trivially 0 and is excluded from n_equations().
        if !self.same_branch_m {
            residuals[2] = state[dis_m_idx] - state[suc_m_idx];
        }

        Ok(())
    }

    fn jacobian_entries(
        &self,
        state: &StateSlice,
        jacobian: &mut JacobianBuilder,
    ) -> Result<(), ComponentError> {
        let (suc_m_idx, suc_h_idx, dis_m_idx, dis_h_idx) = match (
            self.suction_m_idx,
            self.suction_h_idx,
            self.discharge_m_idx,
            self.discharge_h_idx,
        ) {
            (Some(sm), Some(sh), Some(dm), Some(dh)) => (sm, sh, dm, dh),
            _ => {
                return Err(ComponentError::InvalidState(
                    "Compressor Jacobian requires live suction/discharge mass-flow and enthalpy indices".to_string(),
                ));
            }
        };

        let mass_flow_state = state[suc_m_idx];
        let h_suction = state[suc_h_idx];
        let h_discharge = state[dis_h_idx];

        // Get port values
        let p_suction = self.port_suction.pressure().to_pascals();
        let p_discharge = self.port_discharge.pressure().to_pascals();

        let t_suction_k = estimate_temperature(self.fluid_id.as_str(), p_suction, h_suction)?;
        let t_discharge_k = estimate_temperature(self.fluid_id.as_str(), p_discharge, h_discharge)?;

        // Row 0: Mass flow residual  r0 = ṁ_calc − ṁ_suction
        // ∂r0/∂ṁ_suction = -1  (exact, CM1.3)
        jacobian.add_entry(0, suc_m_idx, -1.0);

        // ∂r0/∂h_suction — numerical (density depends on h)
        let dr0_dh_suction = approximate_derivative_result(
            |h| {
                let density = estimate_density(self.fluid_id.as_str(), p_suction, h)?;
                let t_k = estimate_temperature(self.fluid_id.as_str(), p_suction, h)?;
                self.mass_flow_rate(density, t_k, t_discharge_k, Some(state))
                    .map(|m| m.to_kg_per_s())
                    .map_err(|e| ComponentError::CalculationFailed(e.to_string()))
            },
            h_suction,
            1.0,
        )?;
        jacobian.add_entry(0, suc_h_idx, dr0_dh_suction);

        // Row 1: Energy residual  r1 = power·f_w − ṁ·Δh/η_mech
        let f_w = self.live_f_w(Some(state));
        // ∂r1/∂ṁ_suction = −(h_discharge − h_suction) / η_mech
        let dr1_dm = -(h_discharge - h_suction) / self.mechanical_efficiency;
        jacobian.add_entry(1, suc_m_idx, dr1_dm);

        // ∂r1/∂h_suction — numerical
        let dr1_dh_suction = approximate_derivative_result(
            |h| {
                let t = estimate_temperature(self.fluid_id.as_str(), p_suction, h)?;
                let t_discharge =
                    estimate_temperature(self.fluid_id.as_str(), p_discharge, h_discharge)?;
                Ok(self.power_consumption_cooling(
                    Temperature::from_kelvin(t),
                    Temperature::from_kelvin(t_discharge),
                    None,
                ) * f_w)
            },
            h_suction,
            1.0,
        )? + mass_flow_state / self.mechanical_efficiency;
        jacobian.add_entry(1, suc_h_idx, dr1_dh_suction);

        // ∂r1/∂h_discharge — numerical
        let dr1_dh_discharge = approximate_derivative_result(
            |h| {
                let t_suction = estimate_temperature(self.fluid_id.as_str(), p_suction, h_suction)?;
                let t = estimate_temperature(self.fluid_id.as_str(), p_discharge, h)?;
                Ok(self.power_consumption_cooling(
                    Temperature::from_kelvin(t_suction),
                    Temperature::from_kelvin(t),
                    None,
                ) * f_w)
            },
            h_discharge,
            1.0,
        )? - mass_flow_state / self.mechanical_efficiency;
        jacobian.add_entry(1, dis_h_idx, dr1_dh_discharge);

        // Row 2: Mass conservation  r2 = ṁ_discharge − ṁ_suction  (exact, CM1.3)
        // CM1.4: omit when same_branch_m (trivial equation removed from n_equations).
        if !self.same_branch_m {
            jacobian.add_entry(2, dis_m_idx, 1.0);
            jacobian.add_entry(2, suc_m_idx, -1.0);
        }

        // Calibration derivatives (Story 5.5)
        if let Some(z_flow_idx) = self.calib_indices.z_flow {
            let density_suction = estimate_density(self.fluid_id.as_str(), p_suction, h_suction)?;
            let m_nominal = self
                .mass_flow_rate(density_suction, t_suction_k, t_discharge_k, None)
                .map(|m| m.to_kg_per_s())
                .map_err(|e| ComponentError::CalculationFailed(e.to_string()))?;
            jacobian.add_entry(0, z_flow_idx, m_nominal);
        }

        if let Some(z_power_idx) = self.calib_indices.z_power {
            let p_nominal = self.power_consumption_cooling(
                Temperature::from_kelvin(t_suction_k),
                Temperature::from_kelvin(t_discharge_k),
                None,
            );
            // r1 = (z_power·Ẇ_nom)·f_w − …  ⇒  ∂r1/∂z_power = Ẇ_nom·f_w
            jacobian.add_entry(1, z_power_idx, p_nominal * f_w);
        }

        if let Some(f_w_idx) = self.calib_indices.f_w {
            let p_live = self.power_consumption_cooling(
                Temperature::from_kelvin(t_suction_k),
                Temperature::from_kelvin(t_discharge_k),
                Some(state),
            );
            // r1 = Ẇ·f_w − …  ⇒  ∂r1/∂f_w = +Ẇ
            jacobian.add_entry(1, f_w_idx, p_live);
        }

        // ∂r0/∂f_etav (AHRI 540 only): ṁ_calc = f_m · f_etav · base with
        // base = M1 · (1 − (P_suc/P_dis)^(1/M2)) · ρ_suc · V_disp · N/60,
        // so the derivative is exactly f_m · base.
        if let Some(z_etav_idx) = self.calib_indices.z_etav {
            if let CompressorModel::Ahri540(coeffs) = &self.model {
                let density_suction =
                    estimate_density(self.fluid_id.as_str(), p_suction, h_suction)?;
                let inverse_pressure_ratio = p_suction / p_discharge;
                let base = coeffs.m1
                    * (1.0 - inverse_pressure_ratio.powf(1.0 / coeffs.m2))
                    * density_suction
                    * self.displacement_m3_per_rev
                    * (self.speed_rpm / 60.0);
                let f_m = self
                    .calib_indices
                    .z_flow
                    .map(|idx| state[idx])
                    .unwrap_or(self.calib.z_flow);
                jacobian.add_entry(0, z_etav_idx, f_m * base);
            }
        }

        Ok(())
    }

    fn n_equations(&self) -> usize {
        // CM1.4: when suction and discharge share the same ṁ state index (same
        // series branch), the conservation residual r[2] = ṁ_dis − ṁ_suc is
        // trivially 0 and must be dropped to keep the system square.
        if self.same_branch_m {
            2
        } else {
            3
        }
    }

    fn port_mass_flows(
        &self,
        state: &StateSlice,
    ) -> Result<Vec<entropyk_core::MassFlow>, ComponentError> {
        let suc_m_idx = self.suction_m_idx.ok_or_else(|| {
            ComponentError::InvalidState(
                "Compressor mass-flow reporting requires a live suction mass-flow index"
                    .to_string(),
            )
        })?;
        let m = entropyk_core::MassFlow::from_kg_per_s(state[suc_m_idx]);
        // Suction (inlet), Discharge (outlet), Oil (no flow modeled yet)
        Ok(vec![
            m,
            entropyk_core::MassFlow::from_kg_per_s(-m.to_kg_per_s()),
            entropyk_core::MassFlow::from_kg_per_s(0.0),
        ])
    }

    fn port_enthalpies(
        &self,
        state: &StateSlice,
    ) -> Result<Vec<entropyk_core::Enthalpy>, ComponentError> {
        if state.len() < 4 {
            return Err(ComponentError::InvalidStateDimensions {
                expected: 4,
                actual: state.len(),
            });
        }
        Ok(vec![
            entropyk_core::Enthalpy::from_joules_per_kg(state[1]),
            entropyk_core::Enthalpy::from_joules_per_kg(state[2]),
            entropyk_core::Enthalpy::from_joules_per_kg(0.0),
        ])
    }

    fn get_ports(&self) -> &[ConnectedPort] {
        // FIXME: API LIMITATION - This method returns an empty slice due to lifetime constraints.
        //
        // The Component trait's get_ports() requires returning a reference with the same
        // lifetime as &self, but the actual port storage (in Compressor<Connected>) has
        // a different lifetime. This is a fundamental design issue in the trait.
        //
        // WORKAROUND: Use `get_ports_slice()` method on Compressor<Connected> for actual port access.
        //
        // TODO: Redesign Component trait to support owned port iterators or different lifetime bounds.
        // See: https://github.com/your-org/entropyk/issues/XXX
        &[]
    }

    fn energy_transfers(
        &self,
        state: &StateSlice,
    ) -> Option<(entropyk_core::Power, entropyk_core::Power)> {
        match self.operational_state {
            OperationalState::Off | OperationalState::Bypass => Some((
                entropyk_core::Power::from_watts(0.0),
                entropyk_core::Power::from_watts(0.0),
            )),
            OperationalState::On => {
                if state.len() < 4 {
                    return None;
                }
                let h_suction = state[1]; // J/kg
                let h_discharge = state[2]; // J/kg

                let p_suction = self.port_suction.pressure().to_pascals();
                let p_discharge = self.port_discharge.pressure().to_pascals();

                let t_suction_k =
                    estimate_temperature(self.fluid_id.as_str(), p_suction, h_suction)
                        .unwrap_or(273.15);
                let t_discharge_k =
                    estimate_temperature(self.fluid_id.as_str(), p_discharge, h_discharge)
                        .unwrap_or(320.0);

                let power_calc = self.power_consumption_cooling(
                    Temperature::from_kelvin(t_suction_k),
                    Temperature::from_kelvin(t_discharge_k),
                    Some(state),
                );

                // Work is done *on* the compressor, so it is negative
                Some((
                    entropyk_core::Power::from_watts(0.0),
                    entropyk_core::Power::from_watts(-power_calc),
                ))
            }
        }
    }

    fn signature(&self) -> String {
        format!(
            "Compressor(fluid={}, circuit={})",
            self.fluid_id.as_str(),
            self.circuit_id.0
        )
    }

    fn to_params(&self) -> crate::ComponentParams {
        use crate::ComponentParams;
        let mut params = ComponentParams::new("Compressor")
            .with_param("fluid", self.fluid_id.as_str())
            .with_param("circuitId", self.circuit_id.0)
            .with_param("speedRpm", self.speed_rpm)
            .with_param("displacementM3PerRev", self.displacement_m3_per_rev)
            .with_param("mechanicalEfficiency", self.mechanical_efficiency)
            .with_param(
                "calib",
                serde_json::to_value(&self.calib).unwrap_or(serde_json::Value::Null),
            );
        match &self.model {
            CompressorModel::Ahri540(c) => {
                params = params
                    .with_param("modelType", "Ahri540")
                    .with_param("m1", c.m1)
                    .with_param("m2", c.m2)
                    .with_param("m3", c.m3)
                    .with_param("m4", c.m4)
                    .with_param("m5", c.m5)
                    .with_param("m6", c.m6)
                    .with_param("m7", c.m7)
                    .with_param("m8", c.m8)
                    .with_param("m9", c.m9)
                    .with_param("m10", c.m10);
            }
            CompressorModel::SstSdt(c) => {
                params = params
                    .with_param("modelType", "SstSdt")
                    .with_param(
                        "massFlowCurve",
                        serde_json::to_value(&c.mass_flow_curve).unwrap_or(serde_json::Value::Null),
                    )
                    .with_param(
                        "powerCurve",
                        serde_json::to_value(&c.power_curve).unwrap_or(serde_json::Value::Null),
                    );
            }
        }
        params
    }

    fn update_calib_factor(&mut self, factor: &str, value: f64) -> bool {
        let mut c = self.calib().clone();
        if c.set_factor(factor, value) {
            self.set_calib(c);
            true
        } else {
            false
        }
    }

    fn set_calib_indices(&mut self, indices: entropyk_core::CalibIndices) {
        // Without this override the solver's finalize() wiring was silently
        // dropped (trait default is a no-op): dynamic calibration factors
        // (f_m, f_power, f_etav) registered as bounded variables never reached
        // compute_residuals/jacobian_entries, which already read
        // `self.calib_indices` (mass_flow_rate / power_consumption_cooling).
        self.calib_indices = indices;
    }

    fn measure_output(&self, kind: crate::MeasuredOutput, state: &StateSlice) -> Option<f64> {
        use crate::MeasuredOutput;
        match kind {
            // Discharge gas temperature (DGT) so a controls[] loop can hold a
            // maximum-DGT limit on the AHRI-540 compressor (like the
            // IsentropicCompressor's liquid-injection loop).
            MeasuredOutput::Temperature => {
                let dis_h_idx = self.discharge_h_idx?;
                if dis_h_idx >= state.len() {
                    return None;
                }
                let h_discharge = state[dis_h_idx];
                let p_discharge = self.port_discharge.pressure().to_pascals();
                if !h_discharge.is_finite() || p_discharge <= 0.0 {
                    return None;
                }
                estimate_temperature(self.fluid_id.as_str(), p_discharge, h_discharge)
                    .ok()
                    .filter(|t| t.is_finite())
            }
            // Refrigerant mass flow rate at the suction edge.
            MeasuredOutput::MassFlowRate => {
                let m_idx = self.suction_m_idx.or(self.discharge_m_idx)?;
                if m_idx >= state.len() {
                    return None;
                }
                let m = state[m_idx];
                m.is_finite().then_some(m)
            }
            _ => None,
        }
    }
}

use crate::state_machine::StateManageable;

impl StateManageable for Compressor<Connected> {
    fn state(&self) -> OperationalState {
        self.operational_state
    }

    fn set_state(&mut self, state: OperationalState) -> Result<(), ComponentError> {
        if self.operational_state.can_transition_to(state) {
            let from = self.operational_state;
            self.operational_state = state;
            self.on_state_change(from, state);
            Ok(())
        } else {
            Err(ComponentError::InvalidStateTransition {
                from: self.operational_state,
                to: state,
                reason: "Transition not allowed".to_string(),
            })
        }
    }

    fn can_transition_to(&self, target: OperationalState) -> bool {
        self.operational_state.can_transition_to(target)
    }

    fn circuit_id(&self) -> &CircuitId {
        &self.circuit_id
    }

    fn set_circuit_id(&mut self, circuit_id: CircuitId) {
        self.circuit_id = circuit_id;
    }
}

/// Enthalpy/density thresholds for R134a density estimation (J/kg)
mod r134a_density {
    pub const ENTHALPY_VAPOR_THRESHOLD: f64 = 350_000.0;
    pub const ENTHALPY_LIQUID_THRESHOLD: f64 = 200_000.0;
    pub const DENSITY_VAPOR: f64 = 20.0;
    pub const DENSITY_LIQUID: f64 = 1200.0;
}

/// Enthalpy/density thresholds for R410A/R454B density estimation (J/kg)
mod r410a_density {
    pub const ENTHALPY_VAPOR_THRESHOLD: f64 = 380_000.0;
    pub const ENTHALPY_LIQUID_THRESHOLD: f64 = 220_000.0;
    pub const DENSITY_VAPOR: f64 = 25.0;
    pub const DENSITY_LIQUID: f64 = 1100.0;
}

/// Estimates fluid density from pressure and enthalpy.
///
/// **PLACEHOLDER IMPLEMENTATION** - Will be replaced by CoolProp integration
/// in Story 2.2 (Fluid Properties Backend). Current implementation uses
/// rough approximations for R134a, R410A, and R454B.
///
/// # Arguments
///
/// * `fluid_id` - Fluid identifier (e.g., "R134a")
/// * `pressure` - Pressure in Pascals
/// * `enthalpy` - Specific enthalpy in J/kg
///
/// # Returns
///
/// Returns the estimated density in kg/m³.
fn estimate_density(fluid_id: &str, _pressure: f64, enthalpy: f64) -> Result<f64, ComponentError> {
    // Placeholder: simple estimation based on enthalpy for common refrigerants
    // This should be replaced with proper fluid property calculations
    match fluid_id {
        "R134a" => {
            // Rough approximation for R134a at typical conditions
            use r134a_density::*;
            let density = if enthalpy > ENTHALPY_VAPOR_THRESHOLD {
                DENSITY_VAPOR // Superheated vapor
            } else if enthalpy < ENTHALPY_LIQUID_THRESHOLD {
                DENSITY_LIQUID // Subcooled liquid
            } else {
                // Linear interpolation in two-phase region
                DENSITY_VAPOR
                    + (DENSITY_LIQUID - DENSITY_VAPOR) * (ENTHALPY_VAPOR_THRESHOLD - enthalpy)
                        / (ENTHALPY_VAPOR_THRESHOLD - ENTHALPY_LIQUID_THRESHOLD)
            };
            Ok(density)
        }
        "R410A" | "R454B" => {
            // Similar approximation for R410A and R454B (R454B is close to R410A properties)
            use r410a_density::*;
            let density = if enthalpy > ENTHALPY_VAPOR_THRESHOLD {
                DENSITY_VAPOR
            } else if enthalpy < ENTHALPY_LIQUID_THRESHOLD {
                DENSITY_LIQUID
            } else {
                DENSITY_VAPOR
                    + (DENSITY_LIQUID - DENSITY_VAPOR) * (ENTHALPY_VAPOR_THRESHOLD - enthalpy)
                        / (ENTHALPY_VAPOR_THRESHOLD - ENTHALPY_LIQUID_THRESHOLD)
            };
            Ok(density)
        }
        _ => Err(ComponentError::InvalidState(format!(
            "Unknown fluid: {}",
            fluid_id
        ))),
    }
}

/// Estimates fluid temperature from pressure and enthalpy.
///
/// **PLACEHOLDER IMPLEMENTATION** - Will be replaced by CoolProp integration
/// in Story 2.2 (Fluid Properties Backend). Current implementation uses
/// rough approximations for R134a, R410A, and R454B.
///
/// # Arguments
///
/// * `fluid_id` - Fluid identifier (e.g., "R134a")
/// * `pressure` - Pressure in Pascals
/// * `enthalpy` - Specific enthalpy in J/kg
///
/// # Returns
///
/// Returns the estimated temperature in Kelvin.
fn estimate_temperature(
    fluid_id: &str,
    pressure: f64,
    enthalpy: f64,
) -> Result<f64, ComponentError> {
    // Placeholder: simple estimation based on pressure and enthalpy
    match fluid_id {
        "R134a" => {
            // Rough approximation using ideal gas law as baseline
            // T = h / cp (roughly)
            let cp = 1200.0; // J/(kg·K) - approximate specific heat
            let temperature_from_h = enthalpy / cp;

            // Adjust based on pressure (saturation temperature approximation)
            // For R134a, saturation pressure correlation is roughly:
            // log10(P) ≈ A - B/(T + C)
            // We'll use a simplified approximation
            let p_bar = pressure / 100000.0;
            let t_sat_k = if p_bar > 0.1 {
                // Simplified saturation temperature for R134a
                250.0 + 50.0 * (p_bar / 10.0).ln()
            } else {
                250.0
            };

            // Return the higher of the two (superheated or saturated)
            Ok(temperature_from_h.max(t_sat_k))
        }
        "R410A" | "R454B" => {
            // R454B is similar to R410A for temperature estimation
            let cp = 1300.0;
            let temperature_from_h = enthalpy / cp;
            let p_bar = pressure / 100000.0;
            let t_sat_k = if p_bar > 0.1 {
                240.0 + 45.0 * (p_bar / 15.0).ln()
            } else {
                240.0
            };
            Ok(temperature_from_h.max(t_sat_k))
        }
        _ => Err(ComponentError::InvalidState(format!(
            "Unknown fluid: {}",
            fluid_id
        ))),
    }
}

/// Approximates the derivative of a function using finite differences.
///
/// Uses a central difference approximation:
/// f'(x) ≈ (f(x + h) - f(x - h)) / (2h)
#[cfg(test)]
fn approximate_derivative<F>(f: F, x: f64, h: f64) -> f64
where
    F: Fn(f64) -> f64,
{
    (f(x + h) - f(x - h)) / (2.0 * h)
}

fn approximate_derivative_result<F>(f: F, x: f64, h: f64) -> Result<f64, ComponentError>
where
    F: Fn(f64) -> Result<f64, ComponentError>,
{
    Ok((f(x + h)? - f(x - h)?) / (2.0 * h))
}

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

    // Test coefficients representing a typical small compressor
    fn test_coefficients() -> Ahri540Coefficients {
        Ahri540Coefficients::new(
            0.85,   // M1: Flow coefficient
            2.5,    // M2: Pressure ratio exponent (higher value allows reasonable pressure ratios)
            500.0,  // M3: Power constant (cooling)
            1500.0, // M4: Power pressure ratio (cooling)
            -2.5,   // M5: Power suction temp (cooling)
            1.8,    // M6: Power discharge temp (cooling)
            600.0,  // M7: Power constant (heating)
            1600.0, // M8: Power pressure ratio (heating)
            -3.0,   // M9: Power suction temp (heating)
            2.0,    // M10: Power discharge temp (heating)
        )
    }

    fn create_test_compressor() -> Compressor<Connected> {
        let coeffs = test_coefficients();
        // Create ports with same initial pressure and enthalpy to allow connection
        let suction = Port::new(
            FluidId::new("R134a"),
            Pressure::from_bar(3.5),
            Enthalpy::from_joules_per_kg(400000.0),
        );
        let discharge = Port::new(
            FluidId::new("R134a"),
            Pressure::from_bar(3.5), // Same pressure for connection
            Enthalpy::from_joules_per_kg(400000.0), // Same enthalpy for connection
        );
        let (suction_conn, mut discharge_conn) = suction.connect(discharge).unwrap();

        // Modify discharge pressure and enthalpy after connection
        // Use moderate pressure ratio (6/3.5 ≈ 1.71) to ensure positive volumetric efficiency
        discharge_conn.set_pressure(Pressure::from_bar(6.0));
        discharge_conn.set_enthalpy(Enthalpy::from_joules_per_kg(450000.0));

        Compressor {
            model: CompressorModel::Ahri540(coeffs),
            port_suction: suction_conn,
            port_discharge: discharge_conn,
            speed_rpm: 2900.0,
            displacement_m3_per_rev: 0.0001,
            mechanical_efficiency: 0.85,
            calib: Calib::default(),
            calib_indices: entropyk_core::CalibIndices::default(),
            fluid_id: FluidId::new("R134a"),
            circuit_id: CircuitId::default(),
            operational_state: OperationalState::default(),
            suction_m_idx: None,
            suction_h_idx: None,
            discharge_m_idx: None,
            discharge_h_idx: None,
            same_branch_m: false,
            _state: PhantomData,
        }
    }

    #[test]
    fn test_coefficient_creation() {
        let coeffs = test_coefficients();
        assert_eq!(coeffs.m1, 0.85);
        assert_eq!(coeffs.m2, 2.5);
        assert_eq!(coeffs.m3, 500.0);
        assert_eq!(coeffs.m10, 2.0);
    }

    #[test]
    fn test_coefficient_validation_valid() {
        let coeffs = test_coefficients();
        assert!(coeffs.validate().is_ok());
    }

    #[test]
    fn test_coefficient_validation_nan() {
        let mut coeffs = test_coefficients();
        coeffs.m1 = f64::NAN;
        assert!(coeffs.validate().is_err());
    }

    #[test]
    fn test_coefficient_validation_infinite() {
        let mut coeffs = test_coefficients();
        coeffs.m2 = f64::INFINITY;
        assert!(coeffs.validate().is_err());
    }

    #[test]
    fn test_coefficient_validation_negative_m2() {
        let mut coeffs = test_coefficients();
        coeffs.m2 = -0.5;
        assert!(coeffs.validate().is_err());
    }

    #[test]
    fn test_disconnected_compressor_creation() {
        let coeffs = test_coefficients();
        let suction = Port::new(
            FluidId::new("R134a"),
            Pressure::from_bar(3.5),
            Enthalpy::from_joules_per_kg(400000.0),
        );
        let discharge = Port::new(
            FluidId::new("R134a"),
            Pressure::from_bar(15.0),
            Enthalpy::from_joules_per_kg(450000.0),
        );

        let compressor = Compressor::new(coeffs, suction, discharge, 2900.0, 0.0001, 0.85);

        assert!(compressor.is_ok());
        let comp = compressor.unwrap();
        assert_eq!(comp.speed_rpm(), 2900.0);
        assert_eq!(comp.displacement_m3_per_rev(), 0.0001);
        assert_eq!(comp.mechanical_efficiency(), 0.85);
    }

    #[test]
    fn test_compressor_creation_zero_speed() {
        let coeffs = test_coefficients();
        let suction = Port::new(
            FluidId::new("R134a"),
            Pressure::from_bar(3.5),
            Enthalpy::from_joules_per_kg(400000.0),
        );
        let discharge = Port::new(
            FluidId::new("R134a"),
            Pressure::from_bar(15.0),
            Enthalpy::from_joules_per_kg(450000.0),
        );

        let result = Compressor::new(
            coeffs, suction, discharge, 0.0, // Invalid speed
            0.0001, 0.85,
        );

        assert!(result.is_err());
    }

    #[test]
    fn test_compressor_creation_negative_displacement() {
        let coeffs = test_coefficients();
        let suction = Port::new(
            FluidId::new("R134a"),
            Pressure::from_bar(3.5),
            Enthalpy::from_joules_per_kg(400000.0),
        );
        let discharge = Port::new(
            FluidId::new("R134a"),
            Pressure::from_bar(15.0),
            Enthalpy::from_joules_per_kg(450000.0),
        );

        let result = Compressor::new(
            coeffs, suction, discharge, 2900.0, -0.0001, // Invalid displacement
            0.85,
        );

        assert!(result.is_err());
    }

    #[test]
    fn test_compressor_creation_invalid_efficiency() {
        let coeffs = test_coefficients();
        let suction = Port::new(
            FluidId::new("R134a"),
            Pressure::from_bar(3.5),
            Enthalpy::from_joules_per_kg(400000.0),
        );
        let discharge = Port::new(
            FluidId::new("R134a"),
            Pressure::from_bar(15.0),
            Enthalpy::from_joules_per_kg(450000.0),
        );

        let result = Compressor::new(
            coeffs, suction, discharge, 2900.0, 0.0001, 1.5, // Invalid efficiency
        );

        assert!(result.is_err());
    }

    #[test]
    fn test_compressor_creation_different_fluids() {
        let coeffs = test_coefficients();
        let suction = Port::new(
            FluidId::new("R134a"),
            Pressure::from_bar(3.5),
            Enthalpy::from_joules_per_kg(400000.0),
        );
        let discharge = Port::new(
            FluidId::new("R410A"), // Different fluid
            Pressure::from_bar(15.0),
            Enthalpy::from_joules_per_kg(450000.0),
        );

        let result = Compressor::new(coeffs, suction, discharge, 2900.0, 0.0001, 0.85);

        assert!(result.is_err());
    }

    #[test]
    fn test_mass_flow_calculation() {
        let compressor = create_test_compressor();
        let density = 20.0; // kg/m³ (approximate vapor density)
        let t_suction_k = 278.15; // 5°C in Kelvin
        let t_discharge_k = 318.15; // 45°C in Kelvin

        let mass_flow = compressor
            .mass_flow_rate(density, t_suction_k, t_discharge_k, None)
            .unwrap();

        // Verify mass flow is positive
        assert!(mass_flow.to_kg_per_s() > 0.0);

        // Verify calculation: M1 * (1 - (P_suction/P_discharge)^(1/M2)) * rho * Vdisp * N/60
        // Using inverse pressure ratio from create_test_compressor (3.5/6.0)
        let inverse_pressure_ratio: f64 = 3.5 / 6.0;
        let volumetric_eff = 1.0 - inverse_pressure_ratio.powf(1.0 / 2.5);
        let speed_rev_per_s = 2900.0 / 60.0;
        let expected_mass_flow = 0.85 * volumetric_eff * density * 0.0001 * speed_rev_per_s;

        assert_relative_eq!(mass_flow.to_kg_per_s(), expected_mass_flow, epsilon = 1e-10);
    }

    #[test]
    fn test_f_m_scales_mass_flow() {
        let mut compressor = create_test_compressor();
        let density = 20.0;
        let t_suction_k = 278.15; // 5°C in Kelvin
        let t_discharge_k = 318.15; // 45°C in Kelvin
        let m_default = compressor
            .mass_flow_rate(density, t_suction_k, t_discharge_k, None)
            .unwrap()
            .to_kg_per_s();

        compressor.set_calib(Calib {
            z_flow: 1.1,
            ..Calib::default()
        });
        let m_calib = compressor
            .mass_flow_rate(density, t_suction_k, t_discharge_k, None)
            .unwrap()
            .to_kg_per_s();
        assert_relative_eq!(m_calib / m_default, 1.1, epsilon = 1e-10);
    }

    #[test]
    fn test_f_power_scales_compressor_power() {
        let mut compressor = create_test_compressor();
        let t_suction = Temperature::from_celsius(5.0);
        let t_discharge = Temperature::from_celsius(45.0);
        let p_default = compressor.power_consumption_cooling(t_suction, t_discharge, None);

        compressor.set_calib(Calib {
            z_power: 1.1,
            ..Calib::default()
        });
        let p_calib = compressor.power_consumption_cooling(t_suction, t_discharge, None);
        assert_relative_eq!(p_calib / p_default, 1.1, epsilon = 1e-10);
    }

    #[test]
    fn test_f_etav_scales_volumetric_efficiency() {
        let mut compressor = create_test_compressor();
        let t_suction_k = 278.15;
        let t_discharge_k = 318.15;
        let rho = 15.0;
        let m_default = compressor
            .mass_flow_rate(rho, t_suction_k, t_discharge_k, None)
            .unwrap()
            .to_kg_per_s();

        compressor.set_calib(Calib {
            z_etav: 0.9,
            ..Calib::default()
        });
        let m_calib = compressor
            .mass_flow_rate(rho, t_suction_k, t_discharge_k, None)
            .unwrap()
            .to_kg_per_s();

        assert_relative_eq!(m_calib / m_default, 0.9, epsilon = 1e-10);
    }

    #[test]
    fn test_mass_flow_negative_density() {
        let compressor = create_test_compressor();
        let t_suction_k = 278.15; // 5°C in Kelvin
        let t_discharge_k = 318.15; // 45°C in Kelvin
        let result = compressor.mass_flow_rate(-10.0, t_suction_k, t_discharge_k, None);
        assert!(result.is_err());
    }

    #[test]
    fn test_mass_flow_zero_suction_pressure() {
        let coeffs = test_coefficients();
        let suction = Port::new(
            FluidId::new("R134a"),
            Pressure::from_pascals(0.0), // Zero pressure
            Enthalpy::from_joules_per_kg(400000.0),
        );
        let discharge = Port::new(
            FluidId::new("R134a"),
            Pressure::from_pascals(0.0), // Same zero pressure for connection
            Enthalpy::from_joules_per_kg(400000.0), // Same enthalpy for connection
        );
        let (suction_conn, mut discharge_conn) = suction.connect(discharge).unwrap();

        // Modify discharge pressure and enthalpy after connection
        discharge_conn.set_pressure(Pressure::from_bar(15.0));
        discharge_conn.set_enthalpy(Enthalpy::from_joules_per_kg(450000.0));

        let compressor = Compressor {
            model: CompressorModel::Ahri540(coeffs),
            port_suction: suction_conn,
            port_discharge: discharge_conn,
            speed_rpm: 2900.0,
            displacement_m3_per_rev: 0.0001,
            mechanical_efficiency: 0.85,
            calib: Calib::default(),
            calib_indices: entropyk_core::CalibIndices::default(),
            fluid_id: FluidId::new("R134a"),
            circuit_id: CircuitId::default(),
            operational_state: OperationalState::default(),
            suction_m_idx: None,
            suction_h_idx: None,
            discharge_m_idx: None,
            discharge_h_idx: None,
            same_branch_m: false,
            _state: PhantomData,
        };

        let t_suction_k = 278.15; // 5°C in Kelvin
        let t_discharge_k = 318.15; // 45°C in Kelvin
        let result = compressor.mass_flow_rate(20.0, t_suction_k, t_discharge_k, None);
        assert!(result.is_err());
    }

    #[test]
    fn test_power_consumption_cooling() {
        let compressor = create_test_compressor();
        let t_suction = Temperature::from_celsius(5.0);
        let t_discharge = Temperature::from_celsius(45.0);

        let power = compressor.power_consumption_cooling(t_suction, t_discharge, None);

        // Verify power is positive
        assert!(power > 0.0);

        // Verify calculation: M3 + M4 * PR + M5 * T_suction + M6 * T_discharge
        // Using 6.0/3.5 pressure ratio from create_test_compressor
        let pressure_ratio: f64 = 6.0 / 3.5;
        let expected_power = 500.0
            + 1500.0 * pressure_ratio
            + (-2.5) * t_suction.to_kelvin()
            + 1.8 * t_discharge.to_kelvin();

        assert_relative_eq!(power, expected_power, epsilon = 1e-10);
    }

    #[test]
    fn test_power_consumption_heating() {
        let compressor = create_test_compressor();
        let t_suction = Temperature::from_celsius(5.0);
        let t_discharge = Temperature::from_celsius(45.0);

        let power = compressor.power_consumption_heating(t_suction, t_discharge, None);

        // Verify calculation: M7 + M8 * PR + M9 * T_suction + M10 * T_discharge
        // Using 6.0/3.5 pressure ratio from create_test_compressor
        let pressure_ratio: f64 = 6.0 / 3.5;
        let expected_power = 600.0
            + 1600.0 * pressure_ratio
            + (-3.0) * t_suction.to_kelvin()
            + 2.0 * t_discharge.to_kelvin();

        assert_relative_eq!(power, expected_power, epsilon = 1e-10);
    }

    #[test]
    fn test_cooling_capacity() {
        let compressor = create_test_compressor();
        let mass_flow = MassFlow::from_kg_per_s(0.05);
        let h_evap_in = Enthalpy::from_joules_per_kg(250000.0);
        let h_evap_out = Enthalpy::from_joules_per_kg(400000.0);

        let capacity = compressor.cooling_capacity(mass_flow, h_evap_in, h_evap_out);

        // Q = ṁ * (h_out - h_in)
        let expected_capacity = 0.05 * (400000.0 - 250000.0);
        assert_relative_eq!(capacity, expected_capacity, epsilon = 1e-10);
    }

    #[test]
    fn test_heating_capacity() {
        let compressor = create_test_compressor();
        let mass_flow = MassFlow::from_kg_per_s(0.05);
        let h_cond_in = Enthalpy::from_joules_per_kg(450000.0);
        let h_cond_out = Enthalpy::from_joules_per_kg(250000.0);

        let capacity = compressor.heating_capacity(mass_flow, h_cond_in, h_cond_out);

        // Q = ṁ * (h_out - h_in)
        let expected_capacity = 0.05 * (250000.0 - 450000.0); // Negative for heating
        assert_relative_eq!(capacity, expected_capacity, epsilon = 1e-10);
    }

    #[test]
    fn test_coefficient_of_performance() {
        let compressor = create_test_compressor();
        let capacity = 5000.0; // W
        let power = 2000.0; // W

        let cop = compressor
            .coefficient_of_performance(capacity, power)
            .unwrap();
        assert_relative_eq!(cop, 2.5, epsilon = 1e-10);
    }

    #[test]
    fn test_coefficient_of_performance_zero_power() {
        let compressor = create_test_compressor();
        let result = compressor.coefficient_of_performance(5000.0, 0.0);
        assert!(result.is_err());
    }

    #[test]
    fn test_component_n_equations() {
        let compressor = create_test_compressor();
        // CM1.3: 2 thermo + 1 mass-flow conservation = 3 equations
        assert_eq!(compressor.n_equations(), 3);
    }

    #[test]
    fn test_component_compute_residuals() {
        let mut compressor = create_test_compressor();
        compressor.set_system_context(0, &[(0, 0, 1), (3, 0, 2)]);
        let state = vec![0.05, 400000.0, 450000.0, 3500.0];
        let mut residuals = vec![0.0; 3];

        let result = compressor.compute_residuals(&state, &mut residuals);
        assert!(result.is_ok(), "jacobian error: {:?}", result);

        // Verify residuals are calculated (actual values depend on fluid properties)
        assert!(!residuals[0].is_nan());
        assert!(!residuals[1].is_nan());
        assert!(!residuals[2].is_nan());
    }

    #[test]
    fn test_component_compute_residuals_wrong_size() {
        let mut compressor = create_test_compressor();
        compressor.set_system_context(0, &[(0, 0, 1), (3, 0, 2)]);
        let state = vec![0.05, 400000.0, 450000.0, 3500.0];
        let mut residuals = vec![0.0; 2]; // Wrong size (n_equations is now 3)

        let result = compressor.compute_residuals(&state, &mut residuals);
        assert!(result.is_err());
    }

    #[test]
    fn test_component_jacobian_entries() {
        let mut compressor = create_test_compressor();
        compressor.set_system_context(0, &[(0, 0, 1), (3, 0, 2)]);
        let state = vec![0.05, 400000.0, 450000.0, 3500.0];
        let mut jacobian = JacobianBuilder::new();

        let result = compressor.jacobian_entries(&state, &mut jacobian);
        assert!(result.is_ok(), "jacobian error: {:?}", result);

        // Should have at least some entries
        assert!(!jacobian.is_empty());
    }

    #[test]
    fn test_estimate_density_r134a() {
        let density = estimate_density("R134a", 350000.0, 400000.0).unwrap();
        // At high enthalpy (superheated), should be around 20 kg/m³
        assert!(density > 0.0);
        assert!(density < 100.0);
    }

    #[test]
    fn test_estimate_temperature_r134a() {
        let temp = estimate_temperature("R134a", 350000.0, 400000.0).unwrap();
        assert!(temp > 200.0);
        assert!(temp < 500.0);
    }

    #[test]
    fn test_estimate_unknown_fluid() {
        let result = estimate_density("UnknownFluid", 350000.0, 400000.0);
        assert!(result.is_err());
    }

    #[test]
    fn test_approximate_derivative() {
        let f = |x: f64| x * x;
        let derivative = approximate_derivative(f, 2.0, 0.001);
        // f'(x) = 2x, so at x=2, f'(2) = 4
        assert_relative_eq!(derivative, 4.0, epsilon = 1e-6);
    }

    #[test]
    fn test_compressor_with_r410a() {
        let coeffs = test_coefficients();
        let suction = Port::new(
            FluidId::new("R410A"),
            Pressure::from_bar(10.0),
            Enthalpy::from_joules_per_kg(380000.0),
        );
        let discharge = Port::new(
            FluidId::new("R410A"),
            Pressure::from_bar(10.0), // Same pressure for connection
            Enthalpy::from_joules_per_kg(380000.0), // Same enthalpy for connection
        );
        let (suction_conn, mut discharge_conn) = suction.connect(discharge).unwrap();

        // Modify discharge pressure and enthalpy after connection
        // Use moderate pressure ratio (18/10 = 1.8) to ensure positive volumetric efficiency
        discharge_conn.set_pressure(Pressure::from_bar(18.0));
        discharge_conn.set_enthalpy(Enthalpy::from_joules_per_kg(430000.0));

        let compressor = Compressor {
            model: CompressorModel::Ahri540(coeffs),
            port_suction: suction_conn,
            port_discharge: discharge_conn,
            speed_rpm: 3600.0,
            displacement_m3_per_rev: 0.00008,
            mechanical_efficiency: 0.88,
            calib: Calib::default(),
            calib_indices: entropyk_core::CalibIndices::default(),
            fluid_id: FluidId::new("R410A"),
            circuit_id: CircuitId::default(),
            operational_state: OperationalState::default(),
            suction_m_idx: None,
            suction_h_idx: None,
            discharge_m_idx: None,
            discharge_h_idx: None,
            same_branch_m: false,
            _state: PhantomData,
        };

        let density = 25.0; // kg/m³
        let t_suction_k = 283.15; // 10°C in Kelvin
        let t_discharge_k = 323.15; // 50°C in Kelvin
        let mass_flow = compressor
            .mass_flow_rate(density, t_suction_k, t_discharge_k, None)
            .unwrap();
        assert!(mass_flow.to_kg_per_s() > 0.0);

        let t_suction = Temperature::from_celsius(10.0);
        let t_discharge = Temperature::from_celsius(50.0);
        let power = compressor.power_consumption_cooling(t_suction, t_discharge, None);
        assert!(power > 0.0);
    }

    #[test]
    fn test_compressor_with_r454b() {
        // R454B (Opteon XL41) is a low-GWP replacement for R410A
        let coeffs = test_coefficients();
        let suction = Port::new(
            FluidId::new("R454B"),
            Pressure::from_bar(10.0),
            Enthalpy::from_joules_per_kg(380000.0),
        );
        let discharge = Port::new(
            FluidId::new("R454B"),
            Pressure::from_bar(10.0), // Same pressure for connection
            Enthalpy::from_joules_per_kg(380000.0), // Same enthalpy for connection
        );
        let (suction_conn, mut discharge_conn) = suction.connect(discharge).unwrap();

        // Modify discharge pressure and enthalpy after connection
        discharge_conn.set_pressure(Pressure::from_bar(18.0));
        discharge_conn.set_enthalpy(Enthalpy::from_joules_per_kg(430000.0));

        let compressor = Compressor {
            model: CompressorModel::Ahri540(coeffs),
            port_suction: suction_conn,
            port_discharge: discharge_conn,
            speed_rpm: 3600.0,
            displacement_m3_per_rev: 0.00008,
            mechanical_efficiency: 0.88,
            calib: Calib::default(),
            calib_indices: entropyk_core::CalibIndices::default(),
            fluid_id: FluidId::new("R454B"),
            circuit_id: CircuitId::default(),
            operational_state: OperationalState::default(),
            suction_m_idx: None,
            suction_h_idx: None,
            discharge_m_idx: None,
            discharge_h_idx: None,
            same_branch_m: false,
            _state: PhantomData,
        };

        // R454B should use R410A properties as approximation
        let density = 25.0; // kg/m³
        let t_suction_k = 283.15; // 10°C in Kelvin
        let t_discharge_k = 323.15; // 50°C in Kelvin
        let mass_flow = compressor
            .mass_flow_rate(density, t_suction_k, t_discharge_k, None)
            .unwrap();
        assert!(mass_flow.to_kg_per_s() > 0.0);

        let t_suction = Temperature::from_celsius(10.0);
        let t_discharge = Temperature::from_celsius(50.0);
        let power = compressor.power_consumption_cooling(t_suction, t_discharge, None);
        assert!(power > 0.0);
    }

    #[test]
    fn test_mass_flow_with_high_pressure_ratio() {
        let coeffs = Ahri540Coefficients::new(
            0.85, 2.5, // M1, M2
            500.0, 1500.0, -2.5, 1.8, // M3-M6
            600.0, 1600.0, -3.0, 2.0, // M7-M10
        );
        let suction = Port::new(
            FluidId::new("R134a"),
            Pressure::from_bar(1.0), // Low suction pressure
            Enthalpy::from_joules_per_kg(400000.0),
        );
        let discharge = Port::new(
            FluidId::new("R134a"),
            Pressure::from_bar(1.0), // Same pressure for connection
            Enthalpy::from_joules_per_kg(400000.0), // Same enthalpy for connection
        );
        let (suction_conn, mut discharge_conn) = suction.connect(discharge).unwrap();

        // Modify discharge pressure and enthalpy after connection
        discharge_conn.set_pressure(Pressure::from_bar(30.0));
        discharge_conn.set_enthalpy(Enthalpy::from_joules_per_kg(450000.0));

        let compressor = Compressor {
            model: CompressorModel::Ahri540(coeffs),
            port_suction: suction_conn,
            port_discharge: discharge_conn,
            speed_rpm: 2900.0,
            displacement_m3_per_rev: 0.0001,
            mechanical_efficiency: 0.85,
            calib: Calib::default(),
            calib_indices: entropyk_core::CalibIndices::default(),
            fluid_id: FluidId::new("R134a"),
            circuit_id: CircuitId::default(),
            operational_state: OperationalState::default(),
            suction_m_idx: None,
            suction_h_idx: None,
            discharge_m_idx: None,
            discharge_h_idx: None,
            same_branch_m: false,
            _state: PhantomData,
        };

        let density = 20.0;
        let t_suction_k = 283.15; // 10°C in Kelvin
        let t_discharge_k = 323.15; // 50°C in Kelvin
                                    // With high pressure ratio, volumetric efficiency might be negative
                                    // depending on M2 value
        let result = compressor.mass_flow_rate(density, t_suction_k, t_discharge_k, None);
        // This may fail due to negative volumetric efficiency
        // which is expected behavior
        if result.is_ok() {
            let mass_flow = result.unwrap();
            assert!(mass_flow.to_kg_per_s() >= 0.0);
        }
    }

    #[test]
    fn test_compressor_clone() {
        let compressor = create_test_compressor();
        let cloned = compressor.clone();

        assert_eq!(compressor.speed_rpm(), cloned.speed_rpm());
        assert_eq!(
            compressor.displacement_m3_per_rev(),
            cloned.displacement_m3_per_rev()
        );
        assert_eq!(
            compressor.mechanical_efficiency(),
            cloned.mechanical_efficiency()
        );
    }

    #[test]
    fn test_state_manageable_state() {
        let compressor = create_test_compressor();
        assert_eq!(compressor.state(), OperationalState::On);
    }

    #[test]
    fn test_state_manageable_set_state_on_to_off() {
        let mut compressor = create_test_compressor();
        let result = compressor.set_state(OperationalState::Off);
        assert!(result.is_ok());
        assert_eq!(compressor.state(), OperationalState::Off);
    }

    #[test]
    fn test_state_manageable_set_state_on_to_bypass() {
        let mut compressor = create_test_compressor();
        let result = compressor.set_state(OperationalState::Bypass);
        assert!(result.is_ok());
        assert_eq!(compressor.state(), OperationalState::Bypass);
    }

    #[test]
    fn test_state_manageable_can_transition_to() {
        let compressor = create_test_compressor();
        assert!(compressor.can_transition_to(OperationalState::Off));
        assert!(compressor.can_transition_to(OperationalState::Bypass));
        assert!(compressor.can_transition_to(OperationalState::On));
    }

    #[test]
    fn test_state_manageable_circuit_id() {
        let compressor = create_test_compressor();
        assert_eq!(*compressor.circuit_id(), CircuitId::ZERO);
    }

    #[test]
    fn test_state_manageable_set_circuit_id() {
        let mut compressor = create_test_compressor();
        compressor.set_circuit_id(CircuitId::from_number(5));
        assert_eq!(compressor.circuit_id().as_number(), 5);
    }
}
