//! Expansion Valve Component Implementation
//!
//! This module provides an expansion valve component that models isenthalpic
//! expansion in refrigeration systems. The expansion valve reduces pressure
//! while maintaining constant enthalpy (throttling process).
//!
//! ## Thermodynamic Model
//!
//! The expansion valve is modeled as an isenthalpic device:
//! ```text
//! h_out = h_in  (enthalpy conservation - isenthalpic)
//! ṁ_out = ṁ_in  (mass flow continuity)
//! P_out < P_in  (pressure drop - throttling)
//! W = 0         (no work done)
//! Q = 0         (adiabatic)
//! ```
//!
//! ## Operational States
//!
//! - **On**: Normal expansion with isenthalpic process
//! - **Off**: Zero mass flow through the valve
//! - **Bypass**: Acts as adiabatic pipe (P_in = P_out, h_in = h_out)
//!
//! ## Example
//!
//! ```rust
//! use entropyk_components::expansion_valve::ExpansionValve;
//! use entropyk_components::port::{FluidId, Port};
//! use entropyk_core::{Pressure, Enthalpy};
//!
//! // Create disconnected ports
//! let inlet = Port::new(
//!     FluidId::new("R134a"),
//!     Pressure::from_bar(10.0),
//!     Enthalpy::from_joules_per_kg(250000.0)
//! );
//! let outlet = Port::new(
//!     FluidId::new("R134a"),
//!     Pressure::from_bar(10.0),
//!     Enthalpy::from_joules_per_kg(250000.0)
//! );
//!
//! // Create expansion valve
//! let valve = ExpansionValve::new(inlet, outlet, None).unwrap();
//! ```

use crate::port::{Connected, Disconnected, FluidId, Port};
use crate::valve_flow::{valve_mass_flow, ValveFlowInput, ValveFlowModel};
use crate::{
    CircuitId, Component, ComponentError, ConnectedPort, JacobianBuilder, OperationalState,
    ResidualVector, StateSlice,
};
use entropyk_core::Calib;
use std::marker::PhantomData;

const OPENING_THRESHOLD: f64 = 0.01;

const ENTHALPY_TOLERANCE_J_KG: f64 = 100.0;

const MIN_STATE_DIMENSIONS: usize = 2;

fn is_effectively_off_impl(operational_state: OperationalState, opening: Option<f64>) -> bool {
    operational_state == OperationalState::Off || opening.is_some_and(|o| o < OPENING_THRESHOLD)
}

/// Expansion valve component for modeling isenthalpic expansion.
///
/// The expansion valve is a throttling device that reduces pressure while
/// maintaining constant enthalpy (isenthalpic process). It implements the
/// [`Component`] trait for integration with the solver.
///
/// # Type Parameters
///
/// * `State` - Either `Disconnected` or `Connected`, tracking connection state
///
/// # Example
///
/// ```rust
/// use entropyk_components::expansion_valve::ExpansionValve;
/// use entropyk_components::port::{FluidId, Port};
/// use entropyk_core::{Pressure, Enthalpy};
///
/// // Create disconnected ports
/// let inlet = Port::new(
///     FluidId::new("R134a"),
///     Pressure::from_bar(10.0),
///     Enthalpy::from_joules_per_kg(250000.0)
/// );
/// let outlet = Port::new(
///     FluidId::new("R134a"),
///     Pressure::from_bar(10.0),
///     Enthalpy::from_joules_per_kg(250000.0)
/// );
///
/// // Create expansion valve with optional opening parameter
/// let valve = ExpansionValve::new(inlet, outlet, Some(1.0)).unwrap();
/// ```
#[derive(Debug, Clone, PartialEq)]
pub struct ExpansionValve<State> {
    port_inlet: Port<State>,
    port_outlet: Port<State>,
    /// Calibration: ṁ_eff = f_m × ṁ_nominal (mass flow scaling)
    calib: Calib,
    /// Calibration indices to extract factors dynamically from SystemState
    pub calib_indices: entropyk_core::CalibIndices,
    operational_state: OperationalState,
    opening: Option<f64>,
    /// Physical mass-flow capacity model (orifice / EXV / TXV Eames).
    flow_model: ValveFlowModel,
    /// TXV bulb pressure [Pa] (used when `flow_model` is [`ValveFlowModel::TxvEames`]).
    bulb_pressure_pa: f64,
    fluid_id: FluidId,
    circuit_id: CircuitId,
    _state: PhantomData<State>,
}

impl ExpansionValve<Disconnected> {
    /// Creates a new disconnected expansion valve.
    ///
    /// # Arguments
    ///
    /// * `port_inlet` - Inlet port (high pressure, subcooled liquid)
    /// * `port_outlet` - Outlet port (low pressure, two-phase)
    /// * `opening` - Optional opening parameter (0.0 = closed, 1.0 = fully open)
    ///
    /// # Errors
    ///
    /// Returns an error if:
    /// - Opening is outside [0.0, 1.0] range
    /// - Opening is NaN or infinite
    /// - Ports have different fluid types
    pub fn new(
        port_inlet: Port<Disconnected>,
        port_outlet: Port<Disconnected>,
        opening: Option<f64>,
    ) -> Result<Self, ComponentError> {
        if let Some(o) = opening {
            if !(0.0..=1.0).contains(&o) {
                return Err(ComponentError::InvalidState(format!(
                    "Opening must be between 0.0 and 1.0, got {}",
                    o
                )));
            }
            if o.is_nan() || o.is_infinite() {
                return Err(ComponentError::InvalidState(
                    "Opening must be a finite number".to_string(),
                ));
            }
        }

        if port_inlet.fluid_id() != port_outlet.fluid_id() {
            return Err(ComponentError::InvalidState(
                "Inlet and outlet ports must have the same fluid type".to_string(),
            ));
        }

        let fluid_id = port_inlet.fluid_id().clone();

        Ok(Self {
            port_inlet,
            port_outlet,
            calib: Calib::default(),
            calib_indices: entropyk_core::CalibIndices::default(),
            operational_state: OperationalState::default(),
            opening,
            flow_model: ValveFlowModel::default(),
            bulb_pressure_pa: 0.0,
            fluid_id,
            circuit_id: CircuitId::default(),
            _state: PhantomData,
        })
    }

    /// Sets the physical flow model (orifice / EXV CdA / TXV Eames).
    pub fn with_flow_model(mut self, model: ValveFlowModel) -> Self {
        self.flow_model = model;
        self
    }

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

    /// Returns the inlet port.
    pub fn port_inlet(&self) -> &Port<Disconnected> {
        &self.port_inlet
    }

    /// Returns the outlet port.
    pub fn port_outlet(&self) -> &Port<Disconnected> {
        &self.port_outlet
    }

    /// Returns the optional opening parameter (0.0 to 1.0).
    pub fn opening(&self) -> Option<f64> {
        self.opening
    }

    /// 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_m for mass flow scaling).
    pub fn calib(&self) -> &Calib {
        &self.calib
    }

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

    /// Returns true if the valve is effectively off.
    ///
    /// The valve is effectively off when:
    /// - Operational state is Off, or
    /// - Opening is below threshold (< 1%)
    pub fn is_effectively_off(&self) -> bool {
        is_effectively_off_impl(self.operational_state, self.opening)
    }
    /// Connects the expansion valve to inlet and outlet ports.
    ///
    /// This consumes the disconnected valve and returns a connected one,
    /// transitioning the state at compile time.
    pub fn connect(
        self,
        inlet: Port<Disconnected>,
        outlet: Port<Disconnected>,
    ) -> Result<ExpansionValve<Connected>, ComponentError> {
        let (p_in, _) = self
            .port_inlet
            .connect(inlet)
            .map_err(|e| ComponentError::InvalidState(e.to_string()))?;
        let (p_out, _) = self
            .port_outlet
            .connect(outlet)
            .map_err(|e| ComponentError::InvalidState(e.to_string()))?;

        Ok(ExpansionValve {
            port_inlet: p_in,
            port_outlet: p_out,
            calib: self.calib,
            calib_indices: self.calib_indices,
            operational_state: self.operational_state,
            opening: self.opening,
            flow_model: self.flow_model,
            bulb_pressure_pa: self.bulb_pressure_pa,
            fluid_id: self.fluid_id,
            circuit_id: self.circuit_id,
            _state: PhantomData,
        })
    }
}

/// Phase region at a thermodynamic state point.
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum PhaseRegion {
    /// Subcooled liquid (below saturation line)
    Subcooled,
    /// Two-phase mixture (between saturated liquid and vapor)
    TwoPhase,
    /// Superheated vapor (above saturation line)
    Superheated,
}

impl PhaseRegion {
    /// Returns true if the region is two-phase.
    pub fn is_two_phase(self) -> bool {
        self == PhaseRegion::TwoPhase
    }
}

impl ExpansionValve<Connected> {
    /// Returns the inlet port.
    pub fn port_inlet(&self) -> &Port<Connected> {
        &self.port_inlet
    }

    /// Returns the outlet port.
    pub fn port_outlet(&self) -> &Port<Connected> {
        &self.port_outlet
    }

    /// Computes the full thermodynamic state at the inlet port.
    pub fn inlet_state(
        &self,
        backend: &impl entropyk_fluids::FluidBackend,
    ) -> Result<entropyk_fluids::ThermoState, ComponentError> {
        backend
            .full_state(
                entropyk_fluids::FluidId::new(self.port_inlet.fluid_id().as_str()),
                self.port_inlet.pressure(),
                self.port_inlet.enthalpy(),
            )
            .map_err(|e| {
                ComponentError::CalculationFailed(format!("Failed to compute inlet state: {}", e))
            })
    }

    /// Computes the full thermodynamic state at the outlet port.
    pub fn outlet_state(
        &self,
        backend: &impl entropyk_fluids::FluidBackend,
    ) -> Result<entropyk_fluids::ThermoState, ComponentError> {
        backend
            .full_state(
                entropyk_fluids::FluidId::new(self.port_outlet.fluid_id().as_str()),
                self.port_outlet.pressure(),
                self.port_outlet.enthalpy(),
            )
            .map_err(|e| {
                ComponentError::CalculationFailed(format!("Failed to compute outlet state: {}", e))
            })
    }

    /// Returns the optional opening parameter (0.0 to 1.0).
    pub fn opening(&self) -> Option<f64> {
        self.opening
    }

    /// 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 the fluid identifier.
    pub fn fluid_id(&self) -> &FluidId {
        &self.fluid_id
    }

    /// Returns calibration factors (f_m for mass flow scaling).
    pub fn calib(&self) -> &Calib {
        &self.calib
    }

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

    /// Returns true if the valve is effectively off.
    ///
    /// The valve is effectively off when:
    /// - Operational state is Off, or
    /// - Opening is below threshold (< 1%)
    pub fn is_effectively_off(&self) -> bool {
        is_effectively_off_impl(self.operational_state, self.opening)
    }

    /// Sets the valve opening parameter.
    ///
    /// # Arguments
    ///
    /// * `opening` - New opening value (0.0 = closed, 1.0 = fully open), or None
    ///
    /// # Errors
    ///
    /// Returns an error if opening is outside [0.0, 1.0] range or is NaN/infinite.
    pub fn set_opening(&mut self, opening: Option<f64>) -> Result<(), ComponentError> {
        if let Some(o) = opening {
            if !(0.0..=1.0).contains(&o) {
                return Err(ComponentError::InvalidState(format!(
                    "Opening must be between 0.0 and 1.0, got {}",
                    o
                )));
            }
            if o.is_nan() || o.is_infinite() {
                return Err(ComponentError::InvalidState(
                    "Opening must be a finite number".to_string(),
                ));
            }
        }
        self.opening = opening;
        Ok(())
    }

    /// Sets the physical flow model (orifice / EXV CdA / TXV Eames).
    pub fn set_flow_model(&mut self, model: ValveFlowModel) {
        self.flow_model = model;
    }

    /// Sets TXV bulb pressure [Pa].
    pub fn set_bulb_pressure(&mut self, p_pa: f64) {
        self.bulb_pressure_pa = p_pa;
    }

    /// Rated mass-flow capacity from the selected physical flow model [kg/s].
    pub fn capacity_mass_flow(
        &self,
        density_kg_m3: f64,
        p_up_pa: f64,
        p_down_pa: f64,
    ) -> Result<f64, ComponentError> {
        let input = ValveFlowInput {
            density_kg_m3,
            p_upstream_pa: p_up_pa,
            p_downstream_pa: p_down_pa,
            opening: self.opening.unwrap_or(1.0),
            p_bulb_pa: self.bulb_pressure_pa,
        };
        valve_mass_flow(&self.flow_model, &input).map_err(ComponentError::InvalidState)
    }

    /// Returns both ports as an array for solver topology.
    pub fn get_ports_slice(&self) -> [&Port<Connected>; 2] {
        [&self.port_inlet, &self.port_outlet]
    }

    /// Validates that the process is isenthalpic (h_in = h_out).
    ///
    /// # Returns
    ///
    /// Returns `Ok(true)` if inlet and outlet enthalpies are equal within tolerance.
    pub fn validate_isenthalpic(&self) -> Result<bool, ComponentError> {
        let h_in = self.port_inlet.enthalpy().to_joules_per_kg();
        let h_out = self.port_outlet.enthalpy().to_joules_per_kg();

        if h_in.is_nan() || h_out.is_nan() {
            return Err(ComponentError::NumericalError(
                "Enthalpy contains NaN value".to_string(),
            ));
        }

        Ok((h_in - h_out).abs() < ENTHALPY_TOLERANCE_J_KG)
    }

    /// Validates that outlet pressure is lower than inlet pressure.
    ///
    /// # Returns
    ///
    /// Returns `Ok(true)` if P_out < P_in, indicating a pressure drop.
    pub fn validate_pressure_drop(&self) -> Result<bool, ComponentError> {
        let p_in = self.port_inlet.pressure().to_pascals();
        let p_out = self.port_outlet.pressure().to_pascals();

        if p_in <= 0.0 {
            return Err(ComponentError::NumericalError(
                "Inlet pressure must be positive".to_string(),
            ));
        }
        if p_out <= 0.0 {
            return Err(ComponentError::NumericalError(
                "Outlet pressure must be positive".to_string(),
            ));
        }

        Ok(p_out < p_in)
    }

    /// Returns the pressure ratio (P_out / P_in).
    ///
    /// A value less than 1.0 indicates a pressure drop through the valve.
    pub fn pressure_ratio(&self) -> f64 {
        let p_in = self.port_inlet.pressure().to_pascals();
        let p_out = self.port_outlet.pressure().to_pascals();
        if p_in > 0.0 {
            p_out / p_in
        } else {
            0.0
        }
    }

    /// Detects the phase region of the outlet port based on pressure and enthalpy.
    ///
    /// This method determines if the outlet is in subcooled, two-phase, or superheated
    /// region by comparing against saturation enthalpy values at the outlet pressure.
    ///
    /// # Arguments
    ///
    /// * `h_f` - Saturated liquid enthalpy at outlet pressure (J/kg)
    /// * `h_g` - Saturated vapor enthalpy at outlet pressure (J/kg)
    ///
    /// # Returns
    ///
    /// The phase region at the outlet.
    pub fn detect_phase_region(&self, h_f: f64, h_g: f64) -> PhaseRegion {
        let h_out = self.port_outlet.enthalpy().to_joules_per_kg();

        if h_out < h_f {
            PhaseRegion::Subcooled
        } else if h_out > h_g {
            PhaseRegion::Superheated
        } else {
            PhaseRegion::TwoPhase
        }
    }

    /// Calculates the vapor quality at the outlet if in two-phase region.
    ///
    /// Quality is defined as: x = (h - h_f) / (h_g - h_f)
    /// - x = 0: Saturated liquid
    /// - x = 1: Saturated vapor
    /// - 0 < x < 1: Two-phase mixture
    ///
    /// # Arguments
    ///
    /// * `h_f` - Saturated liquid enthalpy at outlet pressure (J/kg)
    /// * `h_g` - Saturated vapor enthalpy at outlet pressure (J/kg)
    ///
    /// # Returns
    ///
    /// Returns `Ok(quality)` if outlet is in two-phase region,
    /// or `Err(ComponentError)` if quality calculation is not applicable.
    pub fn outlet_quality(&self, h_f: f64, h_g: f64) -> Result<f64, ComponentError> {
        let h_out = self.port_outlet.enthalpy().to_joules_per_kg();

        let h_range = h_g - h_f;
        if h_range <= 0.0 {
            return Err(ComponentError::NumericalError(
                "Invalid saturation enthalpy range (h_g must be greater than h_f)".to_string(),
            ));
        }

        if h_out < h_f || h_out > h_g {
            return Err(ComponentError::InvalidState(format!(
                "Outlet is not in two-phase region: h_out={} J/kg, h_f={} J/kg, h_g={} J/kg",
                h_out, h_f, h_g
            )));
        }

        Ok((h_out - h_f) / h_range)
    }

    /// Validates that phase change occurs from inlet to outlet.
    ///
    /// For isenthalpic expansion, the outlet should typically be in two-phase
    /// if the inlet was subcooled liquid.
    ///
    /// # Arguments
    ///
    /// * `h_f_out` - Saturated liquid enthalpy at outlet pressure (J/kg)
    /// * `h_g_out` - Saturated vapor enthalpy at outlet pressure (J/kg)
    ///
    /// # Returns
    ///
    /// Returns `Ok(true)` if phase change occurs from inlet to outlet.
    /// This is detected when the outlet is in two-phase region.
    pub fn validate_phase_change(
        &self,
        h_f_out: f64,
        h_g_out: f64,
    ) -> Result<bool, ComponentError> {
        let _h_in = self.port_inlet.enthalpy().to_joules_per_kg();
        let h_out = self.port_outlet.enthalpy().to_joules_per_kg();

        if h_out >= h_f_out && h_out <= h_g_out {
            return Ok(true);
        }

        Ok(false)
    }
}

impl Component for ExpansionValve<Connected> {
    fn compute_residuals(
        &self,
        state: &StateSlice,
        residuals: &mut ResidualVector,
    ) -> Result<(), ComponentError> {
        if residuals.len() != self.n_equations() {
            return Err(ComponentError::InvalidResidualDimensions {
                expected: self.n_equations(),
                actual: residuals.len(),
            });
        }

        if self.is_effectively_off() {
            if state.is_empty() {
                return Err(ComponentError::InvalidStateDimensions {
                    expected: MIN_STATE_DIMENSIONS,
                    actual: 0,
                });
            }
            residuals[0] = state[0];
            residuals[1] = 0.0;
            return Ok(());
        }

        match self.operational_state {
            OperationalState::Bypass => {
                let p_in = self.port_inlet.pressure().to_pascals();
                let p_out = self.port_outlet.pressure().to_pascals();
                let h_in = self.port_inlet.enthalpy().to_joules_per_kg();
                let h_out = self.port_outlet.enthalpy().to_joules_per_kg();

                residuals[0] = p_out - p_in;
                residuals[1] = h_out - h_in;
                return Ok(());
            }
            OperationalState::On | OperationalState::Off => {}
        }

        if state.len() < MIN_STATE_DIMENSIONS {
            return Err(ComponentError::InvalidStateDimensions {
                expected: MIN_STATE_DIMENSIONS,
                actual: state.len(),
            });
        }

        let h_in = self.port_inlet.enthalpy().to_joules_per_kg();
        let h_out = self.port_outlet.enthalpy().to_joules_per_kg();

        residuals[0] = h_out - h_in;

        // Mass flow: ṁ_out = f_m × ṁ_in (calibration factor on inlet flow)
        let mass_flow_in = state[0];
        let mass_flow_out = state[1];
        let f_m = self
            .calib_indices
            .z_flow
            .map(|idx| state[idx])
            .unwrap_or(self.calib.z_flow);
        residuals[1] = mass_flow_out - f_m * mass_flow_in;

        Ok(())
    }

    fn jacobian_entries(
        &self,
        _state: &StateSlice,
        jacobian: &mut JacobianBuilder,
    ) -> Result<(), ComponentError> {
        if self.is_effectively_off() {
            jacobian.add_entry(0, 0, 1.0);
            jacobian.add_entry(1, 0, 0.0);
            return Ok(());
        }

        match self.operational_state {
            OperationalState::Bypass => {
                jacobian.add_entry(0, 0, 1.0);
                jacobian.add_entry(0, 1, -1.0);
                jacobian.add_entry(1, 0, 1.0);
                jacobian.add_entry(1, 1, -1.0);
                return Ok(());
            }
            OperationalState::On | OperationalState::Off => {}
        }

        let f_m = self
            .calib_indices
            .z_flow
            .map(|idx| _state[idx])
            .unwrap_or(self.calib.z_flow);
        jacobian.add_entry(0, 0, 0.0);
        jacobian.add_entry(0, 1, 0.0);
        jacobian.add_entry(1, 0, -f_m);
        jacobian.add_entry(1, 1, 1.0);

        if let Some(idx) = self.calib_indices.z_flow {
            // d(R2)/d(f_m) = -mass_flow_in
            // We need mass_flow_in here, which is _state[0]
            let mass_flow_in = _state[0];
            jacobian.add_entry(1, idx, -mass_flow_in);
        }

        Ok(())
    }

    fn n_equations(&self) -> usize {
        2
    }

    fn port_mass_flows(
        &self,
        state: &StateSlice,
    ) -> Result<Vec<entropyk_core::MassFlow>, ComponentError> {
        if state.len() < MIN_STATE_DIMENSIONS {
            return Err(ComponentError::InvalidStateDimensions {
                expected: MIN_STATE_DIMENSIONS,
                actual: state.len(),
            });
        }
        let m_in = entropyk_core::MassFlow::from_kg_per_s(state[0]);
        let m_out = entropyk_core::MassFlow::from_kg_per_s(-state[1]); // Negative because it's leaving
        Ok(vec![m_in, m_out])
    }

    /// Returns the enthalpies at the inlet and outlet ports.
    ///
    /// For an expansion valve (isenthalpic device), the inlet and outlet
    /// enthalpies should be equal: h_in ≈ h_out.
    ///
    /// # Returns
    ///
    /// A vector containing `[h_inlet, h_outlet]` in order.
    fn port_enthalpies(
        &self,
        _state: &StateSlice,
    ) -> Result<Vec<entropyk_core::Enthalpy>, ComponentError> {
        Ok(vec![
            self.port_inlet.enthalpy(),
            self.port_outlet.enthalpy(),
        ])
    }

    /// Returns the energy transfers for the expansion valve.
    ///
    /// An expansion valve is an isenthalpic throttling device:
    /// - **Heat (Q)**: 0 W (adiabatic - no heat exchange with environment)
    /// - **Work (W)**: 0 W (no moving parts - no mechanical work)
    ///
    /// # Returns
    ///
    /// `Some((Q=0, W=0))` always, since expansion valves are passive devices.
    fn energy_transfers(
        &self,
        _state: &StateSlice,
    ) -> Option<(entropyk_core::Power, entropyk_core::Power)> {
        match self.operational_state {
            OperationalState::Off | OperationalState::Bypass | OperationalState::On => Some((
                entropyk_core::Power::from_watts(0.0),
                entropyk_core::Power::from_watts(0.0),
            )),
        }
    }

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

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

    fn signature(&self) -> String {
        format!(
            "ExpansionValve(fluid={}, circuit={})",
            self.fluid_id.as_str(),
            self.circuit_id.0
        )
    }

    fn to_params(&self) -> crate::ComponentParams {
        crate::ComponentParams::new("ExpansionValve")
            .with_param("fluid", self.fluid_id.as_str())
            .with_param("circuitId", self.circuit_id.0)
            .with_param("opening", self.opening)
            .with_param(
                "calib",
                serde_json::to_value(&self.calib).unwrap_or(serde_json::Value::Null),
            )
    }

    fn update_calib_factor(&mut self, factor: &str, value: f64) -> bool {
        let mut c = self.calib().clone();
        if c.set_factor(factor, value) {
            self.set_calib(c);
            true
        } else {
            false
        }
    }
}

use crate::state_machine::StateManageable;

impl StateManageable for ExpansionValve<Connected> {
    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 approx::assert_relative_eq;
    use entropyk_core::{Enthalpy, Pressure};

    fn create_test_valve() -> ExpansionValve<Connected> {
        let inlet = Port::new(
            FluidId::new("R134a"),
            Pressure::from_bar(10.0),
            Enthalpy::from_joules_per_kg(250000.0),
        );
        let outlet = Port::new(
            FluidId::new("R134a"),
            Pressure::from_bar(10.0),
            Enthalpy::from_joules_per_kg(250000.0),
        );
        let (inlet_conn, mut outlet_conn) = inlet.connect(outlet).unwrap();

        outlet_conn.set_pressure(Pressure::from_bar(3.5));

        ExpansionValve {
            calib_indices: entropyk_core::CalibIndices::default(),
            port_inlet: inlet_conn,
            port_outlet: outlet_conn,
            calib: Calib::default(),
            operational_state: OperationalState::On,
            opening: Some(1.0),
            flow_model: ValveFlowModel::default(),
            bulb_pressure_pa: 0.0,
            fluid_id: FluidId::new("R134a"),
            circuit_id: CircuitId::default(),
            _state: PhantomData,
        }
    }

    fn create_disconnected_valve() -> ExpansionValve<Disconnected> {
        let inlet = Port::new(
            FluidId::new("R134a"),
            Pressure::from_bar(10.0),
            Enthalpy::from_joules_per_kg(250000.0),
        );
        let outlet = Port::new(
            FluidId::new("R134a"),
            Pressure::from_bar(10.0),
            Enthalpy::from_joules_per_kg(250000.0),
        );
        ExpansionValve::new(inlet, outlet, Some(1.0)).unwrap()
    }

    #[test]
    fn test_valve_creation() {
        let valve = create_disconnected_valve();
        assert_eq!(valve.fluid_id().as_str(), "R134a");
        assert_eq!(valve.opening(), Some(1.0));
        assert_eq!(valve.operational_state(), OperationalState::On);
    }

    #[test]
    fn test_valve_creation_without_opening() {
        let inlet = Port::new(
            FluidId::new("R134a"),
            Pressure::from_bar(10.0),
            Enthalpy::from_joules_per_kg(250000.0),
        );
        let outlet = Port::new(
            FluidId::new("R134a"),
            Pressure::from_bar(10.0),
            Enthalpy::from_joules_per_kg(250000.0),
        );
        let valve = ExpansionValve::new(inlet, outlet, None).unwrap();
        assert_eq!(valve.opening(), None);
    }

    #[test]
    fn test_valve_creation_invalid_opening_high() {
        let inlet = Port::new(
            FluidId::new("R134a"),
            Pressure::from_bar(10.0),
            Enthalpy::from_joules_per_kg(250000.0),
        );
        let outlet = Port::new(
            FluidId::new("R134a"),
            Pressure::from_bar(10.0),
            Enthalpy::from_joules_per_kg(250000.0),
        );
        let result = ExpansionValve::new(inlet, outlet, Some(1.5));
        assert!(result.is_err());
    }

    #[test]
    fn test_valve_creation_invalid_opening_low() {
        let inlet = Port::new(
            FluidId::new("R134a"),
            Pressure::from_bar(10.0),
            Enthalpy::from_joules_per_kg(250000.0),
        );
        let outlet = Port::new(
            FluidId::new("R134a"),
            Pressure::from_bar(10.0),
            Enthalpy::from_joules_per_kg(250000.0),
        );
        let result = ExpansionValve::new(inlet, outlet, Some(-0.1));
        assert!(result.is_err());
    }

    #[test]
    fn test_valve_creation_nan_opening() {
        let inlet = Port::new(
            FluidId::new("R134a"),
            Pressure::from_bar(10.0),
            Enthalpy::from_joules_per_kg(250000.0),
        );
        let outlet = Port::new(
            FluidId::new("R134a"),
            Pressure::from_bar(10.0),
            Enthalpy::from_joules_per_kg(250000.0),
        );
        let result = ExpansionValve::new(inlet, outlet, Some(f64::NAN));
        assert!(result.is_err());
    }

    #[test]
    fn test_valve_creation_incompatible_fluids() {
        let inlet = Port::new(
            FluidId::new("R134a"),
            Pressure::from_bar(10.0),
            Enthalpy::from_joules_per_kg(250000.0),
        );
        let outlet = Port::new(
            FluidId::new("R410A"),
            Pressure::from_bar(10.0),
            Enthalpy::from_joules_per_kg(250000.0),
        );
        let result = ExpansionValve::new(inlet, outlet, Some(1.0));
        assert!(result.is_err());
    }

    #[test]
    fn test_isenthalpic_expansion() {
        let valve = create_test_valve();
        assert_relative_eq!(
            valve.port_inlet().enthalpy().to_joules_per_kg(),
            valve.port_outlet().enthalpy().to_joules_per_kg(),
            epsilon = 1e-10
        );
    }

    #[test]
    fn test_validate_isenthalpic() {
        let valve = create_test_valve();
        let result = valve.validate_isenthalpic();
        assert!(result.is_ok());
        assert!(result.unwrap());
    }

    #[test]
    fn test_pressure_drop() {
        let valve = create_test_valve();
        let p_in = valve.port_inlet().pressure().to_bar();
        let p_out = valve.port_outlet().pressure().to_bar();
        assert!(p_out < p_in, "Outlet pressure should be less than inlet");
    }

    #[test]
    fn test_validate_pressure_drop() {
        let valve = create_test_valve();
        let result = valve.validate_pressure_drop();
        assert!(result.is_ok());
        assert!(result.unwrap());
    }

    #[test]
    fn test_pressure_ratio() {
        let valve = create_test_valve();
        let ratio = valve.pressure_ratio();
        assert_relative_eq!(ratio, 0.35, epsilon = 1e-10);
    }

    #[test]
    fn test_off_mode() {
        let mut valve = create_test_valve();
        valve.set_operational_state(OperationalState::Off);

        let state = vec![0.05, 0.05];
        let mut residuals = vec![0.0; 2];

        valve.compute_residuals(&state, &mut residuals).unwrap();

        assert_eq!(valve.operational_state(), OperationalState::Off);
        assert!(valve.is_effectively_off());
    }

    #[test]
    fn test_bypass_mode() {
        let inlet = Port::new(
            FluidId::new("R134a"),
            Pressure::from_bar(10.0),
            Enthalpy::from_joules_per_kg(250000.0),
        );
        let outlet = Port::new(
            FluidId::new("R134a"),
            Pressure::from_bar(10.0),
            Enthalpy::from_joules_per_kg(250000.0),
        );
        let (inlet_conn, outlet_conn) = inlet.connect(outlet).unwrap();

        let valve = ExpansionValve {
            calib_indices: entropyk_core::CalibIndices::default(),
            port_inlet: inlet_conn,
            port_outlet: outlet_conn,
            calib: Calib::default(),
            operational_state: OperationalState::Bypass,
            opening: Some(1.0),
            flow_model: ValveFlowModel::default(),
            bulb_pressure_pa: 0.0,
            fluid_id: FluidId::new("R134a"),
            circuit_id: CircuitId::default(),
            _state: PhantomData,
        };

        let state = vec![0.05, 0.05];
        let mut residuals = vec![0.0; 2];

        valve.compute_residuals(&state, &mut residuals).unwrap();

        assert_eq!(valve.operational_state(), OperationalState::Bypass);
        assert_relative_eq!(residuals[0], 0.0, epsilon = 1e-10);
        assert_relative_eq!(residuals[1], 0.0, epsilon = 1e-10);
    }

    #[test]
    fn test_opening_threshold_off() {
        let inlet = Port::new(
            FluidId::new("R134a"),
            Pressure::from_bar(10.0),
            Enthalpy::from_joules_per_kg(250000.0),
        );
        let outlet = Port::new(
            FluidId::new("R134a"),
            Pressure::from_bar(10.0),
            Enthalpy::from_joules_per_kg(250000.0),
        );
        let (inlet_conn, mut outlet_conn) = inlet.connect(outlet).unwrap();
        outlet_conn.set_pressure(Pressure::from_bar(3.5));

        let valve = ExpansionValve {
            calib_indices: entropyk_core::CalibIndices::default(),
            port_inlet: inlet_conn,
            port_outlet: outlet_conn,
            calib: Calib::default(),
            operational_state: OperationalState::On,
            opening: Some(0.005),
            flow_model: ValveFlowModel::default(),
            bulb_pressure_pa: 0.0,
            fluid_id: FluidId::new("R134a"),
            circuit_id: CircuitId::default(),
            _state: PhantomData,
        };

        assert!(valve.is_effectively_off());
    }

    #[test]
    fn test_opening_threshold_on() {
        let inlet = Port::new(
            FluidId::new("R134a"),
            Pressure::from_bar(10.0),
            Enthalpy::from_joules_per_kg(250000.0),
        );
        let outlet = Port::new(
            FluidId::new("R134a"),
            Pressure::from_bar(10.0),
            Enthalpy::from_joules_per_kg(250000.0),
        );
        let (inlet_conn, mut outlet_conn) = inlet.connect(outlet).unwrap();
        outlet_conn.set_pressure(Pressure::from_bar(3.5));

        let valve = ExpansionValve {
            calib_indices: entropyk_core::CalibIndices::default(),
            port_inlet: inlet_conn,
            port_outlet: outlet_conn,
            calib: Calib::default(),
            operational_state: OperationalState::On,
            opening: Some(0.5),
            flow_model: ValveFlowModel::default(),
            bulb_pressure_pa: 0.0,
            fluid_id: FluidId::new("R134a"),
            circuit_id: CircuitId::default(),
            _state: PhantomData,
        };

        assert!(!valve.is_effectively_off());
    }

    #[test]
    fn test_component_n_equations() {
        let valve = create_test_valve();
        assert_eq!(valve.n_equations(), 2);
    }

    #[test]
    fn test_component_compute_residuals() {
        let valve = create_test_valve();
        let state = vec![0.05, 0.05];
        let mut residuals = vec![0.0; 2];

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

        assert_relative_eq!(residuals[0], 0.0, epsilon = 1e-10);
        assert_relative_eq!(residuals[1], 0.0, epsilon = 1e-10);
    }

    #[test]
    fn test_component_compute_residuals_wrong_size() {
        let valve = create_test_valve();
        let state = vec![0.05, 0.05];
        let mut residuals = vec![0.0; 3];

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

    #[test]
    fn test_component_jacobian_entries() {
        let valve = create_test_valve();
        let state = vec![0.05, 0.05];
        let mut jacobian = JacobianBuilder::new();

        let result = valve.jacobian_entries(&state, &mut jacobian);
        assert!(result.is_ok());
        assert!(!jacobian.is_empty());
    }

    #[test]
    fn test_circuit_id() {
        let mut valve = create_disconnected_valve();
        valve.set_circuit_id(CircuitId::from_number(5));
        assert_eq!(valve.circuit_id().as_number(), 5);
    }

    #[test]
    fn test_get_ports_slice() {
        let valve = create_test_valve();
        let ports = valve.get_ports_slice();
        assert_eq!(ports.len(), 2);
        assert_eq!(ports[0].fluid_id().as_str(), "R134a");
        assert_eq!(ports[1].fluid_id().as_str(), "R134a");
    }

    #[test]
    fn test_clone() {
        let valve = create_test_valve();
        let cloned = valve.clone();
        assert_eq!(valve.opening(), cloned.opening());
        assert_eq!(valve.operational_state(), cloned.operational_state());
    }

    #[test]
    fn test_mass_flow_continuity_residual() {
        let valve = create_test_valve();
        let state = vec![0.05, 0.06];
        let mut residuals = vec![0.0; 2];

        valve.compute_residuals(&state, &mut residuals).unwrap();

        assert_relative_eq!(residuals[1], 0.01, epsilon = 1e-10);
    }

    #[test]
    fn test_set_opening_valid() {
        let mut valve = create_test_valve();
        assert!(valve.set_opening(Some(0.5)).is_ok());
        assert_eq!(valve.opening(), Some(0.5));
    }

    #[test]
    fn test_set_opening_invalid_high() {
        let mut valve = create_test_valve();
        assert!(valve.set_opening(Some(1.5)).is_err());
    }

    #[test]
    fn test_set_opening_invalid_low() {
        let mut valve = create_test_valve();
        assert!(valve.set_opening(Some(-0.1)).is_err());
    }

    #[test]
    fn test_set_opening_nan() {
        let mut valve = create_test_valve();
        assert!(valve.set_opening(Some(f64::NAN)).is_err());
    }

    #[test]
    fn test_set_opening_none() {
        let mut valve = create_test_valve();
        assert!(valve.set_opening(None).is_ok());
        assert_eq!(valve.opening(), None);
    }

    #[test]
    fn test_on_mode_empty_state_error() {
        let valve = create_test_valve();
        let state: Vec<f64> = vec![];
        let mut residuals = vec![0.0; 2];

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

    #[test]
    fn test_off_mode_empty_state_error() {
        let mut valve = create_test_valve();
        valve.set_operational_state(OperationalState::Off);

        let state: Vec<f64> = vec![];
        let mut residuals = vec![0.0; 2];

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

    #[test]
    fn test_pressure_ratio_zero_inlet() {
        let inlet = Port::new(
            FluidId::new("R134a"),
            Pressure::from_pascals(0.0),
            Enthalpy::from_joules_per_kg(250000.0),
        );
        let outlet = Port::new(
            FluidId::new("R134a"),
            Pressure::from_pascals(0.0),
            Enthalpy::from_joules_per_kg(250000.0),
        );
        let (inlet_conn, mut outlet_conn) = inlet.connect(outlet).unwrap();
        outlet_conn.set_pressure(Pressure::from_pascals(0.0));

        let valve = ExpansionValve {
            calib_indices: entropyk_core::CalibIndices::default(),
            port_inlet: inlet_conn,
            port_outlet: outlet_conn,
            calib: Calib::default(),
            operational_state: OperationalState::On,
            opening: Some(1.0),
            flow_model: ValveFlowModel::default(),
            bulb_pressure_pa: 0.0,
            fluid_id: FluidId::new("R134a"),
            circuit_id: CircuitId::default(),
            _state: PhantomData,
        };

        assert_relative_eq!(valve.pressure_ratio(), 0.0, epsilon = 1e-10);
    }

    #[test]
    fn test_validate_isenthalpic_with_tolerance() {
        let inlet = Port::new(
            FluidId::new("R134a"),
            Pressure::from_bar(10.0),
            Enthalpy::from_joules_per_kg(250000.0),
        );
        let outlet = Port::new(
            FluidId::new("R134a"),
            Pressure::from_bar(10.0),
            Enthalpy::from_joules_per_kg(250050.0),
        );
        let (inlet_conn, outlet_conn) = inlet.connect(outlet).unwrap();

        let valve = ExpansionValve {
            calib_indices: entropyk_core::CalibIndices::default(),
            port_inlet: inlet_conn,
            port_outlet: outlet_conn,
            calib: Calib::default(),
            operational_state: OperationalState::On,
            opening: Some(1.0),
            flow_model: ValveFlowModel::default(),
            bulb_pressure_pa: 0.0,
            fluid_id: FluidId::new("R134a"),
            circuit_id: CircuitId::default(),
            _state: PhantomData,
        };

        let result = valve.validate_isenthalpic();
        assert!(result.is_ok());
        assert!(result.unwrap());
    }

    #[test]
    fn test_bypass_mode_jacobian() {
        let inlet = Port::new(
            FluidId::new("R134a"),
            Pressure::from_bar(10.0),
            Enthalpy::from_joules_per_kg(250000.0),
        );
        let outlet = Port::new(
            FluidId::new("R134a"),
            Pressure::from_bar(10.0),
            Enthalpy::from_joules_per_kg(250000.0),
        );
        let (inlet_conn, outlet_conn) = inlet.connect(outlet).unwrap();

        let valve = ExpansionValve {
            calib_indices: entropyk_core::CalibIndices::default(),
            port_inlet: inlet_conn,
            port_outlet: outlet_conn,
            calib: Calib::default(),
            operational_state: OperationalState::Bypass,
            opening: Some(1.0),
            flow_model: ValveFlowModel::default(),
            bulb_pressure_pa: 0.0,
            fluid_id: FluidId::new("R134a"),
            circuit_id: CircuitId::default(),
            _state: PhantomData,
        };

        let state = vec![0.05, 0.05];
        let mut jacobian = JacobianBuilder::new();

        valve.jacobian_entries(&state, &mut jacobian).unwrap();

        let entries = jacobian.entries();
        assert!(entries.len() >= 4);

        let has_nonzero = entries.iter().any(|(_, _, v)| *v != 0.0);
        assert!(has_nonzero, "Bypass jacobian should have non-zero entries");
    }

    #[test]
    fn test_state_manageable_state() {
        let valve = create_test_valve();
        assert_eq!(valve.state(), OperationalState::On);
    }

    #[test]
    fn test_state_manageable_set_state_on_to_off() {
        let mut valve = create_test_valve();
        let result = valve.set_state(OperationalState::Off);
        assert!(result.is_ok());
        assert_eq!(valve.state(), OperationalState::Off);
    }

    #[test]
    fn test_state_manageable_set_state_on_to_bypass() {
        let mut valve = create_test_valve();
        let result = valve.set_state(OperationalState::Bypass);
        assert!(result.is_ok());
        assert_eq!(valve.state(), OperationalState::Bypass);
    }

    #[test]
    fn test_state_manageable_can_transition_to() {
        let valve = create_test_valve();
        assert!(valve.can_transition_to(OperationalState::Off));
        assert!(valve.can_transition_to(OperationalState::Bypass));
        assert!(valve.can_transition_to(OperationalState::On));
    }

    #[test]
    fn test_state_manageable_circuit_id() {
        let valve = create_test_valve();
        assert_eq!(*valve.circuit_id(), CircuitId::ZERO);
    }

    #[test]
    fn test_state_manageable_set_circuit_id() {
        let mut valve = create_test_valve();
        valve.set_circuit_id(CircuitId::from_number(2));
        assert_eq!(valve.circuit_id().as_number(), 2);
    }

    #[test]
    fn test_state_transition_cycle() {
        let mut valve = create_test_valve();

        // On -> Off
        valve.set_state(OperationalState::Off).unwrap();
        assert_eq!(valve.state(), OperationalState::Off);

        // Off -> Bypass
        valve.set_state(OperationalState::Bypass).unwrap();
        assert_eq!(valve.state(), OperationalState::Bypass);

        // Bypass -> On
        valve.set_state(OperationalState::On).unwrap();
        assert_eq!(valve.state(), OperationalState::On);
    }

    #[test]
    fn test_detect_phase_region_subcooled() {
        let inlet = Port::new(
            FluidId::new("R134a"),
            Pressure::from_bar(10.0),
            Enthalpy::from_joules_per_kg(200000.0),
        );
        let outlet = Port::new(
            FluidId::new("R134a"),
            Pressure::from_bar(10.0),
            Enthalpy::from_joules_per_kg(200000.0),
        );
        let (inlet_conn, mut outlet_conn) = inlet.connect(outlet).unwrap();
        outlet_conn.set_pressure(Pressure::from_bar(3.5));
        outlet_conn.set_enthalpy(Enthalpy::from_joules_per_kg(180000.0));

        let valve = ExpansionValve {
            calib_indices: entropyk_core::CalibIndices::default(),
            port_inlet: inlet_conn,
            port_outlet: outlet_conn,
            calib: Calib::default(),
            operational_state: OperationalState::On,
            opening: Some(1.0),
            flow_model: ValveFlowModel::default(),
            bulb_pressure_pa: 0.0,
            fluid_id: FluidId::new("R134a"),
            circuit_id: CircuitId::default(),
            _state: PhantomData,
        };

        let h_f = 200000.0;
        let h_g = 400000.0;
        let region = valve.detect_phase_region(h_f, h_g);
        assert_eq!(region, PhaseRegion::Subcooled);
    }

    #[test]
    fn test_detect_phase_region_two_phase() {
        let inlet = Port::new(
            FluidId::new("R134a"),
            Pressure::from_bar(10.0),
            Enthalpy::from_joules_per_kg(250000.0),
        );
        let outlet = Port::new(
            FluidId::new("R134a"),
            Pressure::from_bar(10.0),
            Enthalpy::from_joules_per_kg(250000.0),
        );
        let (inlet_conn, mut outlet_conn) = inlet.connect(outlet).unwrap();
        outlet_conn.set_pressure(Pressure::from_bar(3.5));

        let valve = ExpansionValve {
            calib_indices: entropyk_core::CalibIndices::default(),
            port_inlet: inlet_conn,
            port_outlet: outlet_conn,
            calib: Calib::default(),
            operational_state: OperationalState::On,
            opening: Some(1.0),
            flow_model: ValveFlowModel::default(),
            bulb_pressure_pa: 0.0,
            fluid_id: FluidId::new("R134a"),
            circuit_id: CircuitId::default(),
            _state: PhantomData,
        };

        let h_f = 200000.0;
        let h_g = 400000.0;
        let region = valve.detect_phase_region(h_f, h_g);
        assert_eq!(region, PhaseRegion::TwoPhase);
        assert!(region.is_two_phase());
    }

    #[test]
    fn test_detect_phase_region_superheated() {
        let inlet = Port::new(
            FluidId::new("R134a"),
            Pressure::from_bar(10.0),
            Enthalpy::from_joules_per_kg(450000.0),
        );
        let outlet = Port::new(
            FluidId::new("R134a"),
            Pressure::from_bar(10.0),
            Enthalpy::from_joules_per_kg(450000.0),
        );
        let (inlet_conn, mut outlet_conn) = inlet.connect(outlet).unwrap();
        outlet_conn.set_pressure(Pressure::from_bar(3.5));

        let valve = ExpansionValve {
            calib_indices: entropyk_core::CalibIndices::default(),
            port_inlet: inlet_conn,
            port_outlet: outlet_conn,
            calib: Calib::default(),
            operational_state: OperationalState::On,
            opening: Some(1.0),
            flow_model: ValveFlowModel::default(),
            bulb_pressure_pa: 0.0,
            fluid_id: FluidId::new("R134a"),
            circuit_id: CircuitId::default(),
            _state: PhantomData,
        };

        let h_f = 200000.0;
        let h_g = 400000.0;
        let region = valve.detect_phase_region(h_f, h_g);
        assert_eq!(region, PhaseRegion::Superheated);
    }

    #[test]
    fn test_outlet_quality_valid() {
        let inlet = Port::new(
            FluidId::new("R134a"),
            Pressure::from_bar(10.0),
            Enthalpy::from_joules_per_kg(250000.0),
        );
        let outlet = Port::new(
            FluidId::new("R134a"),
            Pressure::from_bar(10.0),
            Enthalpy::from_joules_per_kg(250000.0),
        );
        let (inlet_conn, mut outlet_conn) = inlet.connect(outlet).unwrap();
        outlet_conn.set_pressure(Pressure::from_bar(3.5));

        let valve = ExpansionValve {
            calib_indices: entropyk_core::CalibIndices::default(),
            port_inlet: inlet_conn,
            port_outlet: outlet_conn,
            calib: Calib::default(),
            operational_state: OperationalState::On,
            opening: Some(1.0),
            flow_model: ValveFlowModel::default(),
            bulb_pressure_pa: 0.0,
            fluid_id: FluidId::new("R134a"),
            circuit_id: CircuitId::default(),
            _state: PhantomData,
        };

        let h_f = 200000.0;
        let h_g = 400000.0;
        let quality = valve.outlet_quality(h_f, h_g).unwrap();
        assert_relative_eq!(quality, 0.25, epsilon = 1e-10);
    }

    #[test]
    fn test_outlet_quality_saturated_liquid() {
        let inlet = Port::new(
            FluidId::new("R134a"),
            Pressure::from_bar(10.0),
            Enthalpy::from_joules_per_kg(200000.0),
        );
        let outlet = Port::new(
            FluidId::new("R134a"),
            Pressure::from_bar(10.0),
            Enthalpy::from_joules_per_kg(200000.0),
        );
        let (inlet_conn, mut outlet_conn) = inlet.connect(outlet).unwrap();
        outlet_conn.set_pressure(Pressure::from_bar(3.5));

        let valve = ExpansionValve {
            calib_indices: entropyk_core::CalibIndices::default(),
            port_inlet: inlet_conn,
            port_outlet: outlet_conn,
            calib: Calib::default(),
            operational_state: OperationalState::On,
            opening: Some(1.0),
            flow_model: ValveFlowModel::default(),
            bulb_pressure_pa: 0.0,
            fluid_id: FluidId::new("R134a"),
            circuit_id: CircuitId::default(),
            _state: PhantomData,
        };

        let h_f = 200000.0;
        let h_g = 400000.0;
        let quality = valve.outlet_quality(h_f, h_g).unwrap();
        assert_relative_eq!(quality, 0.0, epsilon = 1e-10);
    }

    #[test]
    fn test_outlet_quality_invalid_not_two_phase() {
        let inlet = Port::new(
            FluidId::new("R134a"),
            Pressure::from_bar(10.0),
            Enthalpy::from_joules_per_kg(450000.0),
        );
        let outlet = Port::new(
            FluidId::new("R134a"),
            Pressure::from_bar(10.0),
            Enthalpy::from_joules_per_kg(450000.0),
        );
        let (inlet_conn, mut outlet_conn) = inlet.connect(outlet).unwrap();
        outlet_conn.set_pressure(Pressure::from_bar(3.5));

        let valve = ExpansionValve {
            calib_indices: entropyk_core::CalibIndices::default(),
            port_inlet: inlet_conn,
            port_outlet: outlet_conn,
            calib: Calib::default(),
            operational_state: OperationalState::On,
            opening: Some(1.0),
            flow_model: ValveFlowModel::default(),
            bulb_pressure_pa: 0.0,
            fluid_id: FluidId::new("R134a"),
            circuit_id: CircuitId::default(),
            _state: PhantomData,
        };

        let h_f = 200000.0;
        let h_g = 400000.0;
        let result = valve.outlet_quality(h_f, h_g);
        assert!(result.is_err());
    }

    #[test]
    fn test_validate_phase_change_detected() {
        let inlet = Port::new(
            FluidId::new("R134a"),
            Pressure::from_bar(10.0),
            Enthalpy::from_joules_per_kg(250000.0),
        );
        let outlet = Port::new(
            FluidId::new("R134a"),
            Pressure::from_bar(10.0),
            Enthalpy::from_joules_per_kg(250000.0),
        );
        let (inlet_conn, mut outlet_conn) = inlet.connect(outlet).unwrap();
        outlet_conn.set_pressure(Pressure::from_bar(3.5));

        let valve = ExpansionValve {
            calib_indices: entropyk_core::CalibIndices::default(),
            port_inlet: inlet_conn,
            port_outlet: outlet_conn,
            calib: Calib::default(),
            operational_state: OperationalState::On,
            opening: Some(1.0),
            flow_model: ValveFlowModel::default(),
            bulb_pressure_pa: 0.0,
            fluid_id: FluidId::new("R134a"),
            circuit_id: CircuitId::default(),
            _state: PhantomData,
        };

        let h_f_out = 180000.0;
        let h_g_out = 380000.0;
        let result = valve.validate_phase_change(h_f_out, h_g_out).unwrap();
        assert!(result);
    }

    #[test]
    fn test_phase_region_enum() {
        assert!(!PhaseRegion::Subcooled.is_two_phase());
        assert!(PhaseRegion::TwoPhase.is_two_phase());
        assert!(!PhaseRegion::Superheated.is_two_phase());
    }

    #[test]
    fn test_energy_transfers_zero() {
        let valve = create_test_valve();
        let state = vec![0.05, 0.05];

        let (heat, work) = valve.energy_transfers(&state).unwrap();

        assert_relative_eq!(heat.to_watts(), 0.0, epsilon = 1e-10);
        assert_relative_eq!(work.to_watts(), 0.0, epsilon = 1e-10);
    }

    #[test]
    fn test_energy_transfers_off_mode() {
        let mut valve = create_test_valve();
        valve.set_operational_state(OperationalState::Off);
        let state = vec![0.05, 0.05];

        let (heat, work) = valve.energy_transfers(&state).unwrap();

        assert_relative_eq!(heat.to_watts(), 0.0, epsilon = 1e-10);
        assert_relative_eq!(work.to_watts(), 0.0, epsilon = 1e-10);
    }

    #[test]
    fn test_energy_transfers_bypass_mode() {
        let inlet = Port::new(
            FluidId::new("R134a"),
            Pressure::from_bar(10.0),
            Enthalpy::from_joules_per_kg(250000.0),
        );
        let outlet = Port::new(
            FluidId::new("R134a"),
            Pressure::from_bar(10.0),
            Enthalpy::from_joules_per_kg(250000.0),
        );
        let (inlet_conn, outlet_conn) = inlet.connect(outlet).unwrap();

        let valve = ExpansionValve {
            calib_indices: entropyk_core::CalibIndices::default(),
            port_inlet: inlet_conn,
            port_outlet: outlet_conn,
            calib: Calib::default(),
            operational_state: OperationalState::Bypass,
            opening: Some(1.0),
            flow_model: ValveFlowModel::default(),
            bulb_pressure_pa: 0.0,
            fluid_id: FluidId::new("R134a"),
            circuit_id: CircuitId::default(),
            _state: PhantomData,
        };

        let state = vec![0.05, 0.05];
        let (heat, work) = valve.energy_transfers(&state).unwrap();

        assert_relative_eq!(heat.to_watts(), 0.0, epsilon = 1e-10);
        assert_relative_eq!(work.to_watts(), 0.0, epsilon = 1e-10);
    }

    #[test]
    fn test_port_enthalpies_returns_two_values() {
        let valve = create_test_valve();
        let state = vec![0.05, 0.05];

        let enthalpies = valve.port_enthalpies(&state).unwrap();

        assert_eq!(enthalpies.len(), 2);
    }

    #[test]
    fn test_port_enthalpies_isenthalpic() {
        let valve = create_test_valve();
        let state = vec![0.05, 0.05];

        let enthalpies = valve.port_enthalpies(&state).unwrap();

        assert_relative_eq!(
            enthalpies[0].to_joules_per_kg(),
            enthalpies[1].to_joules_per_kg(),
            epsilon = 1e-10
        );
    }

    #[test]
    fn test_port_enthalpies_inlet_value() {
        let inlet = Port::new(
            FluidId::new("R134a"),
            Pressure::from_bar(10.0),
            Enthalpy::from_joules_per_kg(300000.0),
        );
        let outlet = Port::new(
            FluidId::new("R134a"),
            Pressure::from_bar(10.0),
            Enthalpy::from_joules_per_kg(300000.0),
        );
        let (inlet_conn, mut outlet_conn) = inlet.connect(outlet).unwrap();
        outlet_conn.set_pressure(Pressure::from_bar(3.5));

        let valve = ExpansionValve {
            calib_indices: entropyk_core::CalibIndices::default(),
            port_inlet: inlet_conn,
            port_outlet: outlet_conn,
            calib: Calib::default(),
            operational_state: OperationalState::On,
            opening: Some(1.0),
            flow_model: ValveFlowModel::default(),
            bulb_pressure_pa: 0.0,
            fluid_id: FluidId::new("R134a"),
            circuit_id: CircuitId::default(),
            _state: PhantomData,
        };

        let state = vec![0.05, 0.05];
        let enthalpies = valve.port_enthalpies(&state).unwrap();

        assert_relative_eq!(enthalpies[0].to_joules_per_kg(), 300000.0, epsilon = 1e-10);
        assert_relative_eq!(enthalpies[1].to_joules_per_kg(), 300000.0, epsilon = 1e-10);
    }

    #[test]
    fn test_expansion_valve_energy_balance() {
        let valve = create_test_valve();
        let state = vec![0.05, 0.05];

        let energy = valve.energy_transfers(&state);
        let mass_flows = valve.port_mass_flows(&state);
        let enthalpies = valve.port_enthalpies(&state);

        assert!(energy.is_some());
        assert!(mass_flows.is_ok());
        assert!(enthalpies.is_ok());

        let (heat, work) = energy.unwrap();
        let m_flows = mass_flows.unwrap();
        let h_flows = enthalpies.unwrap();

        assert_eq!(m_flows.len(), h_flows.len());

        assert_relative_eq!(heat.to_watts(), 0.0, epsilon = 1e-10);
        assert_relative_eq!(work.to_watts(), 0.0, epsilon = 1e-10);
    }
}
