//! Generic Heat Exchanger Component
//!
//! A heat exchanger with 4 ports (hot inlet, hot outlet, cold inlet, cold outlet)
//! and a pluggable heat transfer model.
//!
//! ## Fluid Backend Integration (Story 5.1)
//!
//! When a `FluidBackend` is provided via `with_fluid_backend()`, `compute_residuals`
//! queries the backend for real Cp and enthalpy values at the boundary conditions
//! instead of using hardcoded placeholder values.

use super::model::{FluidState, HeatTransferModel};
use crate::state_machine::{CircuitId, OperationalState, StateManageable};
use crate::{
    Component, ComponentError, ConnectedPort, JacobianBuilder, ResidualVector, StateSlice,
};
use entropyk_core::{Calib, MassFlow, Pressure, Temperature};
use entropyk_fluids::{FluidBackend, FluidId as FluidsFluidId, Property, ThermoState};
use std::marker::PhantomData;
use std::sync::Arc;

/// Builder for creating a heat exchanger with disconnected ports.
pub struct HeatExchangerBuilder<Model: HeatTransferModel> {
    model: Model,
    name: String,
    circuit_id: CircuitId,
}

impl<Model: HeatTransferModel + 'static> HeatExchangerBuilder<Model> {
    /// Creates a new builder.
    pub fn new(model: Model) -> Self {
        Self {
            model,
            name: String::from("HeatExchanger"),
            circuit_id: CircuitId::default(),
        }
    }

    /// Sets the name.
    pub fn name(mut self, name: impl Into<String>) -> Self {
        self.name = name.into();
        self
    }

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

    /// Builds the heat exchanger with placeholder connected ports.
    pub fn build(self) -> HeatExchanger<Model> {
        HeatExchanger::new(self.model, self.name).with_circuit_id(self.circuit_id)
    }
}

/// Generic heat exchanger component with 4 ports.
///
/// Uses the Strategy Pattern for heat transfer calculations via the
/// `HeatTransferModel` trait.
///
/// # Type Parameters
///
/// * `Model` - The heat transfer model (LmtdModel, EpsNtuModel, etc.)
///
/// # Ports
///
/// - `hot_inlet`: Hot fluid inlet
/// - `hot_outlet`: Hot fluid outlet
/// - `cold_inlet`: Cold fluid inlet
/// - `cold_outlet`: Cold fluid outlet
///
/// # Equations
///
/// The heat exchanger contributes 3 residual equations:
/// 1. Hot side energy balance
/// 2. Cold side energy balance
/// 3. Energy conservation (Q_hot = Q_cold)
///
/// # Operational States
///
/// - **On**: Normal heat transfer operation
/// - **Off**: Zero mass flow on both sides, no heat transfer
/// - **Bypass**: Mass flow continues, no heat transfer (adiabatic)
///
/// # Example
///
/// ```
/// use entropyk_components::heat_exchanger::{HeatExchanger, LmtdModel, FlowConfiguration};
/// use entropyk_components::Component;
///
/// let model = LmtdModel::new(5000.0, FlowConfiguration::CounterFlow);
/// let hx = HeatExchanger::new(model, "Condenser");
/// assert_eq!(hx.n_equations(), 3);
/// ```
/// Boundary conditions for one side of the heat exchanger.
///
/// Specifies the inlet state for a fluid stream: temperature, pressure, mass flow,
/// and the fluid identity used to query thermodynamic properties from the backend.
#[derive(Debug, Clone)]
pub struct HxSideConditions {
    temperature_k: f64,
    pressure_pa: f64,
    mass_flow_kg_s: f64,
    fluid_id: FluidsFluidId,
}

impl HxSideConditions {
    /// Returns the inlet temperature in Kelvin.
    pub fn temperature_k(&self) -> f64 {
        self.temperature_k
    }
    /// Returns the inlet pressure in Pascals.
    pub fn pressure_pa(&self) -> f64 {
        self.pressure_pa
    }
    /// Returns the mass flow rate in kg/s.
    pub fn mass_flow_kg_s(&self) -> f64 {
        self.mass_flow_kg_s
    }
    /// Returns a reference to the fluid identifier.
    pub fn fluid_id(&self) -> &FluidsFluidId {
        &self.fluid_id
    }
}

impl HxSideConditions {
    /// Creates a new set of boundary conditions.
    pub fn new(
        temperature: Temperature,
        pressure: Pressure,
        mass_flow: MassFlow,
        fluid_id: impl Into<String>,
    ) -> Result<Self, ComponentError> {
        let t = temperature.to_kelvin();
        let p = pressure.to_pascals();
        let m = mass_flow.to_kg_per_s();

        // Basic validation for physically plausible states
        if t <= 0.0 {
            return Err(ComponentError::InvalidState(
                "Temperature must be greater than 0 K".to_string(),
            ));
        }
        if p <= 0.0 {
            return Err(ComponentError::InvalidState(
                "Pressure must be strictly positive".to_string(),
            ));
        }
        if m < 0.0 {
            return Err(ComponentError::InvalidState(
                "Mass flow must be non-negative".to_string(),
            ));
        }

        Ok(Self {
            temperature_k: t,
            pressure_pa: p,
            mass_flow_kg_s: m,
            fluid_id: FluidsFluidId::new(fluid_id),
        })
    }
}

/// Generic heat exchanger component with 4 ports.
///
/// Uses the Strategy Pattern for heat transfer calculations via the
/// `HeatTransferModel` trait. When a `FluidBackend` is attached via
/// [`with_fluid_backend`](Self::with_fluid_backend), the `compute_residuals`
/// method queries real thermodynamic properties (Cp, h) from the backend
/// instead of using hardcoded placeholder values.
pub struct HeatExchanger<Model: HeatTransferModel> {
    model: Model,
    name: String,
    /// Calibration: f_dp for refrigerant-side ΔP when modeled, f_ua for UA scaling
    calib: Calib,
    /// Indices for dynamically extracting calibration factors from the system state
    calib_indices: entropyk_core::CalibIndices,
    operational_state: OperationalState,
    circuit_id: CircuitId,
    /// Optional fluid property backend for real thermodynamic calculations (Story 5.1).
    fluid_backend: Option<Arc<dyn FluidBackend>>,
    /// Boundary conditions for the hot side inlet.
    hot_conditions: Option<HxSideConditions>,
    /// Boundary conditions for the cold side inlet.
    cold_conditions: Option<HxSideConditions>,
    _phantom: PhantomData<()>,
}

impl<Model: HeatTransferModel + std::fmt::Debug> std::fmt::Debug for HeatExchanger<Model> {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_struct("HeatExchanger")
            .field("name", &self.name)
            .field("model", &self.model)
            .field("calib", &self.calib)
            .field("operational_state", &self.operational_state)
            .field("circuit_id", &self.circuit_id)
            .field("has_fluid_backend", &self.fluid_backend.is_some())
            .finish()
    }
}

impl<Model: HeatTransferModel + 'static> HeatExchanger<Model> {
    /// Creates a new heat exchanger with the given model.
    pub fn new(mut model: Model, name: impl Into<String>) -> Self {
        let calib = Calib::default();
        model.set_ua_scale(calib.f_ua);
        Self {
            model,
            name: name.into(),
            calib,
            calib_indices: entropyk_core::CalibIndices::default(),
            operational_state: OperationalState::default(),
            circuit_id: CircuitId::default(),
            fluid_backend: None,
            hot_conditions: None,
            cold_conditions: None,
            _phantom: PhantomData,
        }
    }

    /// Attaches a `FluidBackend` so `compute_residuals` can query real thermodynamic properties.
    ///
    /// # Example
    ///
    /// ```no_run
    /// use entropyk_components::heat_exchanger::{HeatExchanger, LmtdModel, FlowConfiguration, HxSideConditions};
    /// use entropyk_fluids::{TestBackend, FluidId};
    /// use entropyk_core::{Temperature, Pressure, MassFlow};
    /// use std::sync::Arc;
    ///
    /// let model = LmtdModel::new(5000.0, FlowConfiguration::CounterFlow);
    /// let hx = HeatExchanger::new(model, "Condenser")
    ///     .with_fluid_backend(Arc::new(TestBackend::new()))
    ///     .with_hot_conditions(HxSideConditions::new(
    ///         Temperature::from_celsius(60.0),
    ///         Pressure::from_bar(25.0),
    ///         MassFlow::from_kg_per_s(0.05),
    ///         "R410A",
    ///     ).unwrap())
    ///     .with_cold_conditions(HxSideConditions::new(
    ///         Temperature::from_celsius(30.0),
    ///         Pressure::from_bar(1.5),
    ///         MassFlow::from_kg_per_s(0.2),
    ///         "Water",
    ///     ).unwrap());
    /// ```
    pub fn with_fluid_backend(mut self, backend: Arc<dyn FluidBackend>) -> Self {
        self.fluid_backend = Some(backend);
        self
    }

    /// Sets the hot side boundary conditions for fluid property queries.
    pub fn with_hot_conditions(mut self, conditions: HxSideConditions) -> Self {
        self.hot_conditions = Some(conditions);
        self
    }

    /// Sets the cold side boundary conditions for fluid property queries.
    pub fn with_cold_conditions(mut self, conditions: HxSideConditions) -> Self {
        self.cold_conditions = Some(conditions);
        self
    }

    /// Sets the hot side boundary conditions (mutable).
    pub fn set_hot_conditions(&mut self, conditions: HxSideConditions) {
        self.hot_conditions = Some(conditions);
    }

    /// Sets the cold side boundary conditions (mutable).
    pub fn set_cold_conditions(&mut self, conditions: HxSideConditions) {
        self.cold_conditions = Some(conditions);
    }

    /// Attaches a fluid backend (mutable).
    pub fn set_fluid_backend(&mut self, backend: Arc<dyn FluidBackend>) {
        self.fluid_backend = Some(backend);
    }

    /// Returns true if a real `FluidBackend` is attached.
    pub fn has_fluid_backend(&self) -> bool {
        self.fluid_backend.is_some()
    }

    /// Returns the hot side fluid identifier, if set.
    pub fn hot_conditions(&self) -> Option<&HxSideConditions> {
        self.hot_conditions.as_ref()
    }

    pub fn cold_conditions(&self) -> Option<&HxSideConditions> {
        self.cold_conditions.as_ref()
    }

    pub fn hot_fluid_id(&self) -> Option<&FluidsFluidId> {
        self.hot_conditions.as_ref().map(|c| c.fluid_id())
    }

    /// Returns the cold side fluid identifier, if set.
    pub fn cold_fluid_id(&self) -> Option<&FluidsFluidId> {
        self.cold_conditions.as_ref().map(|c| c.fluid_id())
    }

    /// Computes the full thermodynamic state at the hot inlet.
    pub fn hot_inlet_state(&self) -> Result<ThermoState, ComponentError> {
        let backend = self.fluid_backend.as_ref().ok_or_else(|| {
            ComponentError::CalculationFailed("No FluidBackend configured".to_string())
        })?;
        let conditions = self.hot_conditions.as_ref().ok_or_else(|| {
            ComponentError::CalculationFailed("Hot conditions not set".to_string())
        })?;
        let h = self.query_enthalpy(conditions)?;
        backend
            .full_state(
                conditions.fluid_id().clone(),
                Pressure::from_pascals(conditions.pressure_pa()),
                entropyk_core::Enthalpy::from_joules_per_kg(h),
            )
            .map_err(|e| {
                ComponentError::CalculationFailed(format!(
                    "Failed to compute hot inlet state: {}",
                    e
                ))
            })
    }

    /// Computes the full thermodynamic state at the cold inlet.
    pub fn cold_inlet_state(&self) -> Result<ThermoState, ComponentError> {
        let backend = self.fluid_backend.as_ref().ok_or_else(|| {
            ComponentError::CalculationFailed("No FluidBackend configured".to_string())
        })?;
        let conditions = self.cold_conditions.as_ref().ok_or_else(|| {
            ComponentError::CalculationFailed("Cold conditions not set".to_string())
        })?;
        let h = self.query_enthalpy(conditions)?;
        backend
            .full_state(
                conditions.fluid_id().clone(),
                Pressure::from_pascals(conditions.pressure_pa()),
                entropyk_core::Enthalpy::from_joules_per_kg(h),
            )
            .map_err(|e| {
                ComponentError::CalculationFailed(format!(
                    "Failed to compute cold inlet state: {}",
                    e
                ))
            })
    }

    /// Queries Cp (J/(kg·K)) from the backend for a given side.
    fn query_cp(&self, conditions: &HxSideConditions) -> Result<f64, ComponentError> {
        if let Some(backend) = &self.fluid_backend {
            let state = entropyk_fluids::FluidState::from_pt(
                Pressure::from_pascals(conditions.pressure_pa()),
                Temperature::from_kelvin(conditions.temperature_k()),
            );
            backend
                .property(conditions.fluid_id().clone(), Property::Cp, state) // Need to clone FluidId because trait signature requires it for now? Actually FluidId can be cloned cheaply depending on its implementation. We'll leave the clone if required by `property()`. Let's assume it is.
                .map_err(|e| {
                    ComponentError::CalculationFailed(format!(
                        "FluidBackend Cp query failed: {}",
                        e
                    ))
                })
        } else {
            Err(ComponentError::CalculationFailed(
                "No FluidBackend configured".to_string(),
            ))
        }
    }

    /// Queries specific enthalpy (J/kg) from the backend for a given side at (P, T).
    fn query_enthalpy(&self, conditions: &HxSideConditions) -> Result<f64, ComponentError> {
        if let Some(backend) = &self.fluid_backend {
            let state = entropyk_fluids::FluidState::from_pt(
                Pressure::from_pascals(conditions.pressure_pa()),
                Temperature::from_kelvin(conditions.temperature_k()),
            );
            backend
                .property(conditions.fluid_id().clone(), Property::Enthalpy, state)
                .map_err(|e| {
                    ComponentError::CalculationFailed(format!(
                        "FluidBackend Enthalpy query failed: {}",
                        e
                    ))
                })
        } else {
            Err(ComponentError::CalculationFailed(
                "No FluidBackend configured".to_string(),
            ))
        }
    }

    /// Sets the circuit identifier and returns self.
    pub fn with_circuit_id(mut self, circuit_id: CircuitId) -> Self {
        self.circuit_id = circuit_id;
        self
    }

    /// Returns the name of this heat exchanger.
    pub fn name(&self) -> &str {
        &self.name
    }

    /// Returns the effective UA value (f_ua × UA_nominal).
    pub fn ua(&self) -> f64 {
        self.model.effective_ua(None)
    }

    /// Returns the nominal (base) UA value [W/K] before any scaling.
    pub fn ua_nominal(&self) -> f64 {
        self.model.ua()
    }

    /// Sets the UA scale factor directly (UA_eff = scale × UA_nominal).
    ///
    /// Used by `MchxCondenserCoil` to apply fan-speed and air-density corrections
    /// without rebuilding the component.
    pub fn set_ua_scale(&mut self, scale: f64) {
        self.model.set_ua_scale(scale.max(0.0));
    }

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

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

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

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

    /// Returns calibration factors (f_dp for refrigerant-side ΔP when modeled, f_ua for UA).
    pub fn calib(&self) -> &Calib {
        &self.calib
    }

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

    /// Creates a fluid state from temperature, pressure, enthalpy, mass flow, and Cp.
    fn create_fluid_state(
        temperature: f64,
        pressure: f64,
        enthalpy: f64,
        mass_flow: f64,
        cp: f64,
    ) -> FluidState {
        FluidState::new(temperature, pressure, enthalpy, mass_flow, cp)
    }

    pub fn compute_residuals_with_ua_scale(
        &self,
        _state: &StateSlice,
        residuals: &mut ResidualVector,
        custom_ua_scale: f64,
    ) -> Result<(), ComponentError> {
        self.do_compute_residuals(_state, residuals, Some(custom_ua_scale))
    }

    pub fn do_compute_residuals(
        &self,
        _state: &StateSlice,
        residuals: &mut ResidualVector,
        custom_ua_scale: Option<f64>,
    ) -> Result<(), ComponentError> {
        if residuals.len() < self.n_equations() {
            return Err(ComponentError::InvalidResidualDimensions {
                expected: self.n_equations(),
                actual: residuals.len(),
            });
        }

        match self.operational_state {
            OperationalState::Off => {
                // In OFF mode: Q = 0, mass flow = 0 on both sides
                // All residuals should be zero (no heat transfer, no flow)
                residuals[0] = 0.0; // Hot side: no energy transfer
                residuals[1] = 0.0; // Cold side: no energy transfer
                residuals[2] = 0.0; // Energy conservation (Q_hot = Q_cold = 0)
                return Ok(());
            }
            OperationalState::Bypass => {
                // In BYPASS mode: Q = 0, mass flow continues
                // Temperature continuity (T_out = T_in for both sides)
                residuals[0] = 0.0; // Hot side: no energy transfer (adiabatic)
                residuals[1] = 0.0; // Cold side: no energy transfer (adiabatic)
                residuals[2] = 0.0; // Energy conservation (Q_hot = Q_cold = 0)
                return Ok(());
            }
            OperationalState::On => {
                // Normal operation - continue with heat transfer model
            }
        }

        let (hot_inlet, hot_outlet, cold_inlet, cold_outlet) =
            if let (Some(hot_cond), Some(cold_cond), Some(_backend)) = (
                &self.hot_conditions,
                &self.cold_conditions,
                &self.fluid_backend,
            ) {
                // Hot side from backend
                let hot_cp = self.query_cp(hot_cond)?;
                let hot_h_in = self.query_enthalpy(hot_cond)?;
                let hot_inlet = Self::create_fluid_state(
                    hot_cond.temperature_k(),
                    hot_cond.pressure_pa(),
                    hot_h_in,
                    hot_cond.mass_flow_kg_s(),
                    hot_cp,
                );

                let hot_dh = hot_cp * 5.0; // J/kg per degree
                let hot_outlet = Self::create_fluid_state(
                    hot_cond.temperature_k() - 5.0,
                    hot_cond.pressure_pa() * 0.998,
                    hot_h_in - hot_dh,
                    hot_cond.mass_flow_kg_s(),
                    hot_cp,
                );

                // Cold side from backend
                let cold_cp = self.query_cp(cold_cond)?;
                let cold_h_in = self.query_enthalpy(cold_cond)?;
                let cold_inlet = Self::create_fluid_state(
                    cold_cond.temperature_k(),
                    cold_cond.pressure_pa(),
                    cold_h_in,
                    cold_cond.mass_flow_kg_s(),
                    cold_cp,
                );
                let cold_dh = cold_cp * 5.0;
                let cold_outlet = Self::create_fluid_state(
                    cold_cond.temperature_k() + 5.0,
                    cold_cond.pressure_pa() * 0.998,
                    cold_h_in + cold_dh,
                    cold_cond.mass_flow_kg_s(),
                    cold_cp,
                );

                (hot_inlet, hot_outlet, cold_inlet, cold_outlet)
            } else {
                let hot_inlet = Self::create_fluid_state(350.0, 500_000.0, 400_000.0, 0.1, 1000.0);
                let hot_outlet = Self::create_fluid_state(330.0, 490_000.0, 380_000.0, 0.1, 1000.0);
                let cold_inlet = Self::create_fluid_state(290.0, 101_325.0, 80_000.0, 0.2, 4180.0);
                let cold_outlet =
                    Self::create_fluid_state(300.0, 101_325.0, 120_000.0, 0.2, 4180.0);
                (hot_inlet, hot_outlet, cold_inlet, cold_outlet)
            };

        let dynamic_f_ua = custom_ua_scale.or_else(|| self.calib_indices.f_ua.map(|idx| _state[idx]));

        self.model.compute_residuals(
            &hot_inlet,
            &hot_outlet,
            &cold_inlet,
            &cold_outlet,
            residuals,
            dynamic_f_ua,
        );

        Ok(())
    }
}

impl<Model: HeatTransferModel + 'static> Component for HeatExchanger<Model> {
    fn compute_residuals(
        &self,
        _state: &StateSlice,
        residuals: &mut ResidualVector,
    ) -> Result<(), ComponentError> {
        self.do_compute_residuals(_state, residuals, None)
    }

    fn jacobian_entries(
        &self,
        _state: &StateSlice,
        _jacobian: &mut JacobianBuilder,
    ) -> Result<(), ComponentError> {
        // ∂r/∂f_ua = -∂Q/∂f_ua (Story 5.5)
        if let Some(f_ua_idx) = self.calib_indices.f_ua {
            // Need to compute Q_nominal (with UA_scale = 1.0)
            // This requires repeating the residual calculation logic with dynamic_ua_scale = None
            // For now, we'll use a finite difference approximation or a simplified nominal calculation.

            // Re-use logic from compute_residuals but only for Q
            if let (Some(hot_cond), Some(cold_cond), Some(_backend)) = (
                &self.hot_conditions,
                &self.cold_conditions,
                &self.fluid_backend,
            ) {
                let hot_cp = self.query_cp(hot_cond)?;
                let hot_h_in = self.query_enthalpy(hot_cond)?;
                let hot_inlet = Self::create_fluid_state(
                    hot_cond.temperature_k(),
                    hot_cond.pressure_pa(),
                    hot_h_in,
                    hot_cond.mass_flow_kg_s(),
                    hot_cp,
                );

                let hot_dh = hot_cp * 5.0;
                let hot_outlet = Self::create_fluid_state(
                    hot_cond.temperature_k() - 5.0,
                    hot_cond.pressure_pa() * 0.998,
                    hot_h_in - hot_dh,
                    hot_cond.mass_flow_kg_s(),
                    hot_cp,
                );

                let cold_cp = self.query_cp(cold_cond)?;
                let cold_h_in = self.query_enthalpy(cold_cond)?;
                let cold_inlet = Self::create_fluid_state(
                    cold_cond.temperature_k(),
                    cold_cond.pressure_pa(),
                    cold_h_in,
                    cold_cond.mass_flow_kg_s(),
                    cold_cp,
                );
                let cold_dh = cold_cp * 5.0;
                let cold_outlet = Self::create_fluid_state(
                    cold_cond.temperature_k() + 5.0,
                    cold_cond.pressure_pa() * 0.998,
                    cold_h_in + cold_dh,
                    cold_cond.mass_flow_kg_s(),
                    cold_cp,
                );

                let q_nominal = self
                    .model
                    .compute_heat_transfer(&hot_inlet, &hot_outlet, &cold_inlet, &cold_outlet, None)
                    .to_watts();

                // r0 = Q_hot - Q -> ∂r0/∂f_ua = -Q_nominal
                // r1 = Q_cold - Q -> ∂r1/∂f_ua = -Q_nominal
                // r2 = Q_hot - Q_cold -> ∂r2/∂f_ua = 0
                _jacobian.add_entry(0, f_ua_idx, -q_nominal);
                _jacobian.add_entry(1, f_ua_idx, -q_nominal);
                _jacobian.add_entry(2, f_ua_idx, 0.0);
            }
        }
        Ok(())
    }

    fn n_equations(&self) -> usize {
        self.model.n_equations()
    }

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

    fn get_ports(&self) -> &[ConnectedPort] {
        // TODO: Return actual ports when port storage is implemented.
        // Port storage pending integration with Port<Connected> system from Story 1.3.
        &[]
    }

    fn port_mass_flows(
        &self,
        _state: &StateSlice,
    ) -> Result<Vec<entropyk_core::MassFlow>, ComponentError> {
        // HeatExchanger has two sides: hot and cold, each with inlet and outlet.
        // Mass balance: hot_in = hot_out, cold_in = cold_out (no mixing between sides)
        //
        // For now, we use the configured conditions if available.
        // When port storage is implemented, this will use actual port state.
        let mut flows = Vec::with_capacity(4);

        if let Some(hot_cond) = &self.hot_conditions {
            let m_hot = hot_cond.mass_flow_kg_s();
            // Hot inlet (positive = entering), Hot outlet (negative = leaving)
            flows.push(entropyk_core::MassFlow::from_kg_per_s(m_hot));
            flows.push(entropyk_core::MassFlow::from_kg_per_s(-m_hot));
        }

        if let Some(cold_cond) = &self.cold_conditions {
            let m_cold = cold_cond.mass_flow_kg_s();
            // Cold inlet (positive = entering), Cold outlet (negative = leaving)
            flows.push(entropyk_core::MassFlow::from_kg_per_s(m_cold));
            flows.push(entropyk_core::MassFlow::from_kg_per_s(-m_cold));
        }

        Ok(flows)
    }

    fn port_enthalpies(
        &self,
        state: &StateSlice,
    ) -> Result<Vec<entropyk_core::Enthalpy>, ComponentError> {
        let mut enthalpies = Vec::with_capacity(4);

        // This matches the order in port_mass_flows
        if let Some(hot_cond) = &self.hot_conditions {
            let h_in = self.query_enthalpy(hot_cond).unwrap_or(400_000.0);
            enthalpies.push(entropyk_core::Enthalpy::from_joules_per_kg(h_in));
            // HACK: As mentioned in compute_residuals, proper port mappings are pending.
            // We use a dummy 5 K delta for the outlet until full Port system integration.
            let cp = self.query_cp(hot_cond).unwrap_or(1000.0);
            enthalpies.push(entropyk_core::Enthalpy::from_joules_per_kg(h_in - cp * 5.0));
        }

        if let Some(cold_cond) = &self.cold_conditions {
            let h_in = self.query_enthalpy(cold_cond).unwrap_or(80_000.0);
            enthalpies.push(entropyk_core::Enthalpy::from_joules_per_kg(h_in));
            let cp = self.query_cp(cold_cond).unwrap_or(4180.0);
            enthalpies.push(entropyk_core::Enthalpy::from_joules_per_kg(h_in + cp * 5.0));
        }

        Ok(enthalpies)
    }

    fn energy_transfers(
        &self,
        _state: &StateSlice,
    ) -> Option<(entropyk_core::Power, entropyk_core::Power)> {
        match self.operational_state {
            OperationalState::Off | OperationalState::Bypass | OperationalState::On => {
                // Internal heat exchange between tracked streams; adiabatic to macro-environment
                Some((
                    entropyk_core::Power::from_watts(0.0),
                    entropyk_core::Power::from_watts(0.0),
                ))
            }
        }
    }
}

impl<Model: HeatTransferModel + 'static> StateManageable for HeatExchanger<Model> {
    fn state(&self) -> OperationalState {
        self.operational_state
    }

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

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

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

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

#[cfg(test)]
mod tests {
    use super::*;
    use crate::heat_exchanger::{FlowConfiguration, LmtdModel};
    use crate::state_machine::StateManageable;

    #[test]
    fn test_heat_exchanger_creation() {
        let model = LmtdModel::new(5000.0, FlowConfiguration::CounterFlow);
        let hx = HeatExchanger::new(model, "TestHX");

        assert_eq!(hx.name(), "TestHX");
        assert_eq!(hx.ua(), 5000.0);
        assert_eq!(hx.operational_state(), OperationalState::On);
    }

    #[test]
    fn test_n_equations() {
        let model = LmtdModel::counter_flow(1000.0);
        let hx = HeatExchanger::new(model, "Test");

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

    #[test]
    fn test_compute_residuals() {
        let model = LmtdModel::counter_flow(5000.0);
        let hx = HeatExchanger::new(model, "Test");

        let state = vec![0.0; 10];
        let mut residuals = vec![0.0; 3];

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

    #[test]
    fn test_residual_dimension_error() {
        let model = LmtdModel::counter_flow(5000.0);
        let hx = HeatExchanger::new(model, "Test");

        let state = vec![0.0; 10];
        let mut residuals = vec![0.0; 1];

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

    #[test]
    fn test_builder() {
        let model = LmtdModel::counter_flow(5000.0);
        let hx = HeatExchangerBuilder::new(model)
            .name("Condenser")
            .circuit_id(CircuitId::from_number(5))
            .build();

        assert_eq!(hx.name(), "Condenser");
        assert_eq!(hx.circuit_id().as_number(), 5);
    }

    #[test]
    fn test_state_manageable_state() {
        let model = LmtdModel::counter_flow(5000.0);
        let hx = HeatExchanger::new(model, "Test");

        assert_eq!(hx.state(), OperationalState::On);
    }

    #[test]
    fn test_state_manageable_set_state() {
        let model = LmtdModel::counter_flow(5000.0);
        let mut hx = HeatExchanger::new(model, "Test");

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

    #[test]
    fn test_state_manageable_can_transition_to() {
        let model = LmtdModel::counter_flow(5000.0);
        let hx = HeatExchanger::new(model, "Test");

        assert!(hx.can_transition_to(OperationalState::Off));
        assert!(hx.can_transition_to(OperationalState::Bypass));
    }

    #[test]
    fn test_state_manageable_circuit_id() {
        let model = LmtdModel::counter_flow(5000.0);
        let hx = HeatExchanger::new(model, "Test");

        assert_eq!(*hx.circuit_id(), CircuitId::ZERO);
    }

    #[test]
    fn test_state_manageable_set_circuit_id() {
        let model = LmtdModel::counter_flow(5000.0);
        let mut hx = HeatExchanger::new(model, "Test");

        hx.set_circuit_id(CircuitId::from_number(2));
        assert_eq!(hx.circuit_id().as_number(), 2);
    }

    #[test]
    fn test_off_mode_residuals() {
        let model = LmtdModel::counter_flow(5000.0);
        let mut hx = HeatExchanger::new(model, "Test");
        hx.set_operational_state(OperationalState::Off);

        let state = vec![0.0; 10];
        let mut residuals = vec![0.0; 3];

        let result = hx.compute_residuals(&state, &mut residuals);
        assert!(result.is_ok());

        // In OFF mode, all residuals should be zero
        assert_eq!(residuals[0], 0.0);
        assert_eq!(residuals[1], 0.0);
        assert_eq!(residuals[2], 0.0);
    }

    #[test]
    fn test_bypass_mode_residuals() {
        let model = LmtdModel::counter_flow(5000.0);
        let mut hx = HeatExchanger::new(model, "Test");
        hx.set_operational_state(OperationalState::Bypass);

        let state = vec![0.0; 10];
        let mut residuals = vec![0.0; 3];

        let result = hx.compute_residuals(&state, &mut residuals);
        assert!(result.is_ok());

        // In BYPASS mode, all residuals should be zero (no heat transfer)
        assert_eq!(residuals[0], 0.0);
        assert_eq!(residuals[1], 0.0);
        assert_eq!(residuals[2], 0.0);
    }

    #[test]
    fn test_circuit_id_via_builder() {
        let model = LmtdModel::counter_flow(5000.0);
        let hx = HeatExchangerBuilder::new(model)
            .circuit_id(CircuitId::from_number(1))
            .build();

        assert_eq!(hx.circuit_id().as_number(), 1);
    }

    #[test]
    fn test_with_circuit_id() {
        let model = LmtdModel::counter_flow(5000.0);
        let hx = HeatExchanger::new(model, "Test").with_circuit_id(CircuitId::from_number(3));

        assert_eq!(hx.circuit_id().as_number(), 3);
    }

    // ===== Story 5.1: FluidBackend Integration Tests =====

    #[test]
    fn test_no_fluid_backend_by_default() {
        let model = LmtdModel::counter_flow(5000.0);
        let hx = HeatExchanger::new(model, "Test");
        assert!(!hx.has_fluid_backend());
    }

    #[test]
    fn test_with_fluid_backend_sets_flag() {
        use entropyk_fluids::TestBackend;
        use std::sync::Arc;

        let model = LmtdModel::counter_flow(5000.0);
        let hx = HeatExchanger::new(model, "Test").with_fluid_backend(Arc::new(TestBackend::new()));

        assert!(hx.has_fluid_backend());
    }

    #[test]
    fn test_hx_side_conditions_construction() {
        use entropyk_core::{MassFlow, Pressure, Temperature};

        let conds = HxSideConditions::new(
            Temperature::from_celsius(60.0),
            Pressure::from_bar(25.0),
            MassFlow::from_kg_per_s(0.05),
            "R410A",
        )
        .expect("Valid conditions should not fail");

        assert!((conds.temperature_k() - 333.15).abs() < 0.01);
        assert!((conds.pressure_pa() - 25.0e5).abs() < 1.0);
        assert!((conds.mass_flow_kg_s() - 0.05).abs() < 1e-10);
        assert_eq!(conds.fluid_id().0, "R410A");
    }

    #[test]
    fn test_compute_residuals_with_backend_succeeds() {
        /// Using TestBackend: Water on cold side, R410A on hot side.
        use entropyk_core::{MassFlow, Pressure, Temperature};
        use entropyk_fluids::TestBackend;
        use std::sync::Arc;

        let model = LmtdModel::counter_flow(5000.0);
        let hx = HeatExchanger::new(model, "Condenser")
            .with_fluid_backend(Arc::new(TestBackend::new()))
            .with_hot_conditions(
                HxSideConditions::new(
                    Temperature::from_celsius(60.0),
                    Pressure::from_bar(20.0),
                    MassFlow::from_kg_per_s(0.05),
                    "R410A",
                )
                .expect("Valid hot conditions"),
            )
            .with_cold_conditions(
                HxSideConditions::new(
                    Temperature::from_celsius(30.0),
                    Pressure::from_pascals(102_000.0),
                    MassFlow::from_kg_per_s(0.2),
                    "Water",
                )
                .expect("Valid cold conditions"),
            );

        let state = vec![0.0f64; 10];
        let mut residuals = vec![0.0f64; 3];
        let result = hx.compute_residuals(&state, &mut residuals);
        assert!(
            result.is_ok(),
            "compute_residuals with FluidBackend should succeed"
        );
    }

    #[test]
    fn test_residuals_with_backend_vs_without_differ() {
        /// Residuals computed with a real backend should differ from placeholder residuals
        /// because real Cp and enthalpy values are used.
        use entropyk_core::{MassFlow, Pressure, Temperature};
        use entropyk_fluids::TestBackend;
        use std::sync::Arc;

        // Without backend (placeholder values)
        let model1 = LmtdModel::counter_flow(5000.0);
        let hx_no_backend = HeatExchanger::new(model1, "HX_nobackend");

        let state = vec![0.0f64; 10];
        let mut residuals_no_backend = vec![0.0f64; 3];
        hx_no_backend
            .compute_residuals(&state, &mut residuals_no_backend)
            .unwrap();

        // With backend (real Water + R410A properties)
        let model2 = LmtdModel::counter_flow(5000.0);
        let hx_with_backend = HeatExchanger::new(model2, "HX_with_backend")
            .with_fluid_backend(Arc::new(TestBackend::new()))
            .with_hot_conditions(
                HxSideConditions::new(
                    Temperature::from_celsius(60.0),
                    Pressure::from_bar(20.0),
                    MassFlow::from_kg_per_s(0.05),
                    "R410A",
                )
                .expect("Valid hot conditions"),
            )
            .with_cold_conditions(
                HxSideConditions::new(
                    Temperature::from_celsius(30.0),
                    Pressure::from_pascals(102_000.0),
                    MassFlow::from_kg_per_s(0.2),
                    "Water",
                )
                .expect("Valid cold conditions"),
            );

        let mut residuals_with_backend = vec![0.0f64; 3];
        hx_with_backend
            .compute_residuals(&state, &mut residuals_with_backend)
            .unwrap();

        // The energy balance residual (index 2) should differ because real Cp differs
        // from the 1000.0/4180.0 hardcoded fallback values.
        // (TestBackend returns Cp=1500 for refrigerants and 4184 for water,
        //  but temperatures and flows differ, so the residual WILL differ)
        let residuals_are_different = residuals_no_backend
            .iter()
            .zip(residuals_with_backend.iter())
            .any(|(a, b)| (a - b).abs() > 1e-6);
        assert!(
            residuals_are_different,
            "Residuals with FluidBackend should differ from placeholder residuals"
        );
    }

    #[test]
    fn test_set_fluid_backend_mutable() {
        use entropyk_fluids::TestBackend;
        use std::sync::Arc;

        let model = LmtdModel::counter_flow(5000.0);
        let mut hx = HeatExchanger::new(model, "Test");

        assert!(!hx.has_fluid_backend());
        hx.set_fluid_backend(Arc::new(TestBackend::new()));
        assert!(hx.has_fluid_backend());
    }
}
