//! IsenthalpicExpansionValve Component
//!
//! A thermostatic/electronic expansion valve model for vapor-compression cycles.
//! Implements the isenthalpic throttling process: the refrigerant expands from
//! condensing pressure to evaporating pressure at constant enthalpy.
//!
//! ## Model
//!
//! Given:
//! - Inlet state (P_in, H_in) from the high-pressure side (condenser outlet)
//! - Target evaporating temperature `t_evap_k` for P_evap_sat computation
//!
//! Residuals (2 equations, constrain the EXV outlet edge):
//!
//! - r0 = P_out_state - P_evap_sat(T_evap)   [drive outlet P to evaporating pressure]
//! - r1 = H_out_state - H_in_state            [isenthalpic throttling: H_out = H_in]
//!
//! ## Jacobian
//!
//! - ∂r0/∂P_out = 1
//! - ∂r1/∂H_out = 1
//! - ∂r1/∂H_in  = -1

use crate::state_machine::{CircuitId, OperationalState, StateManageable};
use crate::{
    Component, ComponentError, ConnectedPort, JacobianBuilder, ResidualVector, StateSlice,
};
use entropyk_core::smoothing::{
    smooth_clamp, smooth_clamp_derivative, smooth_max, smooth_max_derivative,
};
use entropyk_core::{CalibIndices, Enthalpy, Pressure};
use entropyk_fluids::{FluidBackend, FluidId, FluidState, Property};
use std::sync::Arc;

/// C¹ opening transition width on `[0, 1]` (Story 0.2 / 0.3 guidance).
const OPENING_WIDTH: f64 = 1e-2;

/// Softness `k` [Pa] for `dp_eff = smooth_max(ΔP, 0, k)` (replaces hard `DP_FLOOR`).
const DP_SMOOTH_K: f64 = 1e3;

/// Isenthalpic expansion valve (EXV / thermostatic valve).
///
/// Constrains the outlet edge state to:
/// - Evaporating saturation pressure
/// - Same enthalpy as the inlet (isenthalpic throttling)
#[derive(Clone)]
pub struct IsenthalpicExpansionValve {
    /// Target evaporating temperature [K] — used to compute P_evap_sat
    t_evap_k: f64,
    /// Refrigerant identifier (e.g. "R410A")
    refrigerant_id: String,
    /// CoolProp (or other) fluid property backend
    fluid_backend: Option<Arc<dyn FluidBackend>>,

    // State indices (set by set_system_context)
    inlet_h_idx: Option<usize>,
    /// Inlet (high-side) pressure state index — needed for the orifice ΔP.
    inlet_p_idx: Option<usize>,
    outlet_p_idx: Option<usize>,
    outlet_h_idx: Option<usize>,
    /// Mass-flow state index of the inlet edge (ṁ unknown, CM1.3).
    inlet_m_idx: Option<usize>,
    /// Mass-flow state index of the outlet edge (ṁ unknown, CM1.3).
    outlet_m_idx: Option<usize>,
    /// True when inlet and outlet share the same ṁ state index (CM1.4).
    same_branch_m: bool,
    /// When `true`, the evaporating pressure is NOT imposed here. The valve only
    /// enforces isenthalpic throttling; the low-side pressure is set by the
    /// downstream evaporator outlet closure (emergent-pressure mode).
    emergent_pressure: bool,
    /// Physical orifice flow coefficient `Kv` [m²] (effective max flow area).
    /// When set (orifice mode, emergent-only) the valve emits an extra
    /// mass-flow residual `ṁ − Kv·opening·√(2·ρ_in·max(ΔP,0))`. The fractional
    /// opening is either a free-actuator unknown (arch-6) or a fixed parameter
    /// via [`fixed_opening`].
    orifice_kv: Option<f64>,
    /// Fixed orifice opening in [0, 1]. When `Some`, the orifice residual uses
    /// this value and no free-actuator unknown is required (pair with dropping
    /// the compressor displacement ṁ closure so DoF stays square).
    fixed_opening: Option<f64>,
    /// Solver-provided calibration/actuator indices. The generic `actuator`
    /// slot carries the orifice opening state index in free-opening orifice mode.
    calib_indices: CalibIndices,
    /// Operational state (On / Off / Bypass). When `Off`, the valve behaves as
    /// a closed shut-off: the orifice residual becomes `ṁ = 0` (no flow) and
    /// the isenthalpic closure is preserved, so a whole branch can be disabled
    /// in a multi-circuit machine without unbalancing the system DoF.
    operational_state: OperationalState,
    /// Circuit identifier (multi-circuit machines).
    circuit_id: CircuitId,
}

impl std::fmt::Debug for IsenthalpicExpansionValve {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_struct("IsenthalpicExpansionValve")
            .field("t_evap_k", &self.t_evap_k)
            .field("refrigerant_id", &self.refrigerant_id)
            .field("inlet_h_idx", &self.inlet_h_idx)
            .field("outlet_p_idx", &self.outlet_p_idx)
            .field("outlet_h_idx", &self.outlet_h_idx)
            .finish()
    }
}

impl IsenthalpicExpansionValve {
    /// Create a new EXV for the given evaporating temperature.
    pub fn new(t_evap_k: f64) -> Self {
        Self {
            t_evap_k,
            refrigerant_id: String::new(),
            fluid_backend: None,
            inlet_h_idx: None,
            outlet_p_idx: None,
            outlet_h_idx: None,
            inlet_m_idx: None,
            outlet_m_idx: None,
            same_branch_m: false,
            emergent_pressure: false,
            inlet_p_idx: None,
            orifice_kv: None,
            fixed_opening: None,
            calib_indices: CalibIndices::default(),
            operational_state: OperationalState::default(),
            circuit_id: CircuitId::default(),
        }
    }

    /// Set the refrigerant identifier (e.g. "R410A").
    pub fn with_refrigerant(mut self, id: &str) -> Self {
        self.refrigerant_id = id.to_string();
        self
    }

    /// Enables emergent-pressure mode: the valve stops pinning the outlet to
    /// `P_sat(t_evap_k)` and only enforces isenthalpic throttling (`H_out = H_in`).
    /// The evaporating pressure then emerges from the downstream evaporator
    /// outlet closure. Reduces the equation count by one (the dropped P-fix).
    pub fn with_emergent_pressure(mut self) -> Self {
        self.emergent_pressure = true;
        self
    }

    /// Attach a fluid property backend.
    pub fn with_fluid_backend(mut self, backend: Arc<dyn FluidBackend>) -> Self {
        self.fluid_backend = Some(backend);
        self
    }

    /// Enables the physical orifice-flow model with a **free** opening unknown.
    ///
    /// `kv` is the effective maximum orifice flow area `Kv` [m²] such that the
    /// mass flow obeys `ṁ = Kv·opening·√(2·ρ_in·max(P_in − P_out, 0))`, where
    /// `opening ∈ [0, 1]` is a free-actuator solver unknown. Emergent-only.
    pub fn with_orifice(mut self, kv: f64) -> Self {
        self.orifice_kv = Some(kv.max(0.0));
        self.fixed_opening = None;
        self.emergent_pressure = true;
        self
    }

    /// Enables the orifice-flow model with a **fixed** opening parameter.
    ///
    /// Same residual as [`with_orifice`], but `opening` is a user parameter (not
    /// a solver unknown). Pair with skipping the compressor displacement ṁ
    /// closure so the system stays square — ṁ then follows the valve.
    pub fn with_orifice_fixed(mut self, kv: f64, opening: f64) -> Self {
        self.orifice_kv = Some(kv.max(0.0));
        self.fixed_opening = Some(opening.clamp(0.0, 1.0));
        self.emergent_pressure = true;
        self
    }

    /// Returns the configured orifice flow coefficient `Kv` [m²], if any.
    pub fn orifice_kv(&self) -> Option<f64> {
        self.orifice_kv
    }

    /// Returns the fixed orifice opening when configured.
    pub fn fixed_opening(&self) -> Option<f64> {
        self.fixed_opening
    }

    /// True when opening is a free actuator (orifice without fixed opening).
    pub fn orifice_opening_is_free(&self) -> bool {
        self.orifice_configured() && self.fixed_opening.is_none()
    }

    /// True when the physical orifice residual is configured (Kv set + emergent).
    fn orifice_configured(&self) -> bool {
        self.emergent_pressure && self.orifice_kv.is_some()
    }

    /// True when the orifice residual can actually be evaluated.
    fn orifice_ready(&self) -> bool {
        let opening_ok = self.fixed_opening.is_some() || self.calib_indices.actuator.is_some();
        self.orifice_configured()
            && self.fluid_backend.is_some()
            && !self.refrigerant_id.is_empty()
            && self.inlet_p_idx.is_some()
            && self.outlet_p_idx.is_some()
            && self.inlet_h_idx.is_some()
            && opening_ok
            && (self.outlet_m_idx.is_some() || self.inlet_m_idx.is_some())
    }

    /// Inlet-density [kg/m³] from (P_in, h_in) via the fluid backend.
    fn inlet_density(&self, p_in_pa: f64, h_in_jkg: f64) -> Result<f64, ComponentError> {
        let backend = self
            .fluid_backend
            .as_ref()
            .ok_or_else(|| ComponentError::CalculationFailed("no fluid backend".into()))?;
        backend
            .property(
                FluidId::new(&self.refrigerant_id),
                Property::Density,
                FluidState::PressureEnthalpy(
                    Pressure::from_pascals(p_in_pa),
                    Enthalpy::from_joules_per_kg(h_in_jkg),
                ),
            )
            .map_err(|e| ComponentError::from_fluid_error_context("rho_in", e))
    }

    /// Raw orifice opening (fixed parameter or free actuator state).
    fn orifice_opening_raw(&self, state: &StateSlice) -> Option<f64> {
        self.fixed_opening
            .or_else(|| self.calib_indices.actuator.map(|i| state[i]))
    }

    /// Evaluates the orifice mass-flow closure
    /// `f = Kv·opening_eff·√(2·ρ_in·dp_eff)` [kg/s] with Story 0.3 regularization:
    /// - `opening_eff = smooth_clamp(opening, 0, 1, OPENING_WIDTH)`
    /// - `dp_eff = smooth_max(ΔP, 0, DP_SMOOTH_K)`
    ///
    /// Returns `(f, rho_in, sqrt_term, delta_p_raw, opening_raw)` for Jacobian reuse.
    /// Residual and Jacobian share this exact expression so FD matches analytic
    /// across ΔP≤0 and opening bounds (FR19).
    fn orifice_flow(&self, state: &StateSlice) -> Option<(f64, f64, f64, f64, f64)> {
        let (kv, p_in_idx, out_p, in_h) = (
            self.orifice_kv?,
            self.inlet_p_idx?,
            self.outlet_p_idx?,
            self.inlet_h_idx?,
        );
        let opening_raw = self.orifice_opening_raw(state)?;
        let opening = smooth_clamp(opening_raw, 0.0, 1.0, OPENING_WIDTH);
        let p_in = state[p_in_idx];
        let p_out = state[out_p];
        let delta_p = p_in - p_out;
        if p_in <= 0.0 {
            return Some((0.0, 0.0, 0.0, delta_p, opening_raw));
        }
        let rho = self.inlet_density(p_in, state[in_h]).ok()?;
        if rho <= 0.0 {
            return Some((0.0, rho, 0.0, delta_p, opening_raw));
        }
        let dp_eff = smooth_max(delta_p, 0.0, DP_SMOOTH_K);
        let sqrt_term = (2.0 * rho * dp_eff).sqrt();
        Some((
            kv * opening * sqrt_term,
            rho,
            sqrt_term,
            delta_p,
            opening_raw,
        ))
    }
}

impl Component for IsenthalpicExpansionValve {
    fn set_system_context(
        &mut self,
        _state_offset: usize,
        external_edge_state_indices: &[(usize, usize, usize)],
    ) {
        // Layout: [0] = incoming edge (cond→exv), [1] = outgoing edge (exv→evap)
        // Triple: (m_idx, p_idx, h_idx)
        if !external_edge_state_indices.is_empty() {
            self.inlet_m_idx = Some(external_edge_state_indices[0].0);
            self.inlet_p_idx = Some(external_edge_state_indices[0].1);
            self.inlet_h_idx = Some(external_edge_state_indices[0].2);
        }
        if external_edge_state_indices.len() >= 2 {
            self.outlet_m_idx = Some(external_edge_state_indices[1].0);
            self.outlet_p_idx = Some(external_edge_state_indices[1].1);
            self.outlet_h_idx = Some(external_edge_state_indices[1].2);
        }
        // CM1.4: detect same-branch topology.
        self.same_branch_m = matches!(
            (self.inlet_m_idx, self.outlet_m_idx),
            (Some(m_in), Some(m_out)) if m_in == m_out
        );
    }

    fn compute_residuals(
        &self,
        state: &StateSlice,
        residuals: &mut ResidualVector,
    ) -> Result<(), ComponentError> {
        // Emergent-pressure mode: only isenthalpic throttling; the low-side
        // pressure is set by the downstream evaporator outlet closure.
        if self.emergent_pressure {
            if let (Some(inlet_h), Some(out_h)) = (self.inlet_h_idx, self.outlet_h_idx) {
                // r0: isenthalpic throttling — outlet enthalpy equals inlet enthalpy.
                // Preserved in all operational states (Off / Bypass / On): the valve
                // never exchanges heat, even when closed.
                residuals[0] = state[out_h] - state[inlet_h];
                // r1: mass conservation (CM1.3), dropped when same_branch_m (CM1.4).
                let mut next = 1;
                if !self.same_branch_m {
                    let (Some(m_in), Some(m_out)) = (self.inlet_m_idx, self.outlet_m_idx) else {
                        return Err(ComponentError::InvalidState(
                            "Expansion valve mass conservation requires live inlet/outlet mass-flow indices".to_string(),
                        ));
                    };
                    residuals[next] = state[m_out] - state[m_in];
                    next += 1;
                }
                // Physical orifice closure (arch-6): ṁ = Kv·opening·√(2·ρ_in·ΔP).
                if self.orifice_configured() {
                    // Off state: the valve is closed — ṁ = 0. The Jacobian
                    // `∂r/∂ṁ = 1` is preserved so Newton keeps its mass-flow
                    // coupling. This lets a whole circuit be disabled in a
                    // multi-circuit machine without unbalancing the DoF.
                    if self.operational_state.is_off() {
                        let Some(m_idx) = self.outlet_m_idx.or(self.inlet_m_idx) else {
                            return Err(ComponentError::InvalidState(
                                "Expansion valve Off closure requires a live mass-flow index"
                                    .to_string(),
                            ));
                        };
                        residuals[next] = state[m_idx]; // ṁ = 0
                    } else {
                        residuals[next] = if self.orifice_ready() {
                            let Some(m_idx) = self.outlet_m_idx.or(self.inlet_m_idx) else {
                                return Err(ComponentError::InvalidState(
                                    "Expansion valve orifice closure requires a live mass-flow index"
                                        .to_string(),
                                ));
                            };
                            let Some((flow, _, _, _, _)) = self.orifice_flow(state) else {
                                return Err(ComponentError::InvalidState(
                                    "Expansion valve orifice closure could not evaluate from live state"
                                        .to_string(),
                                ));
                            };
                            state[m_idx] - flow
                        } else {
                            return Err(ComponentError::InvalidState(
                                "Expansion valve orifice closure requires pressure, enthalpy, and opening context"
                                    .to_string(),
                            ));
                        };
                    }
                }
                return Ok(());
            }
        }

        if let (Some(backend), Some(inlet_h), Some(out_p), Some(out_h)) = (
            &self.fluid_backend,
            self.inlet_h_idx,
            self.outlet_p_idx,
            self.outlet_h_idx,
        ) {
            if !self.refrigerant_id.is_empty() {
                let fluid = FluidId::new(&self.refrigerant_id);

                let p_evap_sat = backend
                    .saturation_pressure_t(fluid, self.t_evap_k)
                    .map_err(ComponentError::from_fluid_error)?;

                // r0: drive outlet pressure to evaporating saturation pressure
                residuals[0] = state[out_p] - p_evap_sat;
                // r1: isenthalpic throttling — outlet enthalpy equals inlet enthalpy
                residuals[1] = state[out_h] - state[inlet_h];
                // r2: mass conservation (CM1.3)
                // CM1.4: skip when same_branch_m — trivially zero.
                if !self.same_branch_m {
                    let (Some(m_in), Some(m_out)) = (self.inlet_m_idx, self.outlet_m_idx) else {
                        return Err(ComponentError::InvalidState(
                            "Expansion valve mass conservation requires live inlet/outlet mass-flow indices".to_string(),
                        ));
                    };
                    residuals[2] = state[m_out] - state[m_in];
                }
                return Ok(());
            }
        }
        Err(ComponentError::InvalidState(
            "Expansion valve requires live state indices and fluid backend context; refusing zero-residual fallback".to_string(),
        ))
    }

    fn jacobian_entries(
        &self,
        _state: &StateSlice,
        jacobian: &mut JacobianBuilder,
    ) -> Result<(), ComponentError> {
        // Emergent-pressure mode: only the isenthalpic residual (+ mass conservation).
        if self.emergent_pressure {
            if let (Some(inlet_h), Some(out_h)) = (self.inlet_h_idx, self.outlet_h_idx) {
                jacobian.add_entry(0, out_h, 1.0);
                jacobian.add_entry(0, inlet_h, -1.0);
                let mut next = 1;
                if !self.same_branch_m {
                    if let (Some(m_in), Some(m_out)) = (self.inlet_m_idx, self.outlet_m_idx) {
                        jacobian.add_entry(1, m_out, 1.0);
                        jacobian.add_entry(1, m_in, -1.0);
                    }
                    next += 1;
                }
                // Orifice residual r = ṁ − Kv·opening_eff·√(2·ρ_in·dp_eff) with
                // opening_eff = smooth_clamp(opening, 0, 1, w) and
                // dp_eff = smooth_max(ΔP, 0, k). Exact analytic partials of that
                // shared residual expression (Story 0.3 / FR19); ρ derivatives via
                // central finite difference (consistent with dT/dP FD elsewhere).
                if self.orifice_configured() {
                    // Off state: orifice is closed, the residual is `ṁ = 0`.
                    // Jacobian is just `∂r/∂ṁ = 1` — pressure/enthalpy couplings
                    // from the orifice equation are dropped because they no
                    // longer apply. `orifice_ready()` is intentionally NOT
                    // required here: the Off closure only needs the mass-flow
                    // index, not the fluid backend or pressure/enthalpy context.
                    if self.operational_state.is_off() {
                        if let Some(m_idx) = self.outlet_m_idx.or(self.inlet_m_idx) {
                            jacobian.add_entry(next, m_idx, 1.0);
                        }
                    } else if self.orifice_ready() {
                        if let (Some(kv), Some(p_in_idx), Some(out_p), Some(in_h)) = (
                            self.orifice_kv,
                            self.inlet_p_idx,
                            self.outlet_p_idx,
                            self.inlet_h_idx,
                        ) {
                            let m_idx = self.outlet_m_idx.or(self.inlet_m_idx);
                            if let (Some(mi), Some((_flow, rho, sqrt_term, delta_p, opening_raw))) =
                                (m_idx, self.orifice_flow(_state))
                            {
                                // ∂r/∂ṁ = 1
                                jacobian.add_entry(next, mi, 1.0);

                                let opening = smooth_clamp(opening_raw, 0.0, 1.0, OPENING_WIDTH);
                                let d_opening =
                                    smooth_clamp_derivative(opening_raw, 0.0, 1.0, OPENING_WIDTH);
                                let dp_eff = smooth_max(delta_p, 0.0, DP_SMOOTH_K);
                                let d_dp_d_pin = smooth_max_derivative(delta_p, 0.0, DP_SMOOTH_K);
                                let p_in = _state[p_in_idx];
                                let h_in = _state[in_h];
                                if sqrt_term > 0.0 && rho > 0.0 && dp_eff > 0.0 {
                                    // Central FD of ρ(P_in,h_in).
                                    let dp = p_in * 1e-6 + 1.0;
                                    let dh = h_in.abs() * 1e-6 + 1.0;
                                    let drho_dp = match (
                                        self.inlet_density(p_in + dp, h_in),
                                        self.inlet_density(p_in - dp, h_in),
                                    ) {
                                        (Ok(a), Ok(b)) => (a - b) / (2.0 * dp),
                                        _ => 0.0,
                                    };
                                    let drho_dh = match (
                                        self.inlet_density(p_in, h_in + dh),
                                        self.inlet_density(p_in, h_in - dh),
                                    ) {
                                        (Ok(a), Ok(b)) => (a - b) / (2.0 * dh),
                                        _ => 0.0,
                                    };
                                    let inv = kv * opening / sqrt_term;
                                    // ∂r/∂opening_raw = −Kv·√(2ρ·dp_eff)·∂opening_eff/∂raw
                                    if let Some(open_idx) = self.calib_indices.actuator {
                                        if self.fixed_opening.is_none() {
                                            jacobian.add_entry(
                                                next,
                                                open_idx,
                                                -kv * sqrt_term * d_opening,
                                            );
                                        }
                                    }
                                    // ∂r/∂P_out = +Kv·opening·ρ/√ · ∂dp_eff/∂P_in
                                    // (chain: ∂dp_eff/∂P_out = −∂dp_eff/∂P_in, then −∂flow)
                                    jacobian.add_entry(next, out_p, inv * rho * d_dp_d_pin);
                                    // ∂r/∂P_in = −Kv·opening·(∂ρ/∂P·dp_eff + ρ·∂dp_eff/∂P_in)/√
                                    jacobian.add_entry(
                                        next,
                                        p_in_idx,
                                        -inv * (drho_dp * dp_eff + rho * d_dp_d_pin),
                                    );
                                    // ∂r/∂h_in = −Kv·opening·(∂ρ/∂h·dp_eff)/√
                                    jacobian.add_entry(next, in_h, -inv * (drho_dh * dp_eff));
                                }
                            }
                        }
                    }
                }
                return Ok(());
            }
        }

        if let (Some(_), Some(inlet_h), Some(out_p), Some(out_h)) = (
            &self.fluid_backend,
            self.inlet_h_idx,
            self.outlet_p_idx,
            self.outlet_h_idx,
        ) {
            if !self.refrigerant_id.is_empty() {
                // ∂r0/∂P_out = 1
                jacobian.add_entry(0, out_p, 1.0);
                // ∂r1/∂H_out = 1, ∂r1/∂H_in = -1
                jacobian.add_entry(1, out_h, 1.0);
                jacobian.add_entry(1, inlet_h, -1.0);
                // r2 = ṁ_outlet − ṁ_inlet  →  exact analytic entries (CM1.3)
                // CM1.4: omit when same_branch_m.
                if !self.same_branch_m {
                    if let (Some(m_in), Some(m_out)) = (self.inlet_m_idx, self.outlet_m_idx) {
                        jacobian.add_entry(2, m_out, 1.0);
                        jacobian.add_entry(2, m_in, -1.0);
                    }
                }
                return Ok(());
            }
        }
        Ok(())
    }

    fn n_equations(&self) -> usize {
        // Emergent mode drops the evaporating-pressure fix: 1 thermo eq (isenthalpic).
        let thermo = if self.emergent_pressure { 1 } else { 2 };
        // CM1.4: drop conservation equation when in same series branch.
        let base = if self.same_branch_m {
            thermo
        } else {
            thermo + 1
        };
        // arch-6: the physical orifice closure adds one equation (balanced by the
        // free-actuator opening unknown). Emergent-only.
        if self.orifice_configured() {
            base + 1
        } else {
            base
        }
    }

    fn equation_roles(&self) -> Vec<crate::EquationRole> {
        let mut roles = Vec::new();
        if !self.emergent_pressure {
            roles.push(crate::EquationRole::BoundaryDirichlet { quantity: "P" });
        }
        roles.push(crate::EquationRole::EnergyBalance {
            stream: "refrigerant",
        });
        if !self.same_branch_m {
            roles.push(crate::EquationRole::MassConservation {
                stream: "refrigerant",
            });
        }
        if self.orifice_configured() {
            roles.push(crate::EquationRole::ActuatorClosure { name: "orifice" });
        }
        roles
    }

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

    fn set_opening_fraction(&mut self, opening: f64) {
        if self.orifice_kv.is_some() {
            self.fixed_opening = Some(opening.clamp(0.0, 1.0));
        }
    }

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

    fn set_fluid_backend_from_builder(&mut self, backend: Arc<dyn FluidBackend>) {
        self.fluid_backend = Some(backend);
    }

    fn signature(&self) -> String {
        format!(
            "IsenthalpicExpansionValve(t_evap_k={:.2}, fluid={})",
            self.t_evap_k, self.refrigerant_id
        )
    }
}

impl StateManageable for IsenthalpicExpansionValve {
    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;
            tracing::info!(
                from = ?from,
                to = ?state,
                "IsenthalpicExpansionValve operational-state transition"
            );
            Ok(())
        } else {
            Err(ComponentError::InvalidState(format!(
                "Illegal operational-state transition: {:?} → {:?}",
                self.operational_state, state
            )))
        }
    }

    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;
    }
}

impl IsenthalpicExpansionValve {
    /// Read-only access to the operational state.
    pub fn operational_state(&self) -> OperationalState {
        self.operational_state
    }

    /// Direct setter (no transition check) for use in config loading and tests
    /// where the state-machine validation would be in the way.
    pub fn set_operational_state_unchecked(&mut self, state: OperationalState) {
        self.operational_state = state;
    }
}

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

    #[test]
    fn test_new_and_defaults() {
        let exv = IsenthalpicExpansionValve::new(275.15);
        assert_eq!(exv.t_evap_k, 275.15);
        // CM1.3: 2 thermo + 1 mass-flow conservation = 3
        assert_eq!(exv.n_equations(), 3);
        assert!(exv.fluid_backend.is_none());
        assert!(exv.inlet_h_idx.is_none());
        assert!(exv.outlet_p_idx.is_none());
        assert!(exv.outlet_h_idx.is_none());
    }

    #[test]
    fn test_set_system_context() {
        let mut exv = IsenthalpicExpansionValve::new(275.15);
        // CM1.3 stride-3: incoming (ṁ@1, P@2, h@3), outgoing (ṁ@4, P@5, h@6)
        exv.set_system_context(0, &[(1, 2, 3), (4, 5, 6)]);
        assert_eq!(exv.inlet_h_idx, Some(3));
        assert_eq!(exv.outlet_p_idx, Some(5));
        assert_eq!(exv.outlet_h_idx, Some(6));
    }

    #[test]
    fn test_missing_context_errors_instead_of_zero_fallback() {
        let exv = IsenthalpicExpansionValve::new(275.15);
        let state = vec![1.0_f64; 10];
        let mut residuals = vec![99.0_f64; 2];
        let result = exv.compute_residuals(&state, &mut residuals);
        assert!(result.is_err());
    }

    #[test]
    fn test_with_refrigerant() {
        let exv = IsenthalpicExpansionValve::new(275.15).with_refrigerant("R410A");
        assert_eq!(exv.refrigerant_id, "R410A");
    }

    #[test]
    fn test_emergent_mode_drops_pressure_fix() {
        let mut exv = IsenthalpicExpansionValve::new(275.15)
            .with_refrigerant("R410A")
            .with_emergent_pressure();
        // Same-branch: inlet (0,1,2), outlet (0,3,4). Only the isenthalpic eq remains.
        exv.set_system_context(0, &[(0, 1, 2), (0, 3, 4)]);
        assert_eq!(exv.n_equations(), 1);

        let mut state = vec![0.0; 5];
        state[2] = 260_000.0; // h_in
        state[4] = 250_000.0; // h_out (≠ h_in)
        let mut r = vec![0.0; 1];
        exv.compute_residuals(&state, &mut r).unwrap();
        // r0 = h_out − h_in, no pressure residual is emitted.
        assert!(
            (r[0] - (-10_000.0)).abs() < 1e-6,
            "isenthalpic residual: {}",
            r[0]
        );
    }

    // ── arch-6 physical orifice actuator ────────────────────────────────────

    fn orifice_valve() -> IsenthalpicExpansionValve {
        use entropyk_fluids::TestBackend;
        let mut exv = IsenthalpicExpansionValve::new(275.15)
            .with_refrigerant("R134a")
            .with_orifice(3.0e-6)
            .with_fluid_backend(Arc::new(TestBackend::new()));
        // Distinct branches: inlet (m0,p1,h2), outlet (m3,p4,h5); opening @ idx 6.
        exv.set_system_context(0, &[(0, 1, 2), (3, 4, 5)]);
        exv.set_calib_indices(CalibIndices {
            actuator: Some(6),
            ..Default::default()
        });
        exv
    }

    /// State layout for `orifice_valve`: subcooled-liquid inlet, opening 0.5.
    fn orifice_state(opening: f64) -> Vec<f64> {
        let mut s = vec![0.0; 7];
        s[0] = 0.2; // m_in
        s[1] = 1.2e6; // p_in
        s[2] = 2.0e5; // h_in (subcooled liquid)
        s[3] = 0.2; // m_out
        s[4] = 3.5e5; // p_out
        s[5] = 2.0e5; // h_out (isenthalpic)
        s[6] = opening; // orifice opening
        s
    }

    #[test]
    fn test_orifice_adds_one_equation() {
        // emergent isenthalpic (1) + mass conservation (1) + orifice (1) = 3.
        let exv = orifice_valve();
        assert_eq!(exv.n_equations(), 3);
        assert!(exv.orifice_ready());
        assert_eq!(exv.orifice_kv(), Some(3.0e-6));
    }

    #[test]
    fn test_orifice_residual_reacts_to_opening() {
        let exv = orifice_valve();
        // Larger opening ⇒ larger orifice flow ⇒ residual (ṁ − flow) decreases.
        let mut r_small = vec![0.0; 3];
        let mut r_large = vec![0.0; 3];
        exv.compute_residuals(&orifice_state(0.3), &mut r_small)
            .unwrap();
        exv.compute_residuals(&orifice_state(0.9), &mut r_large)
            .unwrap();
        assert!(
            r_large[2] < r_small[2],
            "opening 0.9 must pass more flow than 0.3: {} !< {}",
            r_large[2],
            r_small[2]
        );
        // At the physical opening the orifice flow matches ṁ (residual ≈ 0).
        // Compute the opening that closes the residual and confirm sign change.
        let mut r_closed = vec![0.0; 3];
        exv.compute_residuals(&orifice_state(0.05), &mut r_closed)
            .unwrap();
        assert!(r_closed[2] > 0.0, "tiny opening under-passes flow (r>0)");
    }

    fn assert_orifice_jacobian_fd(exv: &IsenthalpicExpansionValve, state: &[f64]) {
        let mut jb = JacobianBuilder::new();
        exv.jacobian_entries(state, &mut jb).unwrap();
        let mut analytic = std::collections::HashMap::new();
        for &(row, col, val) in jb.entries() {
            if row == 2 {
                *analytic.entry(col).or_insert(0.0) += val;
            }
        }

        let residual_row2 = |s: &[f64]| -> f64 {
            let mut r = vec![0.0; 3];
            exv.compute_residuals(s, &mut r).unwrap();
            r[2]
        };

        for &(col, eps) in &[
            (1usize, 50.0), // p_in
            (2usize, 20.0), // h_in
            (3usize, 1e-4), // m_out
            (4usize, 50.0), // p_out
            (6usize, 1e-4), // opening
        ] {
            let mut sp = state.to_vec();
            let mut sm = state.to_vec();
            sp[col] += eps;
            sm[col] -= eps;
            let fd = (residual_row2(&sp) - residual_row2(&sm)) / (2.0 * eps);
            let an = *analytic.get(&col).unwrap_or(&0.0);
            let tol = 1e-4 * an.abs().max(1.0);
            assert!(
                (fd - an).abs() <= tol,
                "col {col}: analytic {an} vs FD {fd} (tol {tol})"
            );
        }
    }

    #[test]
    fn test_orifice_jacobian_matches_finite_difference() {
        let exv = orifice_valve();
        assert_orifice_jacobian_fd(&exv, &orifice_state(0.5));
    }

    #[test]
    fn test_orifice_jacobian_fd_at_opening_bounds() {
        let exv = orifice_valve();
        assert_orifice_jacobian_fd(&exv, &orifice_state(0.0));
        assert_orifice_jacobian_fd(&exv, &orifice_state(1.0));
    }

    #[test]
    fn test_orifice_jacobian_fd_at_nonpositive_dp() {
        let exv = orifice_valve();
        let mut state = orifice_state(0.5);
        state[1] = 3.0e5; // p_in
        state[4] = 4.0e5; // p_out → ΔP < 0
        assert_orifice_jacobian_fd(&exv, &state);
    }

    #[test]
    fn test_orifice_without_actuator_index_errors() {
        use entropyk_fluids::TestBackend;
        let mut exv = IsenthalpicExpansionValve::new(275.15)
            .with_refrigerant("R134a")
            .with_orifice(3.0e-6)
            .with_fluid_backend(Arc::new(TestBackend::new()));
        exv.set_system_context(0, &[(0, 1, 2), (3, 4, 5)]);
        assert!(!exv.orifice_ready());
        assert_eq!(exv.n_equations(), 3);
        let mut r = vec![0.0; 3];
        let result = exv.compute_residuals(&orifice_state(0.5), &mut r);
        assert!(result.is_err());
    }
}
