//! BphxEvaporator - Brazed Plate Heat Exchanger Evaporator Component
//!
//! A plate evaporator component. Brazed plate exchangers in this project are
//! modeled as Direct Expansion (DX) only: the refrigerant outlet is always
//! superheated vapor (x >= 1), controlled by a target superheat closure.
//!
//! A "flooded" operating style (liquid overfeed with vapor/liquid separation)
//! is a system-topology property achieved by connecting a `Drum` and
//! recirculation loop around a DX-terminated exchanger — it is not a distinct
//! mode of the plate exchanger itself, so it is intentionally not modeled here.
//! Use `FloodedEvaporator` for a shell-and-tube flooded evaporator.
//!
//! ## Example
//!
//! ```ignore
//! use entropyk_components::heat_exchanger::{BphxEvaporator, BphxGeometry};
//!
//! let geo = BphxGeometry::from_dh_area(0.003, 0.5, 20);
//! let evap = BphxEvaporator::new(geo)
//!     .with_target_superheat(5.0)
//!     .with_refrigerant("R410A");
//!
//! assert_eq!(evap.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;

/// BphxEvaporator - Brazed Plate Heat Exchanger configured as evaporator
///
/// Direct Expansion (DX) only: the refrigerant outlet is always superheated
/// vapor (x >= 1), controlled by a target superheat closure. Wraps a
/// `BphxExchanger` for base residual computation.
pub struct BphxEvaporator {
    inner: BphxExchanger,
    target_superheat_k: f64,
    /// When true, emit an outlet-closure residual pinning `h_out` to
    /// `h(P, Tsat(P) + target_superheat_k)` so evaporating pressure can emerge.
    emergent_pressure: bool,
    refrigerant_id: String,
    secondary_fluid_id: String,
    fluid_backend: Option<Arc<dyn FluidBackend>>,
    last_superheat: Cell<Option<f64>>,
    outlet_pressure_idx: Option<usize>,
    outlet_enthalpy_idx: Option<usize>,
}

impl std::fmt::Debug for BphxEvaporator {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_struct("BphxEvaporator")
            .field("ua", &self.inner.ua())
            .field("geometry", &self.inner.geometry())
            .field("target_superheat_k", &self.target_superheat_k)
            .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 BphxEvaporator {
    /// Creates a new BphxEvaporator with the specified geometry.
    ///
    /// # Arguments
    ///
    /// * `geometry` - BPHX geometry specification
    ///
    /// # Example
    ///
    /// ```
    /// use entropyk_components::heat_exchanger::{BphxEvaporator, BphxGeometry};
    /// use entropyk_components::Component;
    ///
    /// let geo = BphxGeometry::from_dh_area(0.003, 0.5, 20);
    /// let evap = BphxEvaporator::new(geo);
    /// assert_eq!(evap.n_equations(), 2);
    /// ```
    pub fn new(geometry: BphxGeometry) -> Self {
        let geometry = geometry.with_exchanger_type(BphxType::Evaporator);
        Self {
            inner: BphxExchanger::new(geometry),
            target_superheat_k: 5.0,
            emergent_pressure: false,
            refrigerant_id: String::new(),
            secondary_fluid_id: String::new(),
            fluid_backend: None,
            last_superheat: Cell::new(None),
            outlet_pressure_idx: None,
            outlet_enthalpy_idx: None,
        }
    }

    /// Creates a BphxEvaporator with a specified geometry and correlation.
    pub fn with_geometry(geometry: BphxGeometry) -> Self {
        Self::new(geometry)
    }

    /// Sets the target superheat (K) used as the DX outlet closure.
    pub fn with_target_superheat(mut self, target_superheat_k: f64) -> Self {
        self.target_superheat_k = target_superheat_k;
        self
    }

    /// Enables emergent-pressure mode: +1 residual pinning the refrigerant
    /// outlet enthalpy to `h(P, Tsat(P) + target_superheat_k)`.
    pub fn with_emergent_pressure(mut self) -> Self {
        self.emergent_pressure = true;
        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_cold_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_hot_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()
    }

    /// 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 target superheat (K) used as the DX outlet closure.
    pub fn target_superheat_k(&self) -> f64 {
        self.target_superheat_k
    }

    /// 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 last computed superheat (K).
    ///
    /// Returns `None` if:
    /// - `compute_residuals` has not been called
    /// - No FluidBackend configured
    pub fn superheat(&self) -> Option<f64> {
        self.last_superheat.get()
    }

    /// Sets the outlet state indices for quality/superheat control.
    ///
    /// 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 (secondary fluid) boundary conditions.
    pub fn set_secondary_conditions(&mut self, conditions: HxSideConditions) {
        self.inner.set_hot_conditions(conditions);
    }

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

    /// Computes outlet quality from enthalpy and saturation properties.
    ///
    /// Used only to validate that the DX outlet is genuinely superheated
    /// (quality >= 1.0). 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 superheat from outlet temperature and saturation temperature.
    ///
    /// Returns `None` if no FluidBackend is configured or saturation properties
    /// cannot be computed.
    fn compute_superheat(&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 t_sat = backend
            .property(
                fluid.clone(),
                Property::Temperature,
                FluidState::from_px(p, Quality::new(1.0)),
            )
            .ok()?;

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

        Some(t_out - t_sat)
    }

    /// Target outlet enthalpy `h(P, Tsat(P) + target_superheat_k)` [J/kg].
    fn h_superheat_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(1.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_superheat_k),
                ),
            )
            .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 the outlet is genuinely superheated (DX operation).
    ///
    /// # Errors
    ///
    /// Returns an error if outlet quality < 1.0 (not superheated).
    pub fn validate_outlet(&self, h_out: f64, p_pa: f64) -> Result<f64, ComponentError> {
        if self.refrigerant_id.is_empty() {
            return Err(ComponentError::InvalidState(
                "BphxEvaporator: refrigerant_id not set".to_string(),
            ));
        }
        if self.fluid_backend.is_none() {
            return Err(ComponentError::CalculationFailed(
                "BphxEvaporator: FluidBackend not configured".to_string(),
            ));
        }

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

        if quality < 1.0 {
            Err(ComponentError::InvalidState(format!(
                "BphxEvaporator DX mode: outlet quality {:.2} < 1.0 (not superheated)",
                quality
            )))
        } else {
            Ok(quality)
        }
    }
}

impl Component for BphxEvaporator {
    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(sh) = self.compute_superheat(h_out, p_pa) {
                    self.last_superheat.set(Some(sh));
                }

                if self.emergent_pressure {
                    let backend = self.fluid_backend.as_ref().ok_or_else(|| {
                        ComponentError::CalculationFailed(
                            "BphxEvaporator: FluidBackend required for emergent outlet closure"
                                .into(),
                        )
                    })?;
                    let fluid = FluidId::new(&self.refrigerant_id);
                    let h_target = self.h_superheat_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(
                    "BphxEvaporator: emergent outlet closure needs live outlet P/h indices".into(),
                ));
            }
        } else if self.emergent_pressure {
            return Err(ComponentError::InvalidState(
                "BphxEvaporator: 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_superheat_at_p(backend.as_ref(), fluid.clone(), p_pa + dp);
                    let hm = self.h_superheat_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 SH 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 = secondary, cold = refrigerant.
        let remapped = [
            port_edges.get(2).copied().flatten(),
            port_edges.get(3).copied().flatten(),
            port_edges.first().copied().flatten(),
            port_edges.get(1).copied().flatten(),
        ];
        self.inner.set_port_context(&remapped);
    }

    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 {
        let mode_str = format!("DX,tsh={:.1}K", self.target_superheat_k);
        format!(
            "BphxEvaporator({} plates, dh={:.2}mm, A={:.3}m², {}, {}, {})",
            self.inner.geometry().n_plates,
            self.inner.geometry().dh * 1000.0,
            self.inner.geometry().area,
            mode_str,
            self.inner.correlation_name(),
            self.refrigerant_id
        )
    }

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

impl StateManageable for BphxEvaporator {
    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)
    }

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

    #[test]
    fn test_bphx_evaporator_mode_default() {
        let geo = test_geometry();
        let evap = BphxEvaporator::new(geo);
        assert_eq!(evap.target_superheat_k(), 5.0);
    }

    #[test]
    fn test_bphx_evaporator_with_target_superheat() {
        let geo = test_geometry();
        let evap = BphxEvaporator::new(geo).with_target_superheat(10.0);

        assert_eq!(evap.target_superheat_k(), 10.0);
    }

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

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

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

    #[test]
    fn test_bphx_evaporator_with_correlation() {
        let geo = test_geometry();
        let evap = BphxEvaporator::new(geo).with_correlation(BphxCorrelation::Shah1979);

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

    #[test]
    fn test_bphx_evaporator_compute_residuals() {
        let geo = test_geometry();
        let evap = BphxEvaporator::new(geo);
        let state = vec![0.0; 10];
        let mut residuals = vec![0.0; 3];

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

    #[test]
    fn test_bphx_evaporator_exposes_four_port_metadata() {
        let geo = test_geometry();
        let evap = BphxEvaporator::new(geo);

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

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

    #[test]
    fn test_bphx_evaporator_set_state() {
        let geo = test_geometry();
        let mut evap = BphxEvaporator::new(geo);

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

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

    #[test]
    fn test_bphx_evaporator_calib_default() {
        let geo = test_geometry();
        let evap = BphxEvaporator::new(geo);
        let calib = evap.calib();
        assert_eq!(calib.z_ua, 1.0);
    }

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

    #[test]
    fn test_bphx_evaporator_geometry() {
        let geo = test_geometry();
        let evap = BphxEvaporator::new(geo.clone());
        assert_eq!(evap.geometry().n_plates, geo.n_plates);
    }

    #[test]
    fn test_bphx_evaporator_signature_dx() {
        let geo = test_geometry();
        let evap = BphxEvaporator::new(geo)
            .with_target_superheat(5.0)
            .with_refrigerant("R410A");

        let sig = evap.signature();
        assert!(sig.contains("BphxEvaporator"));
        assert!(sig.contains("DX"));
        assert!(sig.contains("R410A"));
        assert!(sig.contains("tsh=5.0K"));
    }

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

    #[test]
    fn test_bphx_evaporator_superheat_initial() {
        let geo = test_geometry();
        let evap = BphxEvaporator::new(geo).with_target_superheat(5.0);
        assert_eq!(evap.superheat(), None);
    }

    #[test]
    fn test_bphx_evaporator_compute_htc() {
        let geo = test_geometry();
        let evap = BphxEvaporator::new(geo);

        let result = evap
            .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_evaporator_compute_pressure_drop() {
        let geo = test_geometry();
        let evap = BphxEvaporator::new(geo.clone());

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

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

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

    #[test]
    fn test_bphx_evaporator_validate_outlet_no_refrigerant() {
        let geo = test_geometry();
        let evap = BphxEvaporator::new(geo);
        let result = evap.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_evaporator_validate_outlet_no_backend() {
        let geo = test_geometry();
        let evap = BphxEvaporator::new(geo).with_refrigerant("R134a");
        let result = evap.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_evaporator_update_ua_from_htc() {
        let geo = test_geometry();
        let area = geo.area;
        let mut evap = BphxEvaporator::new(geo);

        let h = 8000.0;
        let ua_before = evap.ua();
        evap.update_ua_from_htc(h);
        let ua_after = evap.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_evaporator_set_outlet_indices() {
        let geo = test_geometry();
        let mut evap = BphxEvaporator::new(geo);
        evap.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 = evap.compute_residuals(&state, &mut residuals);
        assert!(matches!(result, Err(ComponentError::InvalidState(_))));
    }

    #[test]
    fn test_bphx_evaporator_superheat_with_mock_backend() {
        use entropyk_core::Enthalpy;
        use entropyk_fluids::{CriticalPoint, FluidError, FluidResult, Phase, ThermoState};

        struct MockBackend;

        impl entropyk_fluids::FluidBackend for MockBackend {
            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,
                        };
                        let t_sat = 280.0;
                        let cp = 4180.0;
                        match h {
                            Some(h_val) => Ok(t_sat + (h_val.to_joules_per_kg() - 400_000.0) / cp),
                            None => Ok(t_sat),
                        }
                    }
                    Property::Enthalpy => Ok(400_000.0),
                    _ => 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![]
            }
        }

        let backend: Arc<dyn entropyk_fluids::FluidBackend> = Arc::new(MockBackend);
        let geo = test_geometry();
        let evap = BphxEvaporator::new(geo)
            .with_target_superheat(5.0)
            .with_refrigerant("R134a")
            .with_fluid_backend(backend);

        let sh = evap.compute_superheat(420_000.0, 300_000.0);
        assert!(sh.is_some());
        let sh_val = sh.unwrap();
        assert!(sh_val > 0.0);
    }

    #[test]
    fn test_bphx_evaporator_quality_with_mock_backend() {
        use entropyk_core::Enthalpy;
        use entropyk_fluids::{CriticalPoint, FluidError, FluidResult, Phase, ThermoState};

        struct MockBackend;

        impl entropyk_fluids::FluidBackend for MockBackend {
            fn property(
                &self,
                _fluid: FluidId,
                property: Property,
                state: FluidState,
            ) -> FluidResult<f64> {
                match property {
                    Property::Enthalpy => {
                        let q = match state {
                            FluidState::PressureQuality(_, q) => Some(q.value()),
                            _ => None,
                        };
                        match q {
                            Some(q_val) => Ok(200_000.0 + q_val * 200_000.0),
                            None => Ok(300_000.0),
                        }
                    }
                    _ => 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![]
            }
        }

        let backend: Arc<dyn entropyk_fluids::FluidBackend> = Arc::new(MockBackend);
        let geo = test_geometry();
        let evap = BphxEvaporator::new(geo)
            .with_target_superheat(5.0)
            .with_refrigerant("R134a")
            .with_fluid_backend(backend);

        let q = evap.compute_quality(340_000.0, 300_000.0);
        assert!(q.is_some());
        let q_val = q.unwrap();
        assert!((0.0..=1.0).contains(&q_val));
    }

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