//! MovingBoundaryHX - Zone Discretization Heat Exchanger Component
//!
//! A heat exchanger component that discretizes the heat transfer area into
//! phase zones (superheated, two-phase, subcooled) for more accurate modeling
//! of refrigerant-side heat transfer.

use super::bphx_correlation::CorrelationSelector;
use super::bphx_geometry::BphxGeometry;
use super::eps_ntu::EpsNtuModel;
use super::exchanger::HeatExchanger;
use crate::state_machine::{CircuitId, OperationalState, StateManageable};
use crate::{
    Component, ComponentError, ConnectedPort, JacobianBuilder, ResidualVector, StateSlice,
};
use entropyk_core::{Enthalpy, MassFlow, Power};
use std::cell::{Cell, RefCell};
use std::sync::Arc;

/// Zone type for refrigerant-side phase classification
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum ZoneType {
    /// Superheated vapor zone (T > Tsat)
    Superheated,
    /// Two-phase zone (mixture of liquid and vapor)
    #[default]
    TwoPhase,
    /// Subcooled liquid zone (T < Tsat)
    Subcooled,
}

/// Zone boundary with relative position and zone type
#[derive(Debug, Clone)]
pub struct ZoneBoundary {
    /// Relative position along the heat exchanger (0.0 to 1.0)
    pub position: f64,
    /// Zone type at this boundary
    pub zone_type: ZoneType,
    /// UA value for this zone (W/K)
    pub ua: f64,
    /// Hot-side temperature at this boundary (K)
    pub t_hot: f64,
    /// Cold-side temperature at this boundary (K)
    pub t_cold: f64,
    /// Vapor quality at this boundary (0-1 for two-phase)
    pub quality: f64,
}

/// Zone discretization result containing all zones and summary data
#[derive(Debug, Clone, Default)]
pub struct ZoneDiscretization {
    /// List of zone boundaries (ordered by position)
    pub boundaries: Vec<ZoneBoundary>,
    /// Total UA (sum of all zone UAs) (W/K)
    pub total_ua: f64,
    /// Pinch temperature (minimum temperature difference) (K)
    pub pinch_temp: f64,
    /// Position of pinch point (relative, 0.0 to 1.0)
    pub pinch_position: f64,
}

/// Cache for MovingBoundaryHX calculations
#[derive(Debug, Clone, Default)]
pub struct MovingBoundaryCache {
    /// Whether the cache is valid and initialized
    pub valid: bool,
    /// Reference pressure for cache validity (Pa)
    pub p_ref: f64,
    /// Reference mass flow for cache validity (kg/s)
    pub m_ref: f64,
    /// Cached liquid saturation enthalpy (J/kg)
    pub h_sat_l: f64,
    /// Cached vapor saturation enthalpy (J/kg)
    pub h_sat_v: f64,
    /// Cached zone discretization result
    pub discretization: ZoneDiscretization,
}

impl MovingBoundaryCache {
    /// Checks if the cache remains valid given the current pressure and mass flow.
    /// Cache is valid if pressure deviates < 5% and mass flow deviates < 10%.
    pub fn is_valid_for(&self, p_current: f64, m_current: f64) -> bool {
        if !self.valid {
            return false;
        }

        let p_dev = (p_current - self.p_ref).abs() / self.p_ref.max(1e-10);
        let m_dev = (m_current - self.m_ref).abs() / self.m_ref.max(1e-10);

        p_dev < 0.05 && m_dev < 0.10
    }
}

/// MovingBoundaryHX - Zone discretization heat exchanger component
pub struct MovingBoundaryHX {
    inner: HeatExchanger<EpsNtuModel>,
    geometry: BphxGeometry,
    _correlation_selector: CorrelationSelector,
    refrigerant_id: String,
    secondary_fluid_id: String,
    fluid_backend: Option<Arc<dyn entropyk_fluids::FluidBackend>>,
    // Discretization parameters
    n_discretization: usize,
    cache: RefCell<MovingBoundaryCache>,

    // Internal state caching
    _last_htc: Cell<f64>,
    _last_validity_warning: Cell<bool>,
}

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

impl MovingBoundaryHX {
    /// Creates a new `MovingBoundaryHX` with default settings and 51 discretization points.
    pub fn new() -> Self {
        let geometry = BphxGeometry::from_dh_area(0.003, 0.5, 20);
        let model = EpsNtuModel::counter_flow(1000.0);

        Self {
            inner: HeatExchanger::new(model, "MovingBoundaryHX"),
            geometry,
            _correlation_selector: CorrelationSelector::default(),
            refrigerant_id: String::new(),
            secondary_fluid_id: String::new(),
            fluid_backend: None,
            n_discretization: 51,
            cache: RefCell::new(MovingBoundaryCache::default()),
            _last_htc: Cell::new(0.0),
            _last_validity_warning: Cell::new(false),
        }
    }

    /// Returns the number of discretization points.
    pub fn n_discretization(&self) -> usize {
        self.n_discretization
    }

    /// Sets the number of discretization points and returns self.
    pub fn with_discretization(mut self, n: usize) -> Self {
        self.n_discretization = n;
        self
    }

    /// Sets the geometry specification.
    pub fn with_geometry(mut self, geometry: BphxGeometry) -> Self {
        self.geometry = geometry;
        self
    }

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

    /// Sets the secondary fluid identifier.
    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 and returns self.
    pub fn with_fluid_backend(mut self, backend: Arc<dyn entropyk_fluids::FluidBackend>) -> Self {
        self.fluid_backend = Some(backend.clone());
        self.inner = self.inner.with_fluid_backend(backend);
        self
    }
}

impl Component for MovingBoundaryHX {
    fn n_equations(&self) -> usize {
        3
    }

    fn compute_residuals(
        &self,
        state: &StateSlice,
        residuals: &mut ResidualVector,
    ) -> Result<(), ComponentError> {
        let (ref_in, ref_out, sec_in, sec_out) = self.inner.live_fluid_states(state)?;
        let p = ref_in.pressure;
        let m_refrig = ref_in.mass_flow;
        let t_sec_in = sec_in.temperature;
        let t_sec_out = sec_out.temperature;
        let h_in = ref_in.enthalpy;
        let h_out = ref_out.enthalpy;

        let mut cache = self.cache.borrow_mut();
        let use_cache = cache.is_valid_for(p, m_refrig);

        if !use_cache {
            let (disc, h_sat_l, h_sat_v) =
                self.identify_zones(h_in, h_out, p, t_sec_in, t_sec_out)?;
            cache.valid = true;
            cache.p_ref = p;
            cache.m_ref = m_refrig;
            cache.h_sat_l = h_sat_l;
            cache.h_sat_v = h_sat_v;
            cache.discretization = disc;
        }

        let total_ua = cache.discretization.total_ua;
        let base_ua = self.inner.ua_nominal();
        let custom_ua_scale = if base_ua > 0.0 {
            total_ua / base_ua
        } else {
            1.0
        };

        self.inner
            .compute_residuals_with_ua_scale(state, residuals, custom_ua_scale)
    }

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

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

    fn set_port_context(&mut self, port_edges: &[Option<(usize, usize, usize)>]) {
        self.inner.set_port_context(port_edges);
    }

    fn port_names(&self) -> Vec<String> {
        self.inner.port_names()
    }

    fn flow_paths(&self) -> Vec<(usize, usize)> {
        self.inner.flow_paths()
    }

    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 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(backend.clone());
            self.inner.set_fluid_backend_from_builder(backend);
        }
    }

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

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

impl MovingBoundaryHX {
    /// Identifies the phase zones along the heat exchanger and calculates boundaries.
    pub fn identify_zones(
        &self,
        h_refrig_in: f64,
        h_refrig_out: f64,
        p_refrig: f64,
        t_secondary_in: f64,
        t_secondary_out: f64,
    ) -> Result<(ZoneDiscretization, f64, f64), ComponentError> {
        let backend = self.fluid_backend.as_ref().ok_or_else(|| {
            ComponentError::CalculationFailed("No FluidBackend configured".to_string())
        })?;
        let fluid = entropyk_fluids::FluidId::new(&self.refrigerant_id);
        let p = entropyk_core::Pressure::from_pascals(p_refrig);

        let h_sat_l = backend
            .property(
                fluid.clone(),
                entropyk_fluids::Property::Enthalpy,
                entropyk_fluids::FluidState::from_px(p, entropyk_fluids::Quality::new(0.0)),
            )
            .map_err(|e| ComponentError::CalculationFailed(format!("h_sat_l failed: {}", e)))?;
        let h_sat_v = backend
            .property(
                fluid.clone(),
                entropyk_fluids::Property::Enthalpy,
                entropyk_fluids::FluidState::from_px(p, entropyk_fluids::Quality::new(1.0)),
            )
            .map_err(|e| ComponentError::CalculationFailed(format!("h_sat_v failed: {}", e)))?;

        let mut boundaries = Vec::new();

        // Calculate transition positions and types
        let is_condensing = h_refrig_in > h_refrig_out;

        // Add inlet boundary
        let inlet_type = if h_refrig_in > h_sat_v + 1e-3 {
            ZoneType::Superheated
        } else if h_refrig_in < h_sat_l - 1e-3 {
            ZoneType::Subcooled
        } else {
            ZoneType::TwoPhase
        };
        boundaries.push(self.create_boundary(
            0.0,
            h_refrig_in,
            p_refrig,
            inlet_type,
            t_secondary_in,
            h_sat_l,
            h_sat_v,
        )?);

        let (h_min, h_max) = if is_condensing {
            (h_refrig_out, h_refrig_in)
        } else {
            (h_refrig_in, h_refrig_out)
        };

        if h_min < h_sat_l && h_max > h_sat_l {
            let pos = (h_sat_l - h_refrig_in) / (h_refrig_out - h_refrig_in);
            let t_sec = t_secondary_in + pos * (t_secondary_out - t_secondary_in);
            // After sat_l, type is SC (if condensing) or TP (if evaporating)
            let post_type = if is_condensing {
                ZoneType::Subcooled
            } else {
                ZoneType::TwoPhase
            };
            boundaries.push(
                self.create_boundary(pos, h_sat_l, p_refrig, post_type, t_sec, h_sat_l, h_sat_v)?,
            );
        }

        if h_min < h_sat_v && h_max > h_sat_v {
            let pos = (h_sat_v - h_refrig_in) / (h_refrig_out - h_refrig_in);
            let t_sec = t_secondary_in + pos * (t_secondary_out - t_secondary_in);
            // After sat_v, type is TP (if condensing) or SH (if evaporating)
            let post_type = if is_condensing {
                ZoneType::TwoPhase
            } else {
                ZoneType::Superheated
            };
            boundaries.push(
                self.create_boundary(pos, h_sat_v, p_refrig, post_type, t_sec, h_sat_l, h_sat_v)?,
            );
        }

        // Add outlet boundary
        let outlet_type = if h_refrig_out > h_sat_v + 1e-3 {
            ZoneType::Superheated
        } else if h_refrig_out < h_sat_l - 1e-3 {
            ZoneType::Subcooled
        } else {
            ZoneType::TwoPhase
        };
        boundaries.push(self.create_boundary(
            1.0,
            h_refrig_out,
            p_refrig,
            outlet_type,
            t_secondary_out,
            h_sat_l,
            h_sat_v,
        )?);

        // Sort boundaries by position
        boundaries.sort_by(|a, b| a.position.partial_cmp(&b.position).unwrap());

        // Calculate UA for each zone
        let mut total_ua = 0.0;
        for i in 0..boundaries.len() - 1 {
            let ua_zone = self.compute_zone_ua(&boundaries[i], &boundaries[i + 1])?;
            boundaries[i].ua = ua_zone;
            total_ua += ua_zone;
        }

        let (pinch_temp, pinch_pos) = self.calculate_pinch(&boundaries);

        Ok((
            ZoneDiscretization {
                boundaries,
                total_ua,
                pinch_temp,
                pinch_position: pinch_pos,
            },
            h_sat_l,
            h_sat_v,
        ))
    }

    fn compute_zone_ua(&self, b1: &ZoneBoundary, b2: &ZoneBoundary) -> Result<f64, ComponentError> {
        let area_zone = self.geometry.area * (b2.position - b1.position);
        if area_zone <= 1e-10 {
            return Ok(0.0);
        }

        // Without access to fluid phase properties and geometry correlation,
        // we use a simplified approximation based on zone type.
        // A true implementation would query self.correlation_selector
        let h_refrig = match b1.zone_type {
            ZoneType::TwoPhase => 5000.0,   // Boiling or condensation
            ZoneType::Superheated => 500.0, // Vapor
            ZoneType::Subcooled => 1500.0,  // Liquid
        };
        let h_secondary = 5000.0; // Generally high for water/glycol

        let u_overall = 1.0 / (1.0 / h_refrig + 1.0 / h_secondary);

        Ok(u_overall * area_zone)
    }

    fn calculate_pinch(&self, boundaries: &[ZoneBoundary]) -> (f64, f64) {
        let mut min_dt = f64::MAX;
        let mut pinch_pos = 0.0;

        for b in boundaries {
            let dt = (b.t_hot - b.t_cold).abs();
            if dt < min_dt {
                min_dt = dt;
                pinch_pos = b.position;
            }
        }

        (min_dt, pinch_pos)
    }

    fn create_boundary(
        &self,
        pos: f64,
        h: f64,
        p: f64,
        zone_type: ZoneType,
        t_sec: f64,
        h_sat_l: f64,
        h_sat_v: f64,
    ) -> Result<ZoneBoundary, ComponentError> {
        let quality = if h_sat_v > h_sat_l {
            ((h - h_sat_l) / (h_sat_v - h_sat_l)).clamp(0.0, 1.0)
        } else {
            0.0
        };

        let t_refrig = if let Some(backend) = &self.fluid_backend {
            let fluid = entropyk_fluids::FluidId::new(&self.refrigerant_id);
            backend
                .property(
                    fluid,
                    entropyk_fluids::Property::Temperature,
                    entropyk_fluids::FluidState::from_ph(
                        entropyk_core::Pressure::from_pascals(p),
                        entropyk_core::Enthalpy::from_joules_per_kg(h),
                    ),
                )
                .map_err(|e| ComponentError::CalculationFailed(format!("T_refrig failed: {}", e)))?
        } else {
            300.0
        };

        Ok(ZoneBoundary {
            position: pos,
            zone_type,
            ua: 0.0,
            t_hot: if t_sec > t_refrig { t_sec } else { t_refrig },
            t_cold: if t_sec > t_refrig { t_refrig } else { t_sec },
            quality,
        })
    }
}

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

    #[test]
    fn test_zone_type_enum_exists() {
        let zone = ZoneType::Superheated;
        assert_eq!(zone, ZoneType::Superheated);

        let zone = ZoneType::TwoPhase;
        assert_eq!(zone, ZoneType::TwoPhase);

        let zone = ZoneType::Subcooled;
        assert_eq!(zone, ZoneType::Subcooled);
    }

    #[test]
    fn test_zone_boundary_struct_exists() {
        let boundary = ZoneBoundary {
            position: 0.5,
            zone_type: ZoneType::TwoPhase,
            ua: 1000.0,
            t_hot: 300.0,
            t_cold: 290.0,
            quality: 0.5,
        };

        assert!((boundary.position - 0.5).abs() < 1e-10);
        assert_eq!(boundary.zone_type, ZoneType::TwoPhase);
        assert!((boundary.ua - 1000.0).abs() < 1e-10);
    }

    #[test]
    fn test_moving_boundary_hx_with_fluids() {
        let hx = MovingBoundaryHX::new()
            .with_refrigerant("R410A")
            .with_secondary_fluid("Water");
        assert_eq!(hx.refrigerant_id, "R410A");
        assert_eq!(hx.secondary_fluid_id, "Water");
    }

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

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

        impl entropyk_fluids::FluidBackend for MockBackend {
            fn property(
                &self,
                _fluid: FluidId,
                property: entropyk_fluids::Property,
                state: entropyk_fluids::FluidState,
            ) -> FluidResult<f64> {
                match property {
                    entropyk_fluids::Property::Temperature => Ok(self.t_sat),
                    entropyk_fluids::Property::Enthalpy => {
                        let q = match state {
                            entropyk_fluids::FluidState::PressureQuality(_, q) => Some(q.value()),
                            _ => None,
                        };
                        match q {
                            Some(0.0) => Ok(self.h_sat_l),
                            Some(1.0) => Ok(self.h_sat_v),
                            _ => 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: entropyk_fluids::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 = MockBackend {
            h_sat_l: 200_000.0,
            h_sat_v: 400_000.0,
            t_sat: 280.0,
        };

        let hx = MovingBoundaryHX::new()
            .with_refrigerant("R410A")
            .with_fluid_backend(Arc::new(backend));

        // Condensing: 450,000 (SH) -> 150,000 (SC)
        let result = hx.identify_zones(450_000.0, 150_000.0, 500_000.0, 300.0, 320.0);
        assert!(result.is_ok());
        let (disc, h_sat_l_res, h_sat_v_res) = result.unwrap();

        assert_eq!(h_sat_l_res, 200_000.0);
        assert_eq!(h_sat_v_res, 400_000.0);

        // Should have 4 boundaries: inlet(SH), sat_v(SH/TP), sat_l(TP/SC), outlet(SC)
        assert_eq!(disc.boundaries.len(), 4);
        assert_eq!(disc.boundaries[0].zone_type, ZoneType::Superheated);
        assert_eq!(disc.boundaries[1].zone_type, ZoneType::TwoPhase);
        assert_eq!(disc.boundaries[2].zone_type, ZoneType::Subcooled);
        assert_eq!(disc.boundaries[3].zone_type, ZoneType::Subcooled);

        // Total UA should be positive
        assert!(disc.total_ua > 0.0);
    }

    #[test]
    fn test_cache_is_valid_for() {
        let mut cache = MovingBoundaryCache {
            valid: true,
            p_ref: 100_000.0,
            m_ref: 1.0,
            h_sat_l: 100.0,
            h_sat_v: 200.0,
            discretization: ZoneDiscretization::default(),
        };

        // Identical
        assert!(cache.is_valid_for(100_000.0, 1.0));

        // P < 5% deviation (104,000 is 4%)
        assert!(cache.is_valid_for(104_000.0, 1.0));

        // P > 5% deviation (106,000 is 6%)
        assert!(!cache.is_valid_for(106_000.0, 1.0));

        // M < 10% deviation (1.09 is 9%)
        assert!(cache.is_valid_for(100_000.0, 1.09));

        // M > 10% deviation (1.11 is 11%)
        assert!(!cache.is_valid_for(100_000.0, 1.11));

        // Invalid if explicitly invalid
        cache.valid = false;
        assert!(!cache.is_valid_for(100_000.0, 1.0));
    }

    #[test]
    fn test_compute_residuals_uses_cache() {
        use crate::Component;
        use entropyk_core::Pressure;
        use entropyk_fluids::{
            CriticalPoint, FluidError, FluidId, FluidResult, Phase, ThermoState,
        };

        struct TrackingMockBackend {
            pub calls: std::sync::atomic::AtomicUsize,
        }

        impl entropyk_fluids::FluidBackend for TrackingMockBackend {
            fn property(
                &self,
                _fluid: FluidId,
                _property: entropyk_fluids::Property,
                _state: entropyk_fluids::FluidState,
            ) -> FluidResult<f64> {
                self.calls.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
                Ok(100.0)
            }
            fn critical_point(&self, _fluid: FluidId) -> FluidResult<CriticalPoint> {
                Err(FluidError::NoCriticalPoint {
                    fluid: "".to_string(),
                })
            }
            fn is_fluid_available(&self, _fluid: &FluidId) -> bool {
                true
            }
            fn phase(
                &self,
                _fluid: FluidId,
                _state: entropyk_fluids::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::new(TrackingMockBackend {
            calls: std::sync::atomic::AtomicUsize::new(0),
        });

        let mut hx = MovingBoundaryHX::new()
            .with_refrigerant("R410A")
            .with_secondary_fluid("Air")
            .with_fluid_backend(backend.clone());
        hx.set_port_context(&[
            Some((0, 1, 2)),
            Some((0, 3, 4)),
            Some((5, 6, 7)),
            Some((5, 8, 9)),
        ]);

        let h_air = |t_k: f64| 1006.0 * (t_k - 273.15);
        let state = vec![
            0.1,
            500_000.0,
            400_000.0,
            490_000.0,
            200_000.0,
            0.2,
            101_325.0,
            h_air(300.0),
            101_325.0,
            h_air(310.0),
        ];
        let mut residuals = vec![0.0; 3];

        // First call should calculate property (backend calls)
        let _ = hx.compute_residuals(&state, &mut residuals);
        let calls_first = backend.calls.load(std::sync::atomic::Ordering::SeqCst);
        assert!(calls_first > 0);

        // Second call with same state still evaluates live edge properties, but
        // should reuse cached zone identification.
        let _ = hx.compute_residuals(&state, &mut residuals);
        let calls_second = backend.calls.load(std::sync::atomic::Ordering::SeqCst);
        assert!(
            calls_second - calls_first < calls_first,
            "cache should avoid the zone-identification property calls"
        );
    }

    #[test]
    fn test_performance_speedup() {
        use crate::Component;
        use entropyk_core::Pressure;
        use entropyk_fluids::{
            CriticalPoint, FluidError, FluidId, FluidResult, Phase, ThermoState,
        };
        use std::time::Instant;

        struct SlowMockBackend;

        impl entropyk_fluids::FluidBackend for SlowMockBackend {
            fn property(
                &self,
                _fluid: FluidId,
                _property: entropyk_fluids::Property,
                _state: entropyk_fluids::FluidState,
            ) -> FluidResult<f64> {
                // Simulate somewhat slow fluid property calculation
                std::thread::sleep(std::time::Duration::from_micros(10));
                Ok(100.0)
            }
            fn critical_point(&self, _fluid: FluidId) -> FluidResult<CriticalPoint> {
                Err(FluidError::NoCriticalPoint {
                    fluid: "".to_string(),
                })
            }
            fn is_fluid_available(&self, _fluid: &FluidId) -> bool {
                true
            }
            fn phase(
                &self,
                _fluid: FluidId,
                _state: entropyk_fluids::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::new(SlowMockBackend);

        let hx = MovingBoundaryHX::new()
            .with_refrigerant("R410A")
            .with_fluid_backend(backend.clone());

        let state = vec![500_000.0, 400_000.0];
        let mut residuals = vec![0.0; 3];

        // First run (no cache)
        let start = Instant::now();
        let _ = hx.compute_residuals(&state, &mut residuals);
        let duration_uncached = start.elapsed();

        // Second run (cached)
        let start = Instant::now();
        let _ = hx.compute_residuals(&state, &mut residuals);
        let duration_cached = start.elapsed();

        println!("Uncached duration: {:?}", duration_uncached);
        println!("Cached duration: {:?}", duration_cached);

        let speedup = duration_uncached.as_secs_f64() / duration_cached.as_secs_f64().max(1e-9);
        println!("Speedup multiplier: {:.1}x", speedup);

        assert!(duration_cached < duration_uncached);
    }
}
