//! BphxCondenser - Brazed Plate Heat Exchanger Condenser Component
//!
//! A plate condenser component for refrigerant condensation with
//! geometry-based heat transfer correlations and subcooling calculation.
//!
//! ## Features
//!
//! - Subcooled liquid outlet (quality <= 0)
//! - Geometry-based heat transfer coefficient calculation
//! - Longo (2004) condensation correlation as default
//! - Calib factor support (f_ua, f_dp)
//!
//! ## Example
//!
//! ```ignore
//! use entropyk_components::heat_exchanger::{BphxCondenser, BphxGeometry};
//!
//! let geo = BphxGeometry::from_dh_area(0.003, 0.5, 20);
//! let cond = BphxCondenser::new(geo)
//!     .with_refrigerant("R410A")
//!     .with_target_subcooling(3.0);
//!
//! assert_eq!(cond.n_equations(), 3);
//! ```

use super::bphx_correlation::{BphxCorrelation, CorrelationEvaluation};
use super::bphx_dp::BphxDpCorrelation;
use super::bphx_exchanger::BphxExchanger;
use super::bphx_geometry::{BphxGeometry, BphxType};
use super::correlation_registry::CorrelationSelectionError;
use super::exchanger::HxSideConditions;
use crate::state_machine::{CircuitId, OperationalState, StateManageable};
use crate::{
    Component, ComponentError, ConnectedPort, JacobianBuilder, ResidualVector, StateSlice,
};
use entropyk_core::{Calib, Enthalpy, MassFlow, Power, Pressure, Temperature};
use entropyk_fluids::{FluidBackend, FluidId, FluidState, Property, Quality};
use std::cell::Cell;
use std::sync::Arc;

/// BphxCondenser - Brazed Plate Heat Exchanger configured as condenser
///
/// Supports condensation with subcooled liquid outlet.
/// Wraps a `BphxExchanger` for base residual computation.
pub struct BphxCondenser {
    inner: BphxExchanger,
    refrigerant_id: String,
    secondary_fluid_id: String,
    fluid_backend: Option<Arc<dyn FluidBackend>>,
    last_subcooling: Cell<Option<f64>>,
    last_outlet_quality: Cell<Option<f64>>,
    target_subcooling: f64,
    /// When true, emit an outlet-closure residual pinning `h_out` to
    /// `h(P, Tsat(P) − target_subcooling)`.
    emergent_pressure: bool,
    outlet_pressure_idx: Option<usize>,
    outlet_enthalpy_idx: Option<usize>,
}

impl std::fmt::Debug for BphxCondenser {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_struct("BphxCondenser")
            .field("ua", &self.inner.ua())
            .field("geometry", &self.inner.geometry())
            .field("target_subcooling", &self.target_subcooling)
            .field("emergent_pressure", &self.emergent_pressure)
            .field("refrigerant_id", &self.refrigerant_id)
            .field("secondary_fluid_id", &self.secondary_fluid_id)
            .field("has_fluid_backend", &self.fluid_backend.is_some())
            .finish()
    }
}

impl BphxCondenser {
    /// Default target subcooling in Kelvin
    pub const DEFAULT_TARGET_SUBCOOLING: f64 = 3.0;

    /// Creates a new BphxCondenser with the specified geometry.
    ///
    /// The geometry's `exchanger_type` is automatically set to `BphxType::Condenser`.
    ///
    /// # Arguments
    ///
    /// * `geometry` - BPHX geometry specification
    ///
    /// # Example
    ///
    /// ```
    /// use entropyk_components::heat_exchanger::{BphxCondenser, BphxGeometry};
    /// use entropyk_components::Component;
    ///
    /// let geo = BphxGeometry::from_dh_area(0.003, 0.5, 20);
    /// let cond = BphxCondenser::new(geo);
    /// assert_eq!(cond.n_equations(), 2);
    /// ```
    pub fn new(geometry: BphxGeometry) -> Self {
        let geometry = geometry.with_exchanger_type(BphxType::Condenser);
        Self {
            inner: BphxExchanger::new(geometry),
            refrigerant_id: String::new(),
            secondary_fluid_id: String::new(),
            fluid_backend: None,
            last_subcooling: Cell::new(None),
            last_outlet_quality: Cell::new(None),
            target_subcooling: Self::DEFAULT_TARGET_SUBCOOLING,
            emergent_pressure: false,
            outlet_pressure_idx: None,
            outlet_enthalpy_idx: None,
        }
    }

    /// Enables emergent-pressure mode: +1 residual pinning the refrigerant
    /// outlet enthalpy to `h(P, Tsat(P) − target_subcooling)`.
    pub fn with_emergent_pressure(mut self, subcooling_k: f64) -> Self {
        self.emergent_pressure = true;
        self.target_subcooling = subcooling_k.max(0.0);
        self
    }

    /// Returns whether emergent-pressure outlet closure is enabled.
    pub fn emergent_pressure(&self) -> bool {
        self.emergent_pressure
    }

    /// Sets the refrigerant fluid identifier.
    pub fn with_refrigerant(mut self, fluid: impl Into<String>) -> Self {
        self.refrigerant_id = fluid.into();
        self.inner.set_refrigerant_id(self.refrigerant_id.clone());
        self.inner.set_hot_fluid(self.refrigerant_id.clone());
        self
    }

    /// Sets the secondary fluid identifier (water, brine, etc.).
    pub fn with_secondary_fluid(mut self, fluid: impl Into<String>) -> Self {
        self.secondary_fluid_id = fluid.into();
        self.inner.set_cold_fluid(self.secondary_fluid_id.clone());
        self
    }

    /// Attaches a fluid backend for property queries.
    pub fn with_fluid_backend(mut self, backend: Arc<dyn FluidBackend>) -> Self {
        self.inner
            .set_fluid_backend_from_builder(Arc::clone(&backend));
        self.fluid_backend = Some(backend);
        self
    }

    /// Sets the heat transfer correlation.
    pub fn with_correlation(mut self, correlation: BphxCorrelation) -> Self {
        self.inner = self.inner.with_correlation(correlation);
        self
    }

    /// Sets the channel frictional pressure-drop correlation (independent of HTC).
    pub fn with_dp_correlation(mut self, correlation: BphxDpCorrelation) -> Self {
        self.inner = self.inner.with_dp_correlation(correlation);
        self
    }

    /// Returns the configured pressure-drop correlation.
    pub fn dp_correlation(&self) -> BphxDpCorrelation {
        self.inner.dp_correlation()
    }

    /// Sets the target subcooling in Kelvin.
    ///
    /// # Panics
    ///
    /// Panics if `sc` is negative (subcooling must be >= 0 K).
    pub fn with_target_subcooling(mut self, sc: f64) -> Self {
        assert!(sc >= 0.0, "target_subcooling must be >= 0 K, got {}", sc);
        self.target_subcooling = sc;
        self
    }

    /// Returns the component name.
    pub fn name(&self) -> &str {
        self.inner.name()
    }

    /// Returns the geometry specification.
    pub fn geometry(&self) -> &BphxGeometry {
        self.inner.geometry()
    }

    /// Returns the effective UA value (W/K).
    pub fn ua(&self) -> f64 {
        self.inner.ua()
    }

    /// Returns calibration factors.
    pub fn calib(&self) -> &Calib {
        self.inner.calib()
    }

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

    /// Returns the target subcooling (K).
    pub fn target_subcooling(&self) -> f64 {
        self.target_subcooling
    }

    /// Returns the last computed subcooling (K).
    ///
    /// Returns `None` if:
    /// - `compute_residuals` has not been called
    /// - No FluidBackend configured
    pub fn subcooling(&self) -> Option<f64> {
        self.last_subcooling.get()
    }

    /// Returns the last computed outlet quality.
    ///
    /// For a condenser, this should be <= 0 (subcooled liquid).
    pub fn outlet_quality(&self) -> Option<f64> {
        self.last_outlet_quality.get()
    }

    /// Sets the outlet state indices for subcooling calculation.
    ///
    /// These indices point to the pressure and enthalpy in the global state vector
    /// that represent the refrigerant outlet conditions.
    pub fn set_outlet_indices(&mut self, p_idx: usize, h_idx: usize) {
        self.outlet_pressure_idx = Some(p_idx);
        self.outlet_enthalpy_idx = Some(h_idx);
    }

    /// Sets the hot side (refrigerant) boundary conditions.
    pub fn set_refrigerant_conditions(&mut self, conditions: HxSideConditions) {
        self.inner.set_hot_conditions(conditions);
    }

    /// Sets the cold side (secondary fluid) boundary conditions.
    pub fn set_secondary_conditions(&mut self, conditions: HxSideConditions) {
        self.inner.set_cold_conditions(conditions);
    }

    /// Computes outlet quality from enthalpy and saturation properties.
    ///
    /// Returns `None` if no FluidBackend is configured or saturation properties
    /// cannot be computed.
    fn compute_quality(&self, h_out: f64, p_pa: f64) -> Option<f64> {
        if self.refrigerant_id.is_empty() {
            return None;
        }
        let backend = self.fluid_backend.as_ref()?;
        let fluid = FluidId::new(&self.refrigerant_id);
        let p = Pressure::from_pascals(p_pa);

        let h_sat_l = backend
            .property(
                fluid.clone(),
                Property::Enthalpy,
                FluidState::from_px(p, Quality::new(0.0)),
            )
            .ok()?;
        let h_sat_v = backend
            .property(
                fluid,
                Property::Enthalpy,
                FluidState::from_px(p, Quality::new(1.0)),
            )
            .ok()?;

        if h_sat_v > h_sat_l {
            let quality = (h_out - h_sat_l) / (h_sat_v - h_sat_l);
            Some(quality)
        } else {
            None
        }
    }

    /// Computes subcooling from outlet enthalpy and saturation properties.
    ///
    /// Subcooling = T_sat - T_outlet
    ///
    /// - Positive value: outlet is subcooled liquid (T_outlet < T_sat)
    /// - Zero: outlet is saturated liquid
    /// - Negative value: outlet is two-phase or superheated (invalid for condenser)
    ///
    /// Or equivalently: SC = (h_sat_l - h_outlet) / cp_l
    ///
    /// Returns `None` if no FluidBackend is configured or saturation properties
    /// cannot be computed.
    fn compute_subcooling(&self, h_out: f64, p_pa: f64) -> Option<f64> {
        if self.refrigerant_id.is_empty() {
            return None;
        }
        let backend = self.fluid_backend.as_ref()?;
        let fluid = FluidId::new(&self.refrigerant_id);
        let p = Pressure::from_pascals(p_pa);

        let _h_sat_l = backend
            .property(
                fluid.clone(),
                Property::Enthalpy,
                FluidState::from_px(p, Quality::new(0.0)),
            )
            .ok()?;

        let t_sat = backend
            .property(
                fluid.clone(),
                Property::Temperature,
                FluidState::from_px(p, Quality::new(0.0)),
            )
            .ok()?;

        let t_out = backend
            .property(
                fluid,
                Property::Temperature,
                FluidState::from_ph(p, Enthalpy::from_joules_per_kg(h_out)),
            )
            .ok()?;

        Some(t_sat - t_out)
    }

    /// Target outlet enthalpy `h(P, Tsat(P) − target_subcooling)` [J/kg].
    fn h_subcool_at_p(
        &self,
        backend: &dyn FluidBackend,
        fluid: FluidId,
        p_pa: f64,
    ) -> Result<f64, ComponentError> {
        let t_sat_k = backend
            .property(
                fluid.clone(),
                Property::Temperature,
                FluidState::from_px(Pressure::from_pascals(p_pa), Quality::new(0.0)),
            )
            .map_err(|e| ComponentError::CalculationFailed(e.to_string()))?;
        if self.target_subcooling <= 1e-9 {
            return backend
                .property(
                    fluid,
                    Property::Enthalpy,
                    FluidState::from_px(Pressure::from_pascals(p_pa), Quality::new(0.0)),
                )
                .map_err(|e| ComponentError::CalculationFailed(e.to_string()));
        }
        backend
            .property(
                fluid,
                Property::Enthalpy,
                FluidState::PressureTemperature(
                    Pressure::from_pascals(p_pa),
                    Temperature::from_kelvin(t_sat_k - self.target_subcooling),
                ),
            )
            .map_err(|e| ComponentError::CalculationFailed(e.to_string()))
    }

    /// Computes the heat transfer coefficient using the configured correlation.
    #[allow(clippy::too_many_arguments)]
    pub fn compute_htc(
        &self,
        mass_flux: f64,
        quality: f64,
        rho_l: f64,
        rho_v: f64,
        mu_l: f64,
        mu_v: f64,
        k_l: f64,
        pr_l: f64,
        t_sat: f64,
        t_wall: f64,
    ) -> Result<CorrelationEvaluation, CorrelationSelectionError> {
        self.inner.compute_htc(
            mass_flux, quality, rho_l, rho_v, mu_l, mu_v, k_l, pr_l, t_sat, t_wall,
        )
    }

    /// Computes the pressure drop using the correlation.
    pub fn compute_pressure_drop(&self, mass_flux: f64, rho: f64) -> f64 {
        self.inner.compute_pressure_drop(mass_flux, rho)
    }

    /// Updates UA based on computed HTC.
    pub fn update_ua_from_htc(&mut self, h: f64) {
        self.inner.update_ua_from_htc(h);
    }

    /// Validates that outlet is subcooled (quality <= 0).
    ///
    /// # Errors
    ///
    /// - Returns error if outlet quality > 0 (not subcooled)
    /// - Returns error if refrigerant_id not set
    /// - Returns error if FluidBackend not configured
    pub fn validate_outlet(&self, h_out: f64, p_pa: f64) -> Result<f64, ComponentError> {
        if self.refrigerant_id.is_empty() {
            return Err(ComponentError::InvalidState(
                "BphxCondenser: refrigerant_id not set".to_string(),
            ));
        }
        if self.fluid_backend.is_none() {
            return Err(ComponentError::CalculationFailed(
                "BphxCondenser: FluidBackend not configured".to_string(),
            ));
        }

        let quality = self.compute_quality(h_out, p_pa).ok_or_else(|| {
            ComponentError::CalculationFailed(format!(
                "BphxCondenser: Cannot compute quality for {} at P={:.0} Pa",
                self.refrigerant_id, p_pa
            ))
        })?;

        if quality > 0.0 {
            Err(ComponentError::InvalidState(format!(
                "BphxCondenser: outlet quality {:.2} > 0 (not subcooled). Outlet must be subcooled liquid.",
                quality
            )))
        } else {
            Ok(quality)
        }
    }
}

impl Component for BphxCondenser {
    fn n_equations(&self) -> usize {
        self.inner.n_equations() + usize::from(self.emergent_pressure)
    }

    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(),
            });
        }
        self.inner.compute_residuals(state, residuals)?;

        if let (Some(p_idx), Some(h_idx)) = (self.outlet_pressure_idx, self.outlet_enthalpy_idx) {
            if p_idx < state.len() && h_idx < state.len() {
                let p_pa = state[p_idx];
                let h_out = state[h_idx];

                if let Some(sc) = self.compute_subcooling(h_out, p_pa) {
                    self.last_subcooling.set(Some(sc));
                }

                if let Some(q) = self.compute_quality(h_out, p_pa) {
                    self.last_outlet_quality.set(Some(q));
                }

                if self.emergent_pressure {
                    let backend = self.fluid_backend.as_ref().ok_or_else(|| {
                        ComponentError::CalculationFailed(
                            "BphxCondenser: FluidBackend required for emergent outlet closure"
                                .into(),
                        )
                    })?;
                    let fluid = FluidId::new(&self.refrigerant_id);
                    let h_target = self.h_subcool_at_p(backend.as_ref(), fluid, p_pa)?;
                    residuals[self.inner.n_equations()] = h_out - h_target;
                }
            } else if self.emergent_pressure {
                return Err(ComponentError::InvalidState(
                    "BphxCondenser: emergent outlet closure needs live outlet P/h indices".into(),
                ));
            }
        } else if self.emergent_pressure {
            return Err(ComponentError::InvalidState(
                "BphxCondenser: emergent outlet closure needs live outlet P/h indices".into(),
            ));
        }

        Ok(())
    }

    fn jacobian_entries(
        &self,
        state: &StateSlice,
        jacobian: &mut JacobianBuilder,
    ) -> Result<(), ComponentError> {
        self.inner.jacobian_entries(state, jacobian)?;

        if self.emergent_pressure {
            let (Some(p_idx), Some(h_idx)) = (self.outlet_pressure_idx, self.outlet_enthalpy_idx)
            else {
                return Ok(());
            };
            if p_idx >= state.len() || h_idx >= state.len() {
                return Ok(());
            }
            let row = self.inner.n_equations();
            jacobian.add_entry(row, h_idx, 1.0);
            if let Some(backend) = self.fluid_backend.as_ref() {
                let p_pa = state[p_idx];
                if p_pa > 10_000.0 {
                    let dp = p_pa * 1e-4 + 100.0;
                    let fluid = FluidId::new(&self.refrigerant_id);
                    let hp = self.h_subcool_at_p(backend.as_ref(), fluid.clone(), p_pa + dp);
                    let hm = self.h_subcool_at_p(backend.as_ref(), fluid, (p_pa - dp).max(1.0));
                    if let (Ok(hp), Ok(hm)) = (hp, hm) {
                        jacobian.add_entry(row, p_idx, -(hp - hm) / (2.0 * dp));
                    }
                }
            }
        }
        Ok(())
    }

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

    fn set_port_context(&mut self, port_edges: &[Option<(usize, usize, usize)>]) {
        // Refrigerant outlet = port index 1 → capture P/h for SC closure.
        if let Some(Some((_, p, h))) = port_edges.get(1) {
            self.outlet_pressure_idx = Some(*p);
            self.outlet_enthalpy_idx = Some(*h);
        }
        // Inner HeatExchanger: hot = refrigerant, cold = secondary (same order).
        self.inner.set_port_context(port_edges);
    }

    fn port_names(&self) -> Vec<String> {
        vec![
            "inlet".to_string(),
            "outlet".to_string(),
            "secondary_inlet".to_string(),
            "secondary_outlet".to_string(),
        ]
    }

    fn flow_paths(&self) -> Vec<(usize, usize)> {
        vec![(0, 1), (2, 3)]
    }

    fn set_calib_indices(&mut self, indices: entropyk_core::CalibIndices) {
        self.inner.set_calib_indices(indices);
    }

    fn port_mass_flows(&self, state: &StateSlice) -> Result<Vec<MassFlow>, ComponentError> {
        self.inner.port_mass_flows(state)
    }

    fn port_enthalpies(&self, state: &StateSlice) -> Result<Vec<Enthalpy>, ComponentError> {
        self.inner.port_enthalpies(state)
    }

    fn energy_transfers(&self, state: &StateSlice) -> Option<(Power, Power)> {
        self.inner.energy_transfers(state)
    }

    fn measure_output(&self, kind: crate::MeasuredOutput, state: &StateSlice) -> Option<f64> {
        self.inner.measure_output(kind, state)
    }

    fn set_fluid_backend_from_builder(
        &mut self,
        backend: std::sync::Arc<dyn entropyk_fluids::FluidBackend>,
    ) {
        if self.fluid_backend.is_none() {
            self.fluid_backend = Some(Arc::clone(&backend));
        }
        self.inner.set_fluid_backend_from_builder(backend);
    }

    fn signature(&self) -> String {
        format!(
            "BphxCondenser({} plates, dh={:.2}mm, A={:.3}m², {}, SC={:.1}K, {})",
            self.inner.geometry().n_plates,
            self.inner.geometry().dh * 1000.0,
            self.inner.geometry().area,
            self.inner.correlation_name(),
            self.target_subcooling,
            self.refrigerant_id
        )
    }

    fn update_calib_factor(&mut self, factor: &str, value: f64) -> bool {
        self.inner.update_calib_factor(factor, value)
    }
}

impl StateManageable for BphxCondenser {
    fn state(&self) -> OperationalState {
        self.inner.state()
    }

    fn set_state(&mut self, state: OperationalState) -> Result<(), ComponentError> {
        self.inner.set_state(state)
    }

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

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

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

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

    fn test_geometry() -> BphxGeometry {
        BphxGeometry::from_dh_area(0.003, 0.5, 20)
    }

    use entropyk_core::Enthalpy;
    use entropyk_fluids::{CriticalPoint, FluidError, FluidResult, Phase, ThermoState};

    struct MockCondenserBackend {
        h_sat_l: f64,
        h_sat_v: f64,
        t_sat: f64,
    }

    impl Default for MockCondenserBackend {
        fn default() -> Self {
            Self {
                h_sat_l: 250_000.0,
                h_sat_v: 450_000.0,
                t_sat: 320.0,
            }
        }
    }

    impl entropyk_fluids::FluidBackend for MockCondenserBackend {
        fn property(
            &self,
            _fluid: FluidId,
            property: Property,
            state: FluidState,
        ) -> FluidResult<f64> {
            match property {
                Property::Temperature => {
                    let h = match state {
                        FluidState::PressureEnthalpy(_, h) => Some(h),
                        FluidState::PressureQuality(_, _) => None,
                        _ => None,
                    };
                    match h {
                        Some(h_val) => {
                            let cp_l = 4180.0;
                            Ok(self.t_sat - (self.h_sat_l - h_val.to_joules_per_kg()) / cp_l)
                        }
                        None => Ok(self.t_sat),
                    }
                }
                Property::Enthalpy => {
                    let q = match state {
                        FluidState::PressureQuality(_, q) => Some(q.value()),
                        _ => None,
                    };
                    match q {
                        Some(q_val) => Ok(self.h_sat_l + q_val * (self.h_sat_v - self.h_sat_l)),
                        None => Ok(self.h_sat_v),
                    }
                }
                _ => Err(FluidError::UnsupportedProperty {
                    property: format!("{:?}", property),
                }),
            }
        }

        fn critical_point(&self, _fluid: FluidId) -> FluidResult<CriticalPoint> {
            Err(FluidError::NoCriticalPoint { fluid: _fluid.0 })
        }

        fn is_fluid_available(&self, _fluid: &FluidId) -> bool {
            true
        }

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

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

        fn list_fluids(&self) -> Vec<FluidId> {
            vec![]
        }
    }

    #[test]
    fn test_bphx_condenser_creation() {
        let geo = test_geometry();
        let cond = BphxCondenser::new(geo);
        assert_eq!(cond.n_equations(), 2);
        assert!(cond.ua() > 0.0);
    }

    #[test]
    fn test_bphx_condenser_default_target_subcooling() {
        let geo = test_geometry();
        let cond = BphxCondenser::new(geo);
        assert_eq!(
            cond.target_subcooling(),
            BphxCondenser::DEFAULT_TARGET_SUBCOOLING
        );
        assert_eq!(cond.target_subcooling(), 3.0);
    }

    #[test]
    fn test_bphx_condenser_with_target_subcooling() {
        let geo = test_geometry();
        let cond = BphxCondenser::new(geo).with_target_subcooling(5.0);

        assert_eq!(cond.target_subcooling(), 5.0);
    }

    #[test]
    fn test_bphx_condenser_emergent_pressure_adds_equation() {
        let geo = test_geometry();
        let base = BphxCondenser::new(geo.clone());
        let emergent = BphxCondenser::new(geo).with_emergent_pressure(5.0);
        assert!(!base.emergent_pressure());
        assert!(emergent.emergent_pressure());
        assert_eq!(emergent.target_subcooling(), 5.0);
        assert_eq!(emergent.n_equations(), base.n_equations() + 1);
    }

    #[test]
    fn test_bphx_condenser_with_refrigerant() {
        let geo = test_geometry();
        let cond = BphxCondenser::new(geo).with_refrigerant("R410A");
        assert_eq!(cond.refrigerant_id, "R410A");
        assert_eq!(cond.inner.refrigerant_id(), Some("R410A"));
    }

    #[test]
    fn test_bphx_condenser_with_secondary_fluid() {
        let geo = test_geometry();
        let cond = BphxCondenser::new(geo).with_secondary_fluid("Water");
        assert_eq!(cond.secondary_fluid_id, "Water");
    }

    #[test]
    fn test_bphx_condenser_with_correlation() {
        let geo = test_geometry();
        let cond = BphxCondenser::new(geo).with_correlation(BphxCorrelation::Shah1979);

        let sig = cond.signature();
        assert!(sig.contains("Shah (1979)"));
    }

    #[test]
    fn test_bphx_condenser_compute_residuals() {
        let geo = test_geometry();
        let cond = BphxCondenser::new(geo);
        let state = vec![0.0; 10];
        let mut residuals = vec![0.0; 3];

        let result = cond.compute_residuals(&state, &mut residuals);
        assert!(matches!(result, Err(ComponentError::InvalidState(_))));
    }

    #[test]
    fn test_bphx_condenser_exposes_four_port_metadata() {
        let geo = test_geometry();
        let cond = BphxCondenser::new(geo);

        assert_eq!(
            cond.port_names(),
            vec![
                "inlet".to_string(),
                "outlet".to_string(),
                "secondary_inlet".to_string(),
                "secondary_outlet".to_string(),
            ]
        );
        assert_eq!(cond.flow_paths(), vec![(0, 1), (2, 3)]);
    }

    #[test]
    fn test_bphx_condenser_state_manageable() {
        let geo = test_geometry();
        let cond = BphxCondenser::new(geo);
        assert_eq!(cond.state(), OperationalState::On);
        assert!(cond.can_transition_to(OperationalState::Off));
    }

    #[test]
    fn test_bphx_condenser_set_state() {
        let geo = test_geometry();
        let mut cond = BphxCondenser::new(geo);

        let result = cond.set_state(OperationalState::Off);
        assert!(result.is_ok());
        assert_eq!(cond.state(), OperationalState::Off);

        let result = cond.set_state(OperationalState::Bypass);
        assert!(result.is_ok());
        assert_eq!(cond.state(), OperationalState::Bypass);
    }

    #[test]
    fn test_bphx_condenser_calib_default() {
        let geo = test_geometry();
        let cond = BphxCondenser::new(geo);
        let calib = cond.calib();
        assert_eq!(calib.z_ua, 1.0);
    }

    #[test]
    fn test_bphx_condenser_set_calib() {
        let geo = test_geometry();
        let mut cond = BphxCondenser::new(geo);
        let mut calib = Calib::default();
        calib.z_ua = 0.9;
        cond.set_calib(calib);
        assert_eq!(cond.calib().z_ua, 0.9);
    }

    #[test]
    fn test_bphx_condenser_geometry() {
        let geo = test_geometry();
        let cond = BphxCondenser::new(geo.clone());
        assert_eq!(cond.geometry().n_plates, geo.n_plates);
    }

    #[test]
    fn test_bphx_condenser_signature() {
        let geo = test_geometry();
        let cond = BphxCondenser::new(geo)
            .with_target_subcooling(5.0)
            .with_refrigerant("R410A");

        let sig = cond.signature();
        assert!(sig.contains("BphxCondenser"));
        assert!(sig.contains("R410A"));
        assert!(sig.contains("SC=5.0K"));
    }

    #[test]
    fn test_bphx_condenser_energy_transfers() {
        let geo = test_geometry();
        let cond = BphxCondenser::new(geo);
        let state = vec![0.0; 10];
        let (heat, work) = cond.energy_transfers(&state).unwrap();
        assert_eq!(heat.to_watts(), 0.0);
        assert_eq!(work.to_watts(), 0.0);
    }

    #[test]
    fn test_bphx_condenser_subcooling_initial() {
        let geo = test_geometry();
        let cond = BphxCondenser::new(geo);
        assert_eq!(cond.subcooling(), None);
    }

    #[test]
    fn test_bphx_condenser_outlet_quality_initial() {
        let geo = test_geometry();
        let cond = BphxCondenser::new(geo);
        assert_eq!(cond.outlet_quality(), None);
    }

    #[test]
    fn test_bphx_condenser_compute_htc() {
        let geo = test_geometry();
        let cond = BphxCondenser::new(geo);

        let result = cond
            .compute_htc(
                30.0, 0.5, 1100.0, 30.0, 0.0002, 0.000012, 0.1, 3.5, 280.0, 285.0,
            )
            .unwrap();

        assert!(result.h > 0.0);
        assert!(result.re > 0.0);
        assert!(result.nu > 0.0);
    }

    #[test]
    fn test_bphx_condenser_compute_pressure_drop() {
        let geo = test_geometry();
        let cond = BphxCondenser::new(geo.clone());

        let dp = cond.compute_pressure_drop(30.0, 1100.0);
        assert!(dp >= 0.0);

        let mut cond_with_calib = BphxCondenser::new(geo);
        let mut calib = Calib::default();
        calib.z_dp = 0.5;
        cond_with_calib.set_calib(calib);

        let dp_calib = cond_with_calib.compute_pressure_drop(30.0, 1100.0);
        assert!((dp_calib - dp * 0.5).abs() < 1e-6);
    }

    #[test]
    fn test_bphx_condenser_validate_outlet_no_refrigerant() {
        let geo = test_geometry();
        let cond = BphxCondenser::new(geo);
        let result = cond.validate_outlet(400_000.0, 300_000.0);
        assert!(result.is_err());
        match result {
            Err(ComponentError::InvalidState(msg)) => {
                assert!(msg.contains("refrigerant_id not set"));
            }
            _ => panic!("Expected InvalidState error"),
        }
    }

    #[test]
    fn test_bphx_condenser_validate_outlet_no_backend() {
        let geo = test_geometry();
        let cond = BphxCondenser::new(geo).with_refrigerant("R134a");
        let result = cond.validate_outlet(400_000.0, 300_000.0);
        assert!(result.is_err());
        match result {
            Err(ComponentError::CalculationFailed(msg)) => {
                assert!(msg.contains("FluidBackend not configured"));
            }
            _ => panic!("Expected CalculationFailed error"),
        }
    }

    #[test]
    fn test_bphx_condenser_update_ua_from_htc() {
        let geo = test_geometry();
        let area = geo.area;
        let mut cond = BphxCondenser::new(geo);

        let h = 8000.0;
        let ua_before = cond.ua();
        cond.update_ua_from_htc(h);
        let ua_after = cond.ua();

        assert!(ua_after > ua_before);
        let expected_ua = h * area;
        assert!((ua_after - expected_ua).abs() / expected_ua < 0.01);
    }

    #[test]
    fn test_bphx_condenser_set_outlet_indices() {
        let geo = test_geometry();
        let mut cond = BphxCondenser::new(geo);
        cond.set_outlet_indices(2, 3);

        let state = vec![0.0, 0.0, 300_000.0, 400_000.0, 0.0, 0.0];
        let mut residuals = vec![0.0; 3];
        let result = cond.compute_residuals(&state, &mut residuals);
        assert!(matches!(result, Err(ComponentError::InvalidState(_))));
    }

    #[test]
    fn test_bphx_condenser_subcooling_with_mock_backend() {
        let backend: Arc<dyn entropyk_fluids::FluidBackend> =
            Arc::new(MockCondenserBackend::default());
        let geo = test_geometry();
        let cond = BphxCondenser::new(geo)
            .with_refrigerant("R134a")
            .with_fluid_backend(backend);

        let sc = cond.compute_subcooling(240_000.0, 1_000_000.0);
        assert!(sc.is_some());
        let sc_val = sc.unwrap();
        assert!(sc_val > 0.0);
    }

    #[test]
    fn test_bphx_condenser_quality_with_mock_backend() {
        let backend: Arc<dyn entropyk_fluids::FluidBackend> =
            Arc::new(MockCondenserBackend::default());
        let geo = test_geometry();
        let cond = BphxCondenser::new(geo)
            .with_refrigerant("R134a")
            .with_fluid_backend(backend);

        let q = cond.compute_quality(200_000.0, 1_000_000.0);
        assert!(q.is_some());
        let q_val = q.unwrap();
        assert!(q_val <= 0.0);
    }

    #[test]
    fn test_bphx_condenser_validate_outlet_subcooled() {
        let backend: Arc<dyn entropyk_fluids::FluidBackend> =
            Arc::new(MockCondenserBackend::default());
        let geo = test_geometry();
        let cond = BphxCondenser::new(geo)
            .with_refrigerant("R134a")
            .with_fluid_backend(backend);

        let result = cond.validate_outlet(200_000.0, 1_000_000.0);
        assert!(result.is_ok());
        let quality = result.unwrap();
        assert!(quality <= 0.0);
    }

    #[test]
    fn test_bphx_condenser_validate_outlet_two_phase() {
        let backend: Arc<dyn entropyk_fluids::FluidBackend> =
            Arc::new(MockCondenserBackend::default());
        let geo = test_geometry();
        let cond = BphxCondenser::new(geo)
            .with_refrigerant("R134a")
            .with_fluid_backend(backend);

        let result = cond.validate_outlet(350_000.0, 1_000_000.0);
        assert!(result.is_err());
        match result {
            Err(ComponentError::InvalidState(msg)) => {
                assert!(msg.contains("not subcooled"));
            }
            _ => panic!("Expected InvalidState error"),
        }
    }

    #[test]
    fn test_bphx_condenser_default_selection_is_automatic() {
        let geo = test_geometry();
        let cond = BphxCondenser::new(geo);
        let sig = cond.signature();
        assert!(
            sig.contains("Automatic"),
            "Default correlation policy should be automatic for condensation"
        );
    }

    #[test]
    fn test_bphx_condenser_negative_subcooling_panics() {
        let geo = test_geometry();
        let result = std::panic::catch_unwind(|| {
            BphxCondenser::new(geo).with_target_subcooling(-5.0);
        });
        assert!(result.is_err(), "with_target_subcooling(-5.0) should panic");
    }
}
