//! Refrigerant Boundary Condition Components
//!
//! This module provides `RefrigerantSource` and `RefrigerantSink` components
//! for refrigerant cycles with native vapor quality support.
//!
//! ## Design Philosophy
//!
//! Unlike the generic `FlowSource`/`FlowSink` which use (Pressure, Enthalpy),
//! these components use (Pressure, VaporQuality) for type-safe two-phase
//! state specification.
//!
//! ## Example
//!
//! ```ignore
//! use entropyk_components::refrigerant_boundary::{RefrigerantSource, RefrigerantSink};
//! use entropyk_core::{Pressure, VaporQuality};
//! use entropyk_fluids::CoolPropBackend;
//! use std::sync::Arc;
//!
//! let backend = Arc::new(CoolPropBackend::new());
//!
//! // Evaporator outlet: 8.5 bar, saturated vapor (quality = 1)
//! let source = RefrigerantSource::new(
//!     "R410A",
//!     Pressure::from_pascals(8.5e5),
//!     VaporQuality::SATURATED_VAPOR,
//!     backend.clone(),
//!     outlet_port,
//! ).unwrap();
//!
//! // Condenser inlet: 24 bar back-pressure
//! let sink = RefrigerantSink::new(
//!     "R410A",
//!     Pressure::from_pascals(24.0e5),
//!     None,  // Free enthalpy
//!     backend,
//!     inlet_port,
//! ).unwrap();
//! ```

use crate::flow_junction::is_incompressible;
use crate::{
    Component, ComponentError, ConnectedPort, JacobianBuilder, ResidualVector, StateSlice,
};
use entropyk_core::{Enthalpy, MassFlow, Power, Pressure, VaporQuality};
use entropyk_fluids::{FluidBackend, FluidId, FluidState, Property, Quality};
use std::sync::Arc;

fn is_refrigerant(fluid: &str) -> bool {
    !is_incompressible(fluid)
}

fn quality_to_enthalpy(
    backend: &dyn FluidBackend,
    fluid: &str,
    p: Pressure,
    quality: VaporQuality,
) -> Result<Enthalpy, ComponentError> {
    let fluid_id = FluidId::new(fluid);
    let state = FluidState::from_px(p, Quality::new(quality.to_fraction()));

    backend
        .property(fluid_id, Property::Enthalpy, state)
        .map(Enthalpy::from_joules_per_kg)
        .map_err(|e| {
            ComponentError::CalculationFailed(format!("Quality to enthalpy conversion: {}", e))
        })
}

/// A boundary source that imposes fixed pressure and vapor quality on its outlet edge.
///
/// Unlike [`FlowSource`](crate::FlowSource) which uses (Pressure, Enthalpy), this component
/// uses (Pressure, VaporQuality) for type-safe two-phase state specification in refrigerant cycles.
///
/// # Equations (always 2)
///
/// ```text
/// r₀ = P_edge − P_set = 0
/// r₁ = h_edge − h(P_set, x) = 0
/// ```
///
/// where h(P, x) is the enthalpy at pressure P and vapor quality x, computed via the
/// [`FluidBackend`] using linear interpolation in the two-phase region:
///
/// ```text
/// h(P, x) = h_liquid(P) + x · (h_vapor(P) − h_liquid(P))
/// ```
///
/// # Example
///
/// ```ignore
/// use entropyk_components::refrigerant_boundary::RefrigerantSource;
/// use entropyk_core::{Pressure, VaporQuality};
/// use entropyk_fluids::CoolPropBackend;
/// use std::sync::Arc;
///
/// let backend = Arc::new(CoolPropBackend::new());
///
/// // Evaporator outlet: 8.5 bar, saturated vapor (quality = 1)
/// let source = RefrigerantSource::new(
///     "R410A",
///     Pressure::from_pascals(8.5e5),
///     VaporQuality::SATURATED_VAPOR,
///     backend,
///     outlet_port,
/// ).unwrap();
/// ```
pub struct RefrigerantSource {
    fluid_id: String,
    p_set_pa: f64,
    quality: VaporQuality,
    h_set_jkg: f64,
    backend: Arc<dyn FluidBackend>,
    outlet: ConnectedPort,
    outlet_m_idx: Option<usize>,
    outlet_p_idx: Option<usize>,
    outlet_h_idx: Option<usize>,
    m_flow_set_kg_s: Option<f64>,
}

impl RefrigerantSource {
    /// Creates a new `RefrigerantSource` with fixed pressure and vapor quality.
    ///
    /// # Arguments
    ///
    /// * `fluid` - Refrigerant identifier (e.g., "R410A", "R134a", "R32", "CO2")
    /// * `p_set` - Set-point pressure
    /// * `quality` - Vapor quality (0 = saturated liquid, 1 = saturated vapor)
    /// * `backend` - Fluid property backend for quality→enthalpy conversion
    /// * `outlet` - Connected port linked to the system edge
    ///
    /// # Errors
    ///
    /// Returns [`ComponentError::InvalidState`] if:
    /// - The fluid is incompressible (use [`FlowSource::incompressible`](crate::FlowSource::incompressible) instead)
    /// - The pressure is not positive
    /// - The fluid backend fails to compute saturation properties
    pub fn new(
        fluid: impl Into<String>,
        p_set: Pressure,
        quality: VaporQuality,
        backend: Arc<dyn FluidBackend>,
        outlet: ConnectedPort,
    ) -> Result<Self, ComponentError> {
        let fluid = fluid.into();

        if !is_refrigerant(&fluid) {
            return Err(ComponentError::InvalidState(format!(
                "RefrigerantSource: '{}' is an incompressible fluid. Use FlowSource::incompressible instead.",
                fluid
            )));
        }

        let p_set_pa = p_set.to_pascals();
        if p_set_pa <= 0.0 {
            return Err(ComponentError::InvalidState(
                "RefrigerantSource: set-point pressure must be positive".into(),
            ));
        }

        let h_set = quality_to_enthalpy(backend.as_ref(), &fluid, p_set, quality)?;

        Ok(Self {
            fluid_id: fluid,
            p_set_pa,
            quality,
            h_set_jkg: h_set.to_joules_per_kg(),
            backend,
            outlet,
            outlet_m_idx: None,
            outlet_p_idx: None,
            outlet_h_idx: None,
            m_flow_set_kg_s: None,
        })
    }

    /// Imposes a fixed mass flow on the outlet edge (BOLT `Vd_fixed=true`).
    /// Adds one equation `ṁ_edge − ṁ_set = 0`, bringing `n_equations` to 3.
    pub fn with_imposed_mass_flow(mut self, m_flow_kg_s: f64) -> Result<Self, ComponentError> {
        if m_flow_kg_s <= 0.0 || !m_flow_kg_s.is_finite() {
            return Err(ComponentError::InvalidState(format!(
                "RefrigerantSource: mass flow must be positive and finite, got {m_flow_kg_s}"
            )));
        }
        self.m_flow_set_kg_s = Some(m_flow_kg_s);
        Ok(self)
    }

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

    /// Returns the set-point pressure in Pascals.
    pub fn p_set_pa(&self) -> f64 {
        self.p_set_pa
    }

    /// Returns the vapor quality.
    pub fn quality(&self) -> VaporQuality {
        self.quality
    }

    /// Returns the computed set-point enthalpy in J/kg.
    pub fn h_set_jkg(&self) -> f64 {
        self.h_set_jkg
    }

    /// Returns a reference to the outlet port.
    pub fn outlet(&self) -> &ConnectedPort {
        &self.outlet
    }

    /// Updates the set-point pressure and recomputes enthalpy.
    ///
    /// # Errors
    ///
    /// Returns [`ComponentError::InvalidState`] if the pressure is not positive
    /// or if the fluid backend fails to compute the new enthalpy.
    pub fn set_pressure(&mut self, p: Pressure) -> Result<(), ComponentError> {
        let p_pa = p.to_pascals();
        if p_pa <= 0.0 {
            return Err(ComponentError::InvalidState(
                "RefrigerantSource: pressure must be positive".into(),
            ));
        }
        self.p_set_pa = p_pa;
        self.recompute_enthalpy()
    }

    /// Updates the vapor quality and recomputes enthalpy.
    ///
    /// # Errors
    ///
    /// Returns [`ComponentError::InvalidState`] if the fluid backend fails
    /// to compute the new enthalpy.
    pub fn set_quality(&mut self, quality: VaporQuality) -> Result<(), ComponentError> {
        self.quality = quality;
        self.recompute_enthalpy()
    }

    fn recompute_enthalpy(&mut self) -> Result<(), ComponentError> {
        let h_set = quality_to_enthalpy(
            self.backend.as_ref(),
            &self.fluid_id,
            Pressure::from_pascals(self.p_set_pa),
            self.quality,
        )?;
        self.h_set_jkg = h_set.to_joules_per_kg();
        Ok(())
    }
}

impl Component for RefrigerantSource {
    fn n_equations(&self) -> usize {
        2 + usize::from(self.m_flow_set_kg_s.is_some())
    }

    fn set_system_context(
        &mut self,
        _state_offset: usize,
        external_edge_state_indices: &[(usize, usize, usize)],
    ) {
        if let Some(&(m, p, h)) = external_edge_state_indices.last() {
            self.outlet_m_idx = Some(m);
            self.outlet_p_idx = Some(p);
            self.outlet_h_idx = Some(h);
        }
    }

    fn compute_residuals(
        &self,
        state: &StateSlice,
        residuals: &mut ResidualVector,
    ) -> Result<(), ComponentError> {
        let n = self.n_equations();
        if residuals.len() < n {
            return Err(ComponentError::InvalidResidualDimensions {
                expected: n,
                actual: residuals.len(),
            });
        }
        match (self.outlet_p_idx, self.outlet_h_idx) {
            (Some(p_idx), Some(h_idx)) if p_idx < state.len() && h_idx < state.len() => {
                residuals[0] = state[p_idx] - self.p_set_pa;
                residuals[1] = state[h_idx] - self.h_set_jkg;
                if let Some(m_set) = self.m_flow_set_kg_s {
                    let Some(m_idx) = self.outlet_m_idx.filter(|&i| i < state.len()) else {
                        return Err(ComponentError::InvalidState(
                            "RefrigerantSource imposed mass flow requires a live outlet mass-flow index"
                                .to_string(),
                        ));
                    };
                    residuals[2] = state[m_idx] - m_set;
                }
            }
            _ if state.is_empty() => {
                residuals[0] = self.outlet.pressure().to_pascals() - self.p_set_pa;
                residuals[1] = self.outlet.enthalpy().to_joules_per_kg() - self.h_set_jkg;
                if let Some(m_set) = self.m_flow_set_kg_s {
                    residuals[2] = -m_set;
                }
            }
            _ => {
                return Err(ComponentError::InvalidState(
                    "RefrigerantSource requires a live outlet edge state in solver context"
                        .to_string(),
                ));
            }
        }
        Ok(())
    }

    fn jacobian_entries(
        &self,
        _state: &StateSlice,
        jacobian: &mut JacobianBuilder,
    ) -> Result<(), ComponentError> {
        let (Some(p_idx), Some(h_idx)) = (self.outlet_p_idx, self.outlet_h_idx) else {
            if _state.is_empty() {
                return Ok(());
            }
            return Err(ComponentError::InvalidState(
                "RefrigerantSource Jacobian requires live outlet pressure/enthalpy indices"
                    .to_string(),
            ));
        };
        if p_idx >= _state.len() || h_idx >= _state.len() {
            return Err(ComponentError::InvalidStateDimensions {
                expected: p_idx.max(h_idx) + 1,
                actual: _state.len(),
            });
        }
        jacobian.add_entry(0, p_idx, 1.0);
        jacobian.add_entry(1, h_idx, 1.0);
        if let (Some(_), Some(m_idx)) = (self.m_flow_set_kg_s, self.outlet_m_idx) {
            if m_idx >= _state.len() {
                return Err(ComponentError::InvalidStateDimensions {
                    expected: m_idx + 1,
                    actual: _state.len(),
                });
            }
            jacobian.add_entry(2, m_idx, 1.0);
        }
        Ok(())
    }

    fn get_ports(&self) -> &[ConnectedPort] {
        &[]
    }

    fn port_mass_flows(&self, _state: &StateSlice) -> Result<Vec<MassFlow>, ComponentError> {
        Ok(vec![MassFlow::from_kg_per_s(0.0)])
    }

    fn port_enthalpies(&self, _state: &StateSlice) -> Result<Vec<Enthalpy>, ComponentError> {
        Ok(vec![self.outlet.enthalpy()])
    }

    fn energy_transfers(&self, _state: &StateSlice) -> Option<(Power, Power)> {
        Some((Power::from_watts(0.0), Power::from_watts(0.0)))
    }

    fn signature(&self) -> String {
        format!(
            "RefrigerantSource({}:P={:.0}Pa,q={:.2})",
            self.fluid_id,
            self.p_set_pa,
            self.quality.to_fraction()
        )
    }

    fn to_params(&self) -> crate::ComponentParams {
        crate::ComponentParams::new("RefrigerantSource")
            .with_param("fluid", self.fluid_id.as_str())
            .with_param("pSetPa", self.p_set_pa)
            .with_param("quality", self.quality.to_fraction())
            .with_param("hSetJkg", self.h_set_jkg)
    }

    fn set_fluid_backend_from_builder(&mut self, backend: Arc<dyn FluidBackend>) {
        self.backend = backend;
    }
}

/// A boundary sink that imposes fixed back-pressure on its inlet edge.
///
/// Optionally, a fixed vapor quality can also be imposed. This is useful for
/// modeling condenser outlets or phase separators.
///
/// # Equations (1 or 2)
///
/// ```text
/// r₀ = P_edge − P_back = 0                      (always)
/// r₁ = h_edge − h(P_back, x) = 0                (if quality specified)
/// ```
///
/// where h(P, x) is the enthalpy at pressure P and vapor quality x.
///
/// # Dynamic Quality Toggle
///
/// The quality constraint can be toggled at runtime:
/// - [`RefrigerantSink::set_quality()`] adds the enthalpy equation (2 equations total)
/// - [`RefrigerantSink::clear_quality()`] removes the enthalpy equation (1 equation total)
///
/// # Example
///
/// ```ignore
/// use entropyk_components::refrigerant_boundary::RefrigerantSink;
/// use entropyk_core::{Pressure, VaporQuality};
/// use entropyk_fluids::CoolPropBackend;
/// use std::sync::Arc;
///
/// let backend = Arc::new(CoolPropBackend::new());
///
/// // Condenser inlet: 24 bar back-pressure, free enthalpy
/// let sink = RefrigerantSink::new(
///     "R410A",
///     Pressure::from_pascals(24.0e5),
///     None,  // Free enthalpy
///     backend,
///     inlet_port,
/// ).unwrap();
/// ```
pub struct RefrigerantSink {
    fluid_id: String,
    p_back_pa: f64,
    quality_opt: Option<VaporQuality>,
    h_back_jkg: Option<f64>,
    backend: Arc<dyn FluidBackend>,
    inlet: ConnectedPort,
    inlet_p_idx: Option<usize>,
    inlet_h_idx: Option<usize>,
}

impl RefrigerantSink {
    /// Creates a new `RefrigerantSink` with fixed back-pressure and optional vapor quality.
    ///
    /// # Arguments
    ///
    /// * `fluid` - Refrigerant identifier (e.g., "R410A", "R134a", "R32", "CO2")
    /// * `p_back` - Back-pressure
    /// * `quality_opt` - Optional vapor quality (`None` = free enthalpy, 1 equation)
    /// * `backend` - Fluid property backend for quality→enthalpy conversion
    /// * `inlet` - Connected port linked to the system edge
    ///
    /// # Errors
    ///
    /// Returns [`ComponentError::InvalidState`] if:
    /// - The fluid is incompressible (use [`FlowSink::incompressible`](crate::FlowSink::incompressible) instead)
    /// - The pressure is not positive
    /// - The fluid backend fails to compute saturation properties (when quality is specified)
    pub fn new(
        fluid: impl Into<String>,
        p_back: Pressure,
        quality_opt: Option<VaporQuality>,
        backend: Arc<dyn FluidBackend>,
        inlet: ConnectedPort,
    ) -> Result<Self, ComponentError> {
        let fluid = fluid.into();

        if !is_refrigerant(&fluid) {
            return Err(ComponentError::InvalidState(format!(
                "RefrigerantSink: '{}' is an incompressible fluid. Use FlowSink::incompressible instead.",
                fluid
            )));
        }

        let p_back_pa = p_back.to_pascals();
        if p_back_pa <= 0.0 {
            return Err(ComponentError::InvalidState(
                "RefrigerantSink: back-pressure must be positive".into(),
            ));
        }

        let h_back_jkg = if let Some(quality) = quality_opt {
            let h = quality_to_enthalpy(backend.as_ref(), &fluid, p_back, quality)?;
            Some(h.to_joules_per_kg())
        } else {
            None
        };

        Ok(Self {
            fluid_id: fluid,
            p_back_pa,
            quality_opt,
            h_back_jkg,
            backend,
            inlet,
            inlet_p_idx: None,
            inlet_h_idx: None,
        })
    }

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

    /// Returns the back-pressure in Pascals.
    pub fn p_back_pa(&self) -> f64 {
        self.p_back_pa
    }

    /// Returns the optional vapor quality constraint.
    pub fn quality(&self) -> Option<VaporQuality> {
        self.quality_opt
    }

    /// Returns the computed back-pressure enthalpy in J/kg, if quality is specified.
    pub fn h_back_jkg(&self) -> Option<f64> {
        self.h_back_jkg
    }

    /// Returns a reference to the inlet port.
    pub fn inlet(&self) -> &ConnectedPort {
        &self.inlet
    }

    /// Updates the back-pressure and recomputes enthalpy if quality is set.
    ///
    /// # Errors
    ///
    /// Returns [`ComponentError::InvalidState`] if the pressure is not positive
    /// or if the fluid backend fails to compute the new enthalpy.
    pub fn set_pressure(&mut self, p: Pressure) -> Result<(), ComponentError> {
        let p_pa = p.to_pascals();
        if p_pa <= 0.0 {
            return Err(ComponentError::InvalidState(
                "RefrigerantSink: back-pressure must be positive".into(),
            ));
        }
        self.p_back_pa = p_pa;
        if let Some(quality) = self.quality_opt {
            self.h_back_jkg = Some(
                quality_to_enthalpy(self.backend.as_ref(), &self.fluid_id, p, quality)?
                    .to_joules_per_kg(),
            );
        }
        Ok(())
    }

    /// Sets the vapor quality constraint and adds the enthalpy equation.
    ///
    /// After calling this method, [`Component::n_equations()`] will return 2.
    ///
    /// # Errors
    ///
    /// Returns [`ComponentError::InvalidState`] if the fluid backend fails
    /// to compute the new enthalpy.
    pub fn set_quality(&mut self, quality: VaporQuality) -> Result<(), ComponentError> {
        self.quality_opt = Some(quality);
        self.h_back_jkg = Some(
            quality_to_enthalpy(
                self.backend.as_ref(),
                &self.fluid_id,
                Pressure::from_pascals(self.p_back_pa),
                quality,
            )?
            .to_joules_per_kg(),
        );
        Ok(())
    }

    /// Clears the vapor quality constraint and removes the enthalpy equation.
    ///
    /// After calling this method, [`Component::n_equations()`] will return 1.
    pub fn clear_quality(&mut self) {
        self.quality_opt = None;
        self.h_back_jkg = None;
    }
}

impl Component for RefrigerantSink {
    fn n_equations(&self) -> usize {
        if self.h_back_jkg.is_some() {
            2
        } else {
            1
        }
    }

    fn set_system_context(
        &mut self,
        _state_offset: usize,
        external_edge_state_indices: &[(usize, usize, usize)],
    ) {
        if let Some(&(_, p, h)) = external_edge_state_indices.first() {
            self.inlet_p_idx = Some(p);
            self.inlet_h_idx = Some(h);
        }
    }

    fn compute_residuals(
        &self,
        state: &StateSlice,
        residuals: &mut ResidualVector,
    ) -> Result<(), ComponentError> {
        let n = self.n_equations();
        if residuals.len() < n {
            return Err(ComponentError::InvalidResidualDimensions {
                expected: n,
                actual: residuals.len(),
            });
        }
        match self.inlet_p_idx {
            Some(p_idx) if p_idx < state.len() => {
                residuals[0] = state[p_idx] - self.p_back_pa;
                if let (Some(h_back), Some(h_idx)) = (self.h_back_jkg, self.inlet_h_idx) {
                    if h_idx < state.len() {
                        residuals[1] = state[h_idx] - h_back;
                    }
                }
            }
            _ if state.is_empty() => {
                residuals[0] = self.inlet.pressure().to_pascals() - self.p_back_pa;
                if let Some(h_back) = self.h_back_jkg {
                    residuals[1] = self.inlet.enthalpy().to_joules_per_kg() - h_back;
                }
            }
            _ => {
                return Err(ComponentError::InvalidState(
                    "RefrigerantSink requires a live inlet edge state in solver context"
                        .to_string(),
                ));
            }
        }
        Ok(())
    }

    fn jacobian_entries(
        &self,
        _state: &StateSlice,
        jacobian: &mut JacobianBuilder,
    ) -> Result<(), ComponentError> {
        let Some(p_idx) = self.inlet_p_idx else {
            if _state.is_empty() {
                return Ok(());
            }
            return Err(ComponentError::InvalidState(
                "RefrigerantSink Jacobian requires a live inlet pressure index".to_string(),
            ));
        };
        if p_idx >= _state.len() {
            return Err(ComponentError::InvalidStateDimensions {
                expected: p_idx + 1,
                actual: _state.len(),
            });
        }
        jacobian.add_entry(0, p_idx, 1.0);
        if let Some(h_idx) = self.inlet_h_idx {
            if h_idx >= _state.len() {
                return Err(ComponentError::InvalidStateDimensions {
                    expected: h_idx + 1,
                    actual: _state.len(),
                });
            }
            jacobian.add_entry(1, h_idx, 1.0);
        }
        Ok(())
    }

    fn get_ports(&self) -> &[ConnectedPort] {
        &[]
    }

    fn port_mass_flows(&self, _state: &StateSlice) -> Result<Vec<MassFlow>, ComponentError> {
        Ok(vec![MassFlow::from_kg_per_s(0.0)])
    }

    fn port_enthalpies(&self, _state: &StateSlice) -> Result<Vec<Enthalpy>, ComponentError> {
        Ok(vec![self.inlet.enthalpy()])
    }

    fn energy_transfers(&self, _state: &StateSlice) -> Option<(Power, Power)> {
        Some((Power::from_watts(0.0), Power::from_watts(0.0)))
    }

    fn signature(&self) -> String {
        format!(
            "RefrigerantSink({}:P={:.0}Pa,q={:?})",
            self.fluid_id, self.p_back_pa, self.quality_opt
        )
    }

    fn to_params(&self) -> crate::ComponentParams {
        let mut params = crate::ComponentParams::new("RefrigerantSink")
            .with_param("fluid", self.fluid_id.as_str())
            .with_param("pBackPa", self.p_back_pa);
        if let Some(q) = self.quality_opt {
            params = params.with_param("quality", q.to_fraction());
        }
        if let Some(h) = self.h_back_jkg {
            params = params.with_param("hBackJkg", h);
        }
        params
    }

    fn set_fluid_backend_from_builder(&mut self, backend: Arc<dyn FluidBackend>) {
        self.backend = backend;
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::port::{FluidId, Port};
    use entropyk_core::Temperature;
    use entropyk_fluids::{
        CriticalPoint, FluidBackend, FluidError, FluidResult, FluidState, Phase, Property,
    };

    /// Mock refrigerant backend for unit testing.
    ///
    /// Provides simplified thermodynamic properties:
    /// - Saturation enthalpies: h_liq = 200 kJ/kg, h_vap = 420 kJ/kg at any pressure
    /// - Linear interpolation in two-phase region
    struct MockRefrigerantBackend;

    impl MockRefrigerantBackend {
        fn new() -> Self {
            Self
        }
    }

    impl FluidBackend for MockRefrigerantBackend {
        fn property(
            &self,
            _fluid: FluidId,
            property: Property,
            state: FluidState,
        ) -> FluidResult<f64> {
            match state {
                FluidState::PressureQuality(p, q) => {
                    let _p_pa = p.to_pascals();
                    let x = q.value();
                    match property {
                        Property::Enthalpy => {
                            let h_l = 200_000.0;
                            let h_v = 420_000.0;
                            Ok(h_l + x * (h_v - h_l))
                        }
                        Property::Temperature => Ok(280.0 + 20.0 * x),
                        Property::Pressure => Ok(_p_pa),
                        _ => Err(FluidError::UnsupportedProperty {
                            property: property.to_string(),
                        }),
                    }
                }
                _ => Err(FluidError::InvalidState {
                    reason: "MockRefrigerantBackend only supports P-Q state".to_string(),
                }),
            }
        }

        fn critical_point(&self, _fluid: FluidId) -> FluidResult<CriticalPoint> {
            Ok(CriticalPoint::new(
                Temperature::from_kelvin(344.0),
                Pressure::from_pascals(4.9e6),
                458.0,
            ))
        }

        fn is_fluid_available(&self, fluid: &FluidId) -> bool {
            matches!(fluid.as_str(), "R410A" | "R134a" | "R32" | "CO2")
        }

        fn phase(&self, _fluid: FluidId, _state: FluidState) -> FluidResult<Phase> {
            Ok(Phase::TwoPhase)
        }

        fn full_state(
            &self,
            _fluid: FluidId,
            _p: Pressure,
            _h: entropyk_core::Enthalpy,
        ) -> FluidResult<entropyk_fluids::ThermoState> {
            Err(FluidError::UnsupportedProperty {
                property: "full_state".to_string(),
            })
        }

        fn list_fluids(&self) -> Vec<FluidId> {
            vec![
                FluidId::new("R410A"),
                FluidId::new("R134a"),
                FluidId::new("R32"),
                FluidId::new("CO2"),
            ]
        }
    }

    fn make_port(fluid: &str, p_pa: f64, h_jkg: f64) -> ConnectedPort {
        let a = Port::new(
            FluidId::new(fluid),
            Pressure::from_pascals(p_pa),
            Enthalpy::from_joules_per_kg(h_jkg),
        );
        let b = Port::new(
            FluidId::new(fluid),
            Pressure::from_pascals(p_pa),
            Enthalpy::from_joules_per_kg(h_jkg),
        );
        a.connect(b).unwrap().0
    }

    #[test]
    fn test_refrigerant_source_creation() {
        let backend = Arc::new(MockRefrigerantBackend::new());
        let port = make_port("R410A", 8.5e5, 260_000.0);
        let source = RefrigerantSource::new(
            "R410A",
            Pressure::from_pascals(8.5e5),
            VaporQuality::SATURATED_VAPOR,
            backend,
            port,
        )
        .unwrap();

        assert_eq!(source.n_equations(), 2);
        assert_eq!(source.fluid_id(), "R410A");
        assert!((source.p_set_pa() - 8.5e5).abs() < 1.0);
        assert!(source.quality().is_saturated_vapor());
    }

    #[test]
    fn test_refrigerant_source_quality_zero() {
        let backend = Arc::new(MockRefrigerantBackend::new());
        let port = make_port("R410A", 8.5e5, 200_000.0);
        let source = RefrigerantSource::new(
            "R410A",
            Pressure::from_pascals(8.5e5),
            VaporQuality::SATURATED_LIQUID,
            backend,
            port,
        )
        .unwrap();

        assert!(source.quality().is_saturated_liquid());
        assert!((source.h_set_jkg() - 200_000.0).abs() < 1.0);
    }

    #[test]
    fn test_refrigerant_source_quality_half() {
        let backend = Arc::new(MockRefrigerantBackend::new());
        let port = make_port("R410A", 8.5e5, 310_000.0);
        let source = RefrigerantSource::new(
            "R410A",
            Pressure::from_pascals(8.5e5),
            VaporQuality::from_fraction(0.5),
            backend,
            port,
        )
        .unwrap();

        assert!((source.quality().to_fraction() - 0.5).abs() < 1e-10);
        assert!((source.h_set_jkg() - 310_000.0).abs() < 1.0);
    }

    #[test]
    fn test_refrigerant_source_rejects_water() {
        let backend = Arc::new(MockRefrigerantBackend::new());
        let port = make_port("Water", 1.0e5, 100_000.0);
        let result = RefrigerantSource::new(
            "Water",
            Pressure::from_pascals(1.0e5),
            VaporQuality::from_fraction(0.5),
            backend,
            port,
        );

        assert!(result.is_err());
    }

    #[test]
    fn test_refrigerant_source_rejects_zero_pressure() {
        let backend = Arc::new(MockRefrigerantBackend::new());
        let port = make_port("R410A", 8.5e5, 260_000.0);
        let result = RefrigerantSource::new(
            "R410A",
            Pressure::from_pascals(0.0),
            VaporQuality::SATURATED_VAPOR,
            backend,
            port,
        );

        assert!(result.is_err());
    }

    #[test]
    fn test_refrigerant_source_residuals_zero_at_set_point() {
        let backend = Arc::new(MockRefrigerantBackend::new());
        let h_jkg = 420_000.0;
        let p_pa = 8.5e5;
        let port = make_port("R410A", p_pa, h_jkg);
        let mut source = RefrigerantSource::new(
            "R410A",
            Pressure::from_pascals(p_pa),
            VaporQuality::SATURATED_VAPOR,
            backend,
            port,
        )
        .unwrap();

        source.set_system_context(0, &[(0, 1, 2)]);
        let mut state = vec![0.0; 4];
        state[1] = p_pa;
        state[2] = h_jkg;
        let mut residuals = vec![0.0; 2];
        source.compute_residuals(&state, &mut residuals).unwrap();

        assert!(residuals[0].abs() < 1.0, "P residual = {}", residuals[0]);
        assert!(
            (residuals[1] - (h_jkg - source.h_set_jkg())).abs() < 1.0,
            "h residual = {}",
            residuals[1]
        );
    }

    #[test]
    fn test_refrigerant_source_set_pressure() {
        let backend = Arc::new(MockRefrigerantBackend::new());
        let port = make_port("R410A", 8.5e5, 260_000.0);
        let mut source = RefrigerantSource::new(
            "R410A",
            Pressure::from_pascals(8.5e5),
            VaporQuality::SATURATED_VAPOR,
            backend,
            port,
        )
        .unwrap();

        source.set_pressure(Pressure::from_pascals(10.0e5)).unwrap();
        assert!((source.p_set_pa() - 10.0e5).abs() < 1.0);
    }

    #[test]
    fn test_refrigerant_source_set_quality() {
        let backend = Arc::new(MockRefrigerantBackend::new());
        let port = make_port("R410A", 8.5e5, 260_000.0);
        let mut source = RefrigerantSource::new(
            "R410A",
            Pressure::from_pascals(8.5e5),
            VaporQuality::SATURATED_VAPOR,
            backend,
            port,
        )
        .unwrap();

        source
            .set_quality(VaporQuality::from_fraction(0.5))
            .unwrap();
        assert!((source.quality().to_fraction() - 0.5).abs() < 1e-10);
        assert!((source.h_set_jkg() - 310_000.0).abs() < 1.0);
    }

    #[test]
    fn test_refrigerant_source_as_trait_object() {
        let backend = Arc::new(MockRefrigerantBackend::new());
        let port = make_port("R410A", 8.5e5, 260_000.0);
        let source: Box<dyn Component> = Box::new(
            RefrigerantSource::new(
                "R410A",
                Pressure::from_pascals(8.5e5),
                VaporQuality::SATURATED_VAPOR,
                backend,
                port,
            )
            .unwrap(),
        );

        assert_eq!(source.n_equations(), 2);
    }

    #[test]
    fn test_refrigerant_sink_creation() {
        let backend = Arc::new(MockRefrigerantBackend::new());
        let port = make_port("R410A", 24.0e5, 465_000.0);
        let sink =
            RefrigerantSink::new("R410A", Pressure::from_pascals(24.0e5), None, backend, port)
                .unwrap();

        assert_eq!(sink.n_equations(), 1);
        assert_eq!(sink.fluid_id(), "R410A");
        assert!((sink.p_back_pa() - 24.0e5).abs() < 1.0);
        assert!(sink.quality().is_none());
    }

    #[test]
    fn test_refrigerant_sink_with_quality() {
        let backend = Arc::new(MockRefrigerantBackend::new());
        let port = make_port("R410A", 24.0e5, 250_000.0);
        let sink = RefrigerantSink::new(
            "R410A",
            Pressure::from_pascals(24.0e5),
            Some(VaporQuality::SATURATED_LIQUID),
            backend,
            port,
        )
        .unwrap();

        assert_eq!(sink.n_equations(), 2);
        assert!(sink.quality().unwrap().is_saturated_liquid());
        assert!((sink.h_back_jkg().unwrap() - 200_000.0).abs() < 1.0);
    }

    #[test]
    fn test_refrigerant_sink_rejects_water() {
        let backend = Arc::new(MockRefrigerantBackend::new());
        let port = make_port("Water", 1.0e5, 100_000.0);
        let result =
            RefrigerantSink::new("Water", Pressure::from_pascals(1.0e5), None, backend, port);

        assert!(result.is_err());
    }

    #[test]
    fn test_refrigerant_sink_rejects_zero_pressure() {
        let backend = Arc::new(MockRefrigerantBackend::new());
        let port = make_port("R410A", 24.0e5, 465_000.0);
        let result =
            RefrigerantSink::new("R410A", Pressure::from_pascals(0.0), None, backend, port);

        assert!(result.is_err());
    }

    #[test]
    fn test_refrigerant_sink_residual_zero_at_back_pressure() {
        let backend = Arc::new(MockRefrigerantBackend::new());
        let p_pa = 24.0e5;
        let port = make_port("R410A", p_pa, 465_000.0);
        let mut sink =
            RefrigerantSink::new("R410A", Pressure::from_pascals(p_pa), None, backend, port)
                .unwrap();

        sink.set_system_context(0, &[(0, 1, 2)]);
        let mut state = vec![0.0; 4];
        state[1] = p_pa;
        let mut residuals = vec![0.0; 1];
        sink.compute_residuals(&state, &mut residuals).unwrap();

        assert!(residuals[0].abs() < 1.0, "P residual = {}", residuals[0]);
    }

    #[test]
    fn test_refrigerant_sink_dynamic_quality_toggle() {
        let backend = Arc::new(MockRefrigerantBackend::new());
        let port = make_port("R410A", 24.0e5, 465_000.0);
        let mut sink =
            RefrigerantSink::new("R410A", Pressure::from_pascals(24.0e5), None, backend, port)
                .unwrap();

        assert_eq!(sink.n_equations(), 1);

        sink.set_quality(VaporQuality::SATURATED_LIQUID).unwrap();
        assert_eq!(sink.n_equations(), 2);

        sink.clear_quality();
        assert_eq!(sink.n_equations(), 1);
    }

    #[test]
    fn test_refrigerant_sink_as_trait_object() {
        let backend = Arc::new(MockRefrigerantBackend::new());
        let port = make_port("R410A", 24.0e5, 465_000.0);
        let sink: Box<dyn Component> = Box::new(
            RefrigerantSink::new(
                "R410A",
                Pressure::from_pascals(24.0e5),
                Some(VaporQuality::SATURATED_LIQUID),
                backend,
                port,
            )
            .unwrap(),
        );

        assert_eq!(sink.n_equations(), 2);
    }

    #[test]
    fn test_refrigerant_source_energy_transfers_zero() {
        let backend = Arc::new(MockRefrigerantBackend::new());
        let port = make_port("R410A", 8.5e5, 260_000.0);
        let source = RefrigerantSource::new(
            "R410A",
            Pressure::from_pascals(8.5e5),
            VaporQuality::SATURATED_VAPOR,
            backend,
            port,
        )
        .unwrap();

        let state = vec![0.0; 4];
        let (heat, work) = source.energy_transfers(&state).unwrap();

        assert_eq!(heat.to_watts(), 0.0);
        assert_eq!(work.to_watts(), 0.0);
    }

    #[test]
    fn test_refrigerant_sink_energy_transfers_zero() {
        let backend = Arc::new(MockRefrigerantBackend::new());
        let port = make_port("R410A", 24.0e5, 465_000.0);
        let sink =
            RefrigerantSink::new("R410A", Pressure::from_pascals(24.0e5), None, backend, port)
                .unwrap();

        let state = vec![0.0; 4];
        let (heat, work) = sink.energy_transfers(&state).unwrap();

        assert_eq!(heat.to_watts(), 0.0);
        assert_eq!(work.to_watts(), 0.0);
    }

    #[test]
    fn test_refrigerant_source_port_enthalpies_single() {
        let backend = Arc::new(MockRefrigerantBackend::new());
        let port = make_port("R410A", 8.5e5, 260_000.0);
        let source = RefrigerantSource::new(
            "R410A",
            Pressure::from_pascals(8.5e5),
            VaporQuality::SATURATED_VAPOR,
            backend,
            port,
        )
        .unwrap();

        let state = vec![0.0; 4];
        let enthalpies = source.port_enthalpies(&state).unwrap();

        assert_eq!(enthalpies.len(), 1);
        assert!((enthalpies[0].to_joules_per_kg() - 260_000.0).abs() < 1.0);
    }

    #[test]
    fn test_refrigerant_sink_port_enthalpies_single() {
        let backend = Arc::new(MockRefrigerantBackend::new());
        let port = make_port("R410A", 24.0e5, 465_000.0);
        let sink = RefrigerantSink::new(
            "R410A",
            Pressure::from_pascals(24.0e5),
            Some(VaporQuality::SATURATED_LIQUID),
            backend,
            port,
        )
        .unwrap();

        let state = vec![0.0; 4];
        let enthalpies = sink.port_enthalpies(&state).unwrap();

        assert_eq!(enthalpies.len(), 1);
        assert!((enthalpies[0].to_joules_per_kg() - 465_000.0).abs() < 1.0);
    }

    #[test]
    fn test_refrigerant_source_mass_flow_enthalpy_length_match() {
        let backend = Arc::new(MockRefrigerantBackend::new());
        let port = make_port("R410A", 8.5e5, 260_000.0);
        let source = RefrigerantSource::new(
            "R410A",
            Pressure::from_pascals(8.5e5),
            VaporQuality::SATURATED_VAPOR,
            backend,
            port,
        )
        .unwrap();

        let state = vec![0.0; 4];
        let mass_flows = source.port_mass_flows(&state).unwrap();
        let enthalpies = source.port_enthalpies(&state).unwrap();

        assert_eq!(
            mass_flows.len(),
            enthalpies.len(),
            "port_mass_flows and port_enthalpies must have matching lengths"
        );
    }

    #[test]
    fn test_refrigerant_sink_mass_flow_enthalpy_length_match() {
        let backend = Arc::new(MockRefrigerantBackend::new());
        let port = make_port("R410A", 24.0e5, 465_000.0);
        let sink =
            RefrigerantSink::new("R410A", Pressure::from_pascals(24.0e5), None, backend, port)
                .unwrap();

        let state = vec![0.0; 4];
        let mass_flows = sink.port_mass_flows(&state).unwrap();
        let enthalpies = sink.port_enthalpies(&state).unwrap();

        assert_eq!(
            mass_flows.len(),
            enthalpies.len(),
            "port_mass_flows and port_enthalpies must have matching lengths"
        );
    }

    #[test]
    fn test_refrigerant_source_signature() {
        let backend = Arc::new(MockRefrigerantBackend::new());
        let port = make_port("R410A", 8.5e5, 260_000.0);
        let source = RefrigerantSource::new(
            "R410A",
            Pressure::from_pascals(8.5e5),
            VaporQuality::SATURATED_VAPOR,
            backend,
            port,
        )
        .unwrap();

        let sig = source.signature();
        assert!(sig.contains("RefrigerantSource"));
        assert!(sig.contains("R410A"));
    }

    #[test]
    fn test_refrigerant_sink_signature() {
        let backend = Arc::new(MockRefrigerantBackend::new());
        let port = make_port("R410A", 24.0e5, 465_000.0);
        let sink =
            RefrigerantSink::new("R410A", Pressure::from_pascals(24.0e5), None, backend, port)
                .unwrap();

        let sig = sink.signature();
        assert!(sig.contains("RefrigerantSink"));
        assert!(sig.contains("R410A"));
    }
}
