//! Screw Compressor with Economizer Port
//!
//! Models a twin-screw compressor with an intermediate economizer injection port,
//! as used in industrial air-cooled chillers and heat pumps.
//!
//! ## Physical Description
//!
//! A screw compressor with economizer operates in two internal compression stages
//! within the same machine casing:
//!
//! ```text
//! Stage 1:
//!   Suction (P_evap, h_suc) → compress to P_intermediate
//!
//! Intermediate Injection:
//!   Flash-gas from economizer at (P_eco, h_eco) injects into the rotor lobes
//!   at the intermediate pressure port. This cools the compressed gas and
//!   increases total mass flow delivered to stage 2.
//!
//! Stage 2:
//!   Mixed gas (P_intermediate, h_mix) → compress to P_discharge
//!
//! Net result:
//!   - Higher capacity vs. simple single-stage (~10-20%)
//!   - Better COP (~8-15%) for same condensing/evaporating temperatures
//!   - Extended operating range (higher compression ratios tolerated)
//! ```
//!
//! ## Ports
//!
//! - `port_suction`: Low-pressure refrigerant inlet (from evaporator/drum)
//! - `port_discharge`: High-pressure outlet (to condenser)
//! - `port_economizer`: Intermediate-pressure injection inlet (from economizer flash gas)
//!
//! ## Performance Model
//!
//! Uses the SST/SDT polynomial model (bi-quadratic), where the economizer effect
//! is captured via the intermediate pressure ratio and a user-supplied economizer
//! mass flow fraction curve:
//!
//! ```text
//! ṁ_suction  = z_flow × Σ a_ij × SST^i × SDT^j      (polynomial, manufacturer data)
//! ṁ_eco      = z_flow_eco × x_eco × ṁ_suction,nom   (economizer fraction, 0.05–0.20)
//! ṁ_total    = ṁ_suction + ṁ_eco
//! W_comp     = f_pwr × Σ b_ij × SST^i × SDT^j      (shaft power, manufacturer data)
//!
//! Energy balance (adiabatic compressor):
//!   ṁ_suction × h_suc + ṁ_eco × h_eco + W_comp = ṁ_total × h_discharge
//! ```
//!
//! The intermediate pressure P_eco is typically the geometric mean of suction/discharge:
//!   P_eco = sqrt(P_suc × P_dis)
//!
//! ## Variable Speed Drive (VFD) Integration
//!
//! The `frequency_hz` field (25–60 Hz) scales:
//! - Mass flow  ∝ frequency (volumetric)
//! - Power      ∝ frequency (approximately)
//! - Economizer fraction: unchanged (geometry-driven)
//!
//! ## Usage
//!
//! ```rust,ignore
//! use entropyk_components::screw_economizer_compressor::{
//!     ScrewEconomizerCompressor, ScrewPerformanceCurves
//! };
//! use entropyk_components::polynomials::Polynomial2D;
//!
//! // Manufacturer curves for a ~200 kW screw (R134a, at 50 Hz):
//! let curves = ScrewPerformanceCurves {
//!     mass_flow_curve: Polynomial2D::bilinear(0.8, 0.005, -0.003, 0.00001),
//!     power_curve:     Polynomial2D::bilinear(50_000.0, 300.0, -200.0, 1.0),
//!     eco_flow_fraction_curve: Polynomial2D::bilinear(0.12, 0.001, -0.0005, 0.0),
//! };
//!
//! let compressor = ScrewEconomizerCompressor::new(curves, "R134a", 50.0, 0.92)?;
//! ```

use crate::polynomials::Polynomial2D;
use crate::state_machine::{CircuitId, OperationalState, StateManageable};
use crate::{
    Component, ComponentError, ConnectedPort, JacobianBuilder, ResidualVector, StateSlice,
};
use entropyk_core::{Calib, CalibIndices, Enthalpy, MassFlow, Power};
use serde::{Deserialize, Serialize};

// ─────────────────────────────────────────────────────────────────────────────
// Performance Curves
// ─────────────────────────────────────────────────────────────────────────────

/// Performance curves for a screw compressor with economizer port.
///
/// All curves are functions of (SST, SDT) evaluated in Kelvin.
/// Manufacturer data (e.g., Bitzer HSK/CSH series, Grasso) typically provides
/// these as polynomial fits to measured data tables.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ScrewPerformanceCurves {
    /// Suction mass flow rate: ṁ_suc = f(SST, SDT)  [kg/s]
    ///
    /// SST = Saturated Suction Temperature  [K]
    /// SDT = Saturated Discharge Temperature [K]
    pub mass_flow_curve: Polynomial2D,

    /// Shaft power consumption: W = f(SST, SDT)  [W]
    pub power_curve: Polynomial2D,

    /// Economizer mass flow fraction: x_eco = f(SST, SDT)  [dimensionless, 0–1]
    ///
    /// x_eco = ṁ_eco / ṁ_suction
    /// Typically 0.05–0.20, increasing with pressure ratio.
    /// Set all coefficients to 0.0 and constant term to desired fraction
    /// for a fixed economizer fraction.
    pub eco_flow_fraction_curve: Polynomial2D,
}

impl ScrewPerformanceCurves {
    /// Creates curves with a constant economizer fraction (simple model).
    ///
    /// # Arguments
    /// * `mass_flow_curve`   — Polynomial2D for ṁ_suction = f(SST, SDT)
    /// * `power_curve`       — Polynomial2D for W = f(SST, SDT)
    /// * `eco_fraction`      — Fixed economizer mass flow fraction (0.05–0.20 typical)
    pub fn with_fixed_eco_fraction(
        mass_flow_curve: Polynomial2D,
        power_curve: Polynomial2D,
        eco_fraction: f64,
    ) -> Self {
        // Constant polynomial: f(x,y) = eco_fraction
        let eco_curve = Polynomial2D::bilinear(eco_fraction, 0.0, 0.0, 0.0);
        Self {
            mass_flow_curve,
            power_curve,
            eco_flow_fraction_curve: eco_curve,
        }
    }

    /// Validates that all curve coefficients are finite.
    pub fn validate(&self) -> Result<(), ComponentError> {
        self.mass_flow_curve.validate()?;
        self.power_curve.validate()?;
        self.eco_flow_fraction_curve.validate()?;
        Ok(())
    }
}

// ─────────────────────────────────────────────────────────────────────────────
// ScrewEconomizerCompressor
// ─────────────────────────────────────────────────────────────────────────────

/// Screw compressor with intermediate economizer injection port.
///
/// **Ports (3 total):**
/// - `port_suction` (index 0): Low-pressure inlet
/// - `port_discharge` (index 1): High-pressure outlet  
/// - `port_economizer` (index 2): Intermediate-pressure injection inlet
///
/// **Internal state variables (3 total, via `internal_state_len()`):**
/// - `state[offset+0]`: ṁ_suction   (kg/s)
/// - `state[offset+1]`: ṁ_eco       (kg/s)
/// - `state[offset+2]`: W_shaft      (W)
///
/// Note: h_suction and h_discharge are read from the connected port enthalpies
/// (graph state), not from the component's internal state block.
///
/// **Equations (5 total, via `n_equations()`):**
/// 1. Mass flow suction: ṁ_suc_calc − ṁ_suc_state = 0
/// 2. Economizer mass flow: ṁ_eco_calc − ṁ_eco_state = 0
/// 3. Energy balance: ṁ_suc×h_suc + ṁ_eco×h_eco + W = ṁ_total×h_dis
/// 4. Economizer pressure: P_eco − P_eco_nominal = 0  (geometric mean)
/// 5. Power: W_calc − W_state = 0
pub struct ScrewEconomizerCompressor {
    /// Performance curves
    curves: ScrewPerformanceCurves,
    /// Refrigerant fluid identifier (e.g., "R134a", "R410A", "R1234ze(E)")
    fluid_id: String,
    /// Nominal frequency [Hz] — typically 50 Hz at full speed
    nominal_frequency_hz: f64,
    /// Current operating frequency [Hz] (VFD controlled, 25–60 Hz)
    frequency_hz: f64,
    /// Mechanical efficiency (0.0–1.0)
    mechanical_efficiency: f64,
    /// Built-in volume index Vi = V_s / V_d (typically 2.2–5.0).
    volume_index: f64,
    /// Slide-valve position in [0, 1] (1 = full load). Reduces effective Vi.
    slide_valve_position: f64,
    /// Circuit identifier for multi-circuit topology
    circuit_id: CircuitId,
    /// Operational state: On / Off / Bypass
    operational_state: OperationalState,
    /// Calibration factors (f_m, f_power)
    calib: Calib,
    /// Calibration state vector indices (injected by solver)
    calib_indices: CalibIndices,
    /// All 3 ports stored in a Vec for `get_ports()` compatibility.
    /// Index 0: suction (inlet), Index 1: discharge (outlet), Index 2: economizer (inlet)
    ports: Vec<ConnectedPort>,
    /// Offset of this component's internal state block in the global state vector.
    /// Set by `System::finalize()` via `set_system_context()`.
    /// After CM1.3: 1 internal variable: [W_shaft] at `state[offset]`.
    /// ṁ_suc and ṁ_eco are now edge unknowns accessed via m_idx fields.
    global_state_offset: usize,
    /// State-vector index of the suction edge ṁ unknown (CM1.3).
    suction_m_idx: Option<usize>,
    /// State-vector index of the economizer edge ṁ unknown (CM1.3).
    eco_m_idx: Option<usize>,
    /// State-vector index of the discharge edge ṁ unknown (CM1.3).
    discharge_m_idx: Option<usize>,
}

impl std::fmt::Debug for ScrewEconomizerCompressor {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_struct("ScrewEconomizerCompressor")
            .field("fluid_id", &self.fluid_id)
            .field("frequency_hz", &self.frequency_hz)
            .field("mechanical_efficiency", &self.mechanical_efficiency)
            .field("operational_state", &self.operational_state)
            .finish()
    }
}

impl ScrewEconomizerCompressor {
    /// Creates a new screw compressor with economizer port.
    ///
    /// # Arguments
    ///
    /// * `curves`                 — SST/SDT polynomial performance curves
    /// * `fluid_id`               — Refrigerant name (CoolProp compatible, e.g. `"R134a"`)
    /// * `nominal_frequency_hz`   — Full-speed frequency (usually 50 Hz in Europe)
    /// * `mechanical_efficiency`  — Shaft-to-fluid mechanical efficiency (0.0–1.0)
    /// * `port_suction`           — Connected suction port
    /// * `port_discharge`         — Connected discharge port
    /// * `port_economizer`        — Connected economizer injection port
    ///
    /// # Errors
    ///
    /// Returns [`ComponentError::InvalidState`] if:
    /// - Curves are invalid (NaN, infinite)
    /// - Frequency ≤ 0
    /// - Mechanical efficiency outside (0, 1]
    #[allow(clippy::too_many_arguments)]
    pub fn new(
        curves: ScrewPerformanceCurves,
        fluid_id: impl Into<String>,
        nominal_frequency_hz: f64,
        mechanical_efficiency: f64,
        port_suction: ConnectedPort,
        port_discharge: ConnectedPort,
        port_economizer: ConnectedPort,
    ) -> Result<Self, ComponentError> {
        curves.validate()?;

        if nominal_frequency_hz <= 0.0 {
            return Err(ComponentError::InvalidState(
                "ScrewEconomizerCompressor: nominal frequency must be positive".into(),
            ));
        }
        if !(0.0..=1.0).contains(&mechanical_efficiency) || mechanical_efficiency == 0.0 {
            return Err(ComponentError::InvalidState(
                "ScrewEconomizerCompressor: mechanical_efficiency must be in (0, 1]".into(),
            ));
        }

        Ok(Self {
            curves,
            fluid_id: fluid_id.into(),
            nominal_frequency_hz,
            frequency_hz: nominal_frequency_hz,
            mechanical_efficiency,
            volume_index: 2.8,
            slide_valve_position: 1.0,
            circuit_id: CircuitId::default(),
            operational_state: OperationalState::On,
            calib: Calib::default(),
            calib_indices: CalibIndices::default(),
            ports: vec![port_suction, port_discharge, port_economizer],
            global_state_offset: 0,
            suction_m_idx: None,
            eco_m_idx: None,
            discharge_m_idx: None,
        })
    }

    /// Sets built-in volume index Vi (typical 2.2–5.0).
    pub fn with_volume_index(mut self, vi: f64) -> Self {
        self.volume_index = vi.max(1.1);
        self
    }

    /// Sets slide-valve position (1 = full load, 0.5 ≈ 50 % capacity).
    pub fn with_slide_valve(mut self, position: f64) -> Self {
        self.slide_valve_position = position.clamp(0.05, 1.0);
        self
    }

    /// Effective Vi after slide-valve unloading (Shaw / Manske simplified).
    ///
    /// `Vi_eff ≈ 1 + (Vi − 1) · slide` — port geometry fixed, trapped volume shrinks.
    pub fn effective_volume_index(&self) -> f64 {
        1.0 + (self.volume_index - 1.0) * self.slide_valve_position
    }

    /// Ideal Vi for a polytropic compression: `CR^(1/k)`.
    pub fn ideal_volume_index(pressure_ratio: f64, kappa: f64) -> f64 {
        let cr = pressure_ratio.max(1.01);
        let k = kappa.clamp(1.05, 1.4);
        cr.powf(1.0 / k)
    }

    /// Power / efficiency penalty when Vi_eff ≠ Vi_ideal (Manske/Shaw style).
    ///
    /// Returns a multiplier ≥ 1 on shaft power. Quadratic in relative mismatch,
    /// capped at 1.25 (25 % over-power at extreme mismatch).
    pub fn vi_power_penalty(vi_eff: f64, vi_ideal: f64) -> f64 {
        let denom = vi_ideal.max(1.1);
        let mismatch = ((vi_eff - vi_ideal) / denom).abs();
        (1.0 + 0.35 * mismatch * mismatch).min(1.25)
    }

    // ─── Accessors ────────────────────────────────────────────────────────────

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

    /// Returns the built-in volume index.
    pub fn volume_index(&self) -> f64 {
        self.volume_index
    }

    /// Returns the slide-valve position.
    pub fn slide_valve_position(&self) -> f64 {
        self.slide_valve_position
    }

    #[cfg(test)]
    pub(crate) fn set_volume_index_for_test(&mut self, vi: f64) {
        self.volume_index = vi.max(1.1);
    }

    #[cfg(test)]
    pub(crate) fn set_slide_for_test(&mut self, pos: f64) {
        self.slide_valve_position = pos.clamp(0.05, 1.0);
    }

    /// Returns the current operating frequency [Hz].
    pub fn frequency_hz(&self) -> f64 {
        self.frequency_hz
    }

    /// Sets the operating frequency [Hz] (VFD control).
    ///
    /// Allowed range: [0.0, nominal_frequency_hz × 1.1]
    /// Setting to 0.0 effectively stops the compressor (but use `set_state(Off)` instead).
    pub fn set_frequency_hz(&mut self, freq: f64) -> Result<(), ComponentError> {
        if freq < 0.0 {
            return Err(ComponentError::InvalidState(
                "ScrewEconomizerCompressor: frequency cannot be negative".into(),
            ));
        }
        let max_freq = self.nominal_frequency_hz * 1.1; // allow 10% overspeed
        if freq > max_freq {
            return Err(ComponentError::InvalidState(format!(
                "ScrewEconomizerCompressor: frequency {:.1} Hz exceeds maximum {:.1} Hz",
                freq, max_freq
            )));
        }
        self.frequency_hz = freq;
        Ok(())
    }

    /// Returns the frequency ratio (current / nominal).
    ///
    /// Used internally to scale volumetric performance.
    pub fn frequency_ratio(&self) -> f64 {
        if self.nominal_frequency_hz > 0.0 {
            self.frequency_hz / self.nominal_frequency_hz
        } else {
            1.0
        }
    }

    /// Returns the nominal frequency [Hz].
    pub fn nominal_frequency_hz(&self) -> f64 {
        self.nominal_frequency_hz
    }

    /// Returns mechanical efficiency.
    pub fn mechanical_efficiency(&self) -> f64 {
        self.mechanical_efficiency
    }

    /// Returns calibration factors.
    pub fn calib(&self) -> &Calib {
        &self.calib
    }

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

    /// Returns reference to suction port (index 0).
    pub fn port_suction(&self) -> &ConnectedPort {
        &self.ports[0]
    }

    /// Returns reference to discharge port (index 1).
    pub fn port_discharge(&self) -> &ConnectedPort {
        &self.ports[1]
    }

    /// Returns reference to economizer injection port (index 2).
    pub fn port_economizer(&self) -> &ConnectedPort {
        &self.ports[2]
    }

    // ─── Internal calculations ────────────────────────────────────────────────

    /// Extracts saturated temperatures from port pressures via Clausius-Clapeyron
    /// approximation (placeholder until CoolProp integration is wired).
    ///
    /// For the SST/SDT model these only need to be approximately correct.
    fn estimate_sst_sdt_k(&self) -> (f64, f64) {
        let p_suc_pa = self.ports[0].pressure().to_pascals();
        let p_dis_pa = self.ports[1].pressure().to_pascals();

        // Simple Clausius-Clapeyron approximation for R134a family refrigerants:
        // T_sat [K] ≈ T_ref / (1 - (R*T_ref/h_vap) * ln(P/P_ref))
        // Using linearised form: T_sat ≈ A + B * ln(P)
        // These constants are approximate for R134a around typical chiller conditions.
        // They will be overridden by CoolProp when the full fluid backend is wired.
        const A: f64 = -47.0 + 273.15; // K
        const B: f64 = 22.0; // K / ln-unit

        let sst_k = (A + B * (p_suc_pa / 1e5_f64).ln()).max(230.0).min(320.0);
        let sdt_k = (A + B * (p_dis_pa / 1e5_f64).ln()).max(280.0).min(360.0);
        (sst_k, sdt_k)
    }

    /// Computes nominal suction mass flow [kg/s] at current SST/SDT, scaled by VFD.
    fn calc_mass_flow_suction_nominal(&self, sst_k: f64, sdt_k: f64) -> f64 {
        let m_nominal = self.curves.mass_flow_curve.evaluate(sst_k, sdt_k);

        // VFD scaling: ṁ ∝ frequency (volumetric machine)
        m_nominal * self.frequency_ratio()
    }

    /// Computes calibrated suction mass flow [kg/s].
    fn calc_mass_flow_suction(&self, sst_k: f64, sdt_k: f64, state: Option<&StateSlice>) -> f64 {
        let m_scaled = self.calc_mass_flow_suction_nominal(sst_k, sdt_k);

        // Calibration factor f_m
        let f_m = if let Some(st) = state {
            self.calib_indices
                .z_flow
                .map(|idx| st[idx])
                .unwrap_or(self.calib.z_flow)
        } else {
            self.calib.z_flow
        };

        m_scaled * f_m
    }

    /// Computes calibrated economizer injection mass flow [kg/s].
    fn calc_mass_flow_economizer(&self, sst_k: f64, sdt_k: f64, state: Option<&StateSlice>) -> f64 {
        let m_suc_nominal = self.calc_mass_flow_suction_nominal(sst_k, sdt_k);
        let x_eco = self.calc_eco_fraction(sst_k, sdt_k);
        let z_flow_eco = if let Some(st) = state {
            self.calib_indices
                .z_flow_eco
                .map(|idx| st[idx])
                .unwrap_or(self.calib.z_flow_eco)
        } else {
            self.calib.z_flow_eco
        };
        m_suc_nominal * x_eco * z_flow_eco
    }

    /// Computes economizer mass flow fraction at current SST/SDT.
    fn calc_eco_fraction(&self, sst_k: f64, sdt_k: f64) -> f64 {
        self.curves
            .eco_flow_fraction_curve
            .evaluate(sst_k, sdt_k)
            .clamp(0.0, 0.4) // physical bound: eco fraction 0–40%
    }

    /// Computes nominal shaft power [W] scaled by VFD, Vi mismatch, and calibration.
    fn calc_power(&self, sst_k: f64, sdt_k: f64, state: Option<&StateSlice>) -> f64 {
        let w_nominal = self.curves.power_curve.evaluate(sst_k, sdt_k);

        // VFD scaling: power ∝ frequency (approximately, for screw compressors)
        let w_scaled = w_nominal * self.frequency_ratio();

        // Vi mismatch penalty (built-in volume ratio vs ideal polytropic).
        let p_suc = self.ports[0].pressure().to_pascals().max(1.0);
        let p_dis = self.ports[1].pressure().to_pascals().max(p_suc * 1.01);
        let vi_ideal = Self::ideal_volume_index(p_dis / p_suc, 1.14);
        let vi_pen = Self::vi_power_penalty(self.effective_volume_index(), vi_ideal);

        // Calibration factor f_power
        let f_power = if let Some(st) = state {
            self.calib_indices
                .z_power
                .map(|idx| st[idx])
                .unwrap_or(self.calib.z_power)
        } else {
            self.calib.z_power
        };

        w_scaled * vi_pen * f_power
    }
}

// ─────────────────────────────────────────────────────────────────────────────
// Component trait implementation
// ─────────────────────────────────────────────────────────────────────────────

impl Component for ScrewEconomizerCompressor {
    /// Returns 5 equations:
    /// 1. Suction mass flow balance
    /// 2. Economizer mass flow balance
    /// 3. Energy balance (1st law)
    /// 4. Economizer pressure constraint
    /// 5. Shaft power balance
    fn n_equations(&self) -> usize {
        6 // r0: ṁ_suc, r1: ṁ_eco, r2: energy, r3: P_eco, r4: W_shaft, r5: mass balance (CM1.3)
    }

    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(),
            });
        }

        // ── Resolve edge ṁ indices (CM1.3) — fallback to internal slots for isolated tests ──
        let off = self.global_state_offset;
        // W_shaft is the only remaining internal variable (at offset 0 after CM1.3).
        // Fallback m_indices point into the old internal layout so isolated unit tests
        // (which call set_system_context with empty slice) still produce numeric residuals.
        let suc_m_idx = self.suction_m_idx.unwrap_or(off);
        let eco_m_idx = self.eco_m_idx.unwrap_or(off + 1);
        let dis_m_idx = self.discharge_m_idx.unwrap_or(off + 2);
        let w_off = off; // W_shaft at internal offset 0

        match self.operational_state {
            OperationalState::Off => {
                residuals[0] = state.get(suc_m_idx).copied().unwrap_or(0.0); // ṁ_suc = 0
                residuals[1] = state.get(eco_m_idx).copied().unwrap_or(0.0); // ṁ_eco = 0
                residuals[2] = 0.0;
                residuals[3] = 0.0;
                residuals[4] = state.get(w_off).copied().unwrap_or(0.0); // W = 0
                                                                         // r5: ṁ_dis = ṁ_suc + ṁ_eco (all zero)
                residuals[5] = state.get(dis_m_idx).copied().unwrap_or(0.0)
                    - state.get(suc_m_idx).copied().unwrap_or(0.0)
                    - state.get(eco_m_idx).copied().unwrap_or(0.0);
                return Ok(());
            }
            OperationalState::Bypass => {
                let p_suc = self.ports[0].pressure().to_pascals();
                let p_dis = self.ports[1].pressure().to_pascals();
                let h_suc = self.ports[0].enthalpy().to_joules_per_kg();
                let h_dis = self.ports[1].enthalpy().to_joules_per_kg();
                residuals[0] = p_suc - p_dis;
                residuals[1] = h_suc - h_dis;
                residuals[2] = state.get(eco_m_idx).copied().unwrap_or(0.0); // ṁ_eco = 0
                residuals[3] = 0.0;
                residuals[4] = state.get(w_off).copied().unwrap_or(0.0); // W = 0
                residuals[5] = state.get(dis_m_idx).copied().unwrap_or(0.0)
                    - state.get(suc_m_idx).copied().unwrap_or(0.0)
                    - state.get(eco_m_idx).copied().unwrap_or(0.0);
                return Ok(());
            }
            OperationalState::On => {}
        }

        // ── Validate state vector ────────────────────────────────────────────
        let required_len = [suc_m_idx, eco_m_idx, dis_m_idx, w_off]
            .into_iter()
            .max()
            .map(|idx| idx + 1)
            .unwrap_or(0);
        if state.len() < required_len {
            return Err(ComponentError::InvalidStateDimensions {
                expected: required_len,
                actual: state.len(),
            });
        }

        let m_suc_state = state[suc_m_idx]; // kg/s — edge unknown (CM1.3)
        let m_eco_state = state[eco_m_idx]; // kg/s — edge unknown (CM1.3)
        let h_suc = self.ports[0].enthalpy().to_joules_per_kg();
        let h_dis = self.ports[1].enthalpy().to_joules_per_kg();
        let h_eco = self.ports[2].enthalpy().to_joules_per_kg();
        let w_state = state[w_off]; // W — sole internal variable after CM1.3

        // ── Compute performance from curves ──────────────────────────────────
        let (sst_k, sdt_k) = self.estimate_sst_sdt_k();
        let m_suc_calc = self.calc_mass_flow_suction(sst_k, sdt_k, Some(state));
        let m_eco_calc = self.calc_mass_flow_economizer(sst_k, sdt_k, Some(state));
        let w_calc = self.calc_power(sst_k, sdt_k, Some(state));

        // ── Residual 0: Suction mass flow ────────────────────────────────────
        residuals[0] = m_suc_calc - m_suc_state;

        // ── Residual 1: Economizer mass flow ─────────────────────────────────
        residuals[1] = m_eco_calc - m_eco_state;

        // ── Residual 2: First-law energy balance ─────────────────────────────
        let energy_in =
            m_suc_state * h_suc + m_eco_state * h_eco + w_state * self.mechanical_efficiency;
        let energy_out = (m_suc_state + m_eco_state) * h_dis;
        residuals[2] = energy_in - energy_out;

        // ── Residual 3: Economizer pressure (geometric mean) ─────────────────
        let p_suc = self.ports[0].pressure().to_pascals();
        let p_dis = self.ports[1].pressure().to_pascals();
        let p_eco_port = self.ports[2].pressure().to_pascals();
        let p_eco_set = (p_suc * p_dis).sqrt();
        residuals[3] = p_eco_port - p_eco_set;

        // ── Residual 4: Shaft power ──────────────────────────────────────────
        residuals[4] = w_calc - w_state;

        // ── Residual 5: Mass balance  ṁ_dis = ṁ_suc + ṁ_eco  (CM1.3) ────────
        residuals[5] = state[dis_m_idx] - m_suc_state - m_eco_state;

        Ok(())
    }

    fn jacobian_entries(
        &self,
        state: &StateSlice,
        jacobian: &mut JacobianBuilder,
    ) -> Result<(), ComponentError> {
        let off = self.global_state_offset;
        let suc_m_idx = self.suction_m_idx.unwrap_or(off);
        let eco_m_idx = self.eco_m_idx.unwrap_or(off + 1);
        let dis_m_idx = self.discharge_m_idx.unwrap_or(off + 2);
        let w_off = off; // W_shaft at internal offset 0

        let h_suc = self.ports[0].enthalpy().to_joules_per_kg();
        let h_dis = self.ports[1].enthalpy().to_joules_per_kg();
        let h_eco = self.ports[2].enthalpy().to_joules_per_kg();

        // Row 0: ∂r₀/∂ṁ_suc = -1 (exact, CM1.3)
        jacobian.add_entry(0, suc_m_idx, -1.0);

        // Row 1: ∂r₁/∂ṁ_eco = -1 (exact, CM1.3)
        jacobian.add_entry(1, eco_m_idx, -1.0);

        // Row 2: energy balance partial derivatives
        jacobian.add_entry(2, suc_m_idx, h_suc - h_dis);
        jacobian.add_entry(2, eco_m_idx, h_eco - h_dis);
        jacobian.add_entry(2, w_off, self.mechanical_efficiency);

        // Row 3: economizer pressure — no mass-flow dependency
        // (depends only on port pressures, which are edge P unknowns, not captured here)

        // Row 4: ∂r₄/∂W_state = -1
        jacobian.add_entry(4, w_off, -1.0);

        // Row 5: mass balance  r5 = ṁ_dis − ṁ_suc − ṁ_eco  (exact, CM1.3)
        jacobian.add_entry(5, dis_m_idx, 1.0);
        jacobian.add_entry(5, suc_m_idx, -1.0);
        jacobian.add_entry(5, eco_m_idx, -1.0);

        // Calibration Jacobian entries
        if let Some(z_flow_idx) = self.calib_indices.z_flow {
            let (sst_k, sdt_k) = self.estimate_sst_sdt_k();
            let m_nominal = self.calc_mass_flow_suction_nominal(sst_k, sdt_k);
            jacobian.add_entry(0, z_flow_idx, m_nominal);
        }
        if let Some(z_flow_eco_idx) = self.calib_indices.z_flow_eco {
            let (sst_k, sdt_k) = self.estimate_sst_sdt_k();
            let m_nominal = self.calc_mass_flow_suction_nominal(sst_k, sdt_k);
            let x_eco = self.calc_eco_fraction(sst_k, sdt_k);
            jacobian.add_entry(1, z_flow_eco_idx, m_nominal * x_eco);
        }
        if let Some(z_power_idx) = self.calib_indices.z_power {
            let (sst_k, sdt_k) = self.estimate_sst_sdt_k();
            let w_nominal = self.curves.power_curve.evaluate(sst_k, sdt_k) * self.frequency_ratio();
            jacobian.add_entry(4, z_power_idx, w_nominal);
        }

        // Suppress unused state variable access (edge ṁ and h are read via port values)
        let _ = state;

        Ok(())
    }

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

    fn port_names(&self) -> Vec<String> {
        vec![
            "suction".to_string(),
            "discharge".to_string(),
            "economizer".to_string(),
        ]
    }

    fn internal_state_len(&self) -> usize {
        // 1 internal variable: [W_shaft] — ṁ_suc and ṁ_eco moved to edge unknowns (CM1.3)
        1
    }

    fn set_system_context(
        &mut self,
        state_offset: usize,
        external_edge_state_indices: &[(usize, usize, usize)],
    ) {
        self.global_state_offset = state_offset;
        // Layout: incoming edges first (suction, economizer), then outgoing (discharge).
        // Triple: (m_idx, p_idx, h_idx)
        // Typical ordering: [0]=suction(incoming), [1]=discharge(outgoing), [2]=economizer(incoming)
        // Map by ports order: port[0]=suction, port[1]=discharge, port[2]=economizer
        let incoming_iter = external_edge_state_indices.iter().take_while(|_| true); // all incoming edges come first in finalize()
                                                                                     // The exact ordering depends on graph traversal; use index-based approach:
                                                                                     // incoming[0] = suction edge, incoming[1] = economizer edge, outgoing[0] = discharge edge
        let n = external_edge_state_indices.len();
        if n >= 1 {
            self.suction_m_idx = Some(external_edge_state_indices[0].0);
        }
        if n >= 2 {
            // Second incoming is economizer (or outgoing discharge if only 1 incoming)
            // For 3-port: incoming = [suction, economizer], outgoing = [discharge]
            // finalize() appends incoming first then outgoing, so:
            // [0]=suction, [1]=economizer, [2]=discharge
            self.eco_m_idx = Some(external_edge_state_indices[1].0);
        }
        if n >= 3 {
            self.discharge_m_idx = Some(external_edge_state_indices[2].0);
        }
        let _ = incoming_iter; // suppress warning
    }

    fn port_mass_flows(&self, state: &StateSlice) -> Result<Vec<MassFlow>, ComponentError> {
        let off = self.global_state_offset;
        let suc_m_idx = self.suction_m_idx.unwrap_or(off);
        let eco_m_idx = self.eco_m_idx.unwrap_or(off + 1);
        let required_len = suc_m_idx.max(eco_m_idx) + 1;
        if state.len() < required_len {
            return Err(ComponentError::InvalidStateDimensions {
                expected: required_len,
                actual: state.len(),
            });
        }
        let m_suc = MassFlow::from_kg_per_s(state[suc_m_idx]);
        let m_eco = MassFlow::from_kg_per_s(state[eco_m_idx]);
        let m_total = MassFlow::from_kg_per_s(state[suc_m_idx] + state[eco_m_idx]);
        Ok(vec![
            m_suc,                                           // suction in (+)
            MassFlow::from_kg_per_s(-m_total.to_kg_per_s()), // discharge out (-)
            m_eco,                                           // economizer in (+)
        ])
    }

    fn port_enthalpies(&self, _state: &StateSlice) -> Result<Vec<Enthalpy>, ComponentError> {
        Ok(self.ports.iter().map(|port| port.enthalpy()).collect())
    }

    fn energy_transfers(&self, state: &StateSlice) -> Option<(Power, Power)> {
        match self.operational_state {
            OperationalState::Off | OperationalState::Bypass => {
                Some((Power::from_watts(0.0), Power::from_watts(0.0)))
            }
            OperationalState::On => {
                let off = self.global_state_offset;
                if state.len() <= off {
                    return None;
                }
                let w = state[off]; // shaft power W (CM1.3: sole internal variable)
                                    // Work done ON the compressor → negative sign convention
                Some((Power::from_watts(0.0), Power::from_watts(-w)))
            }
        }
    }

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

    fn signature(&self) -> String {
        format!(
            "ScrewEconomizerCompressor(fluid={},freq={:.0}Hz,eta_mech={:.2})",
            self.fluid_id, self.frequency_hz, self.mechanical_efficiency
        )
    }

    fn to_params(&self) -> crate::ComponentParams {
        crate::ComponentParams::new("ScrewEconomizerCompressor")
            .with_param("fluid", self.fluid_id.as_str())
            .with_param("nominal_frequency_hz", self.nominal_frequency_hz)
            .with_param("frequency_hz", self.frequency_hz)
            .with_param("mechanical_efficiency", self.mechanical_efficiency)
            .with_param(
                "curves",
                serde_json::to_value(&self.curves).unwrap_or(serde_json::Value::Null),
            )
            .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
        }
    }
}

// ─────────────────────────────────────────────────────────────────────────────
// StateManageable
// ─────────────────────────────────────────────────────────────────────────────

impl StateManageable for ScrewEconomizerCompressor {
    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 for ScrewEconomizerCompressor".into(),
            })
        }
    }

    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, id: CircuitId) {
        self.circuit_id = id;
    }
}

// ─────────────────────────────────────────────────────────────────────────────
// Tests
// ─────────────────────────────────────────────────────────────────────────────

#[cfg(test)]
mod tests {
    use super::*;
    use crate::port::{FluidId, Port};
    use entropyk_core::{Enthalpy, Pressure};

    fn make_port(fluid: &str, p_bar: f64, h_kj_kg: f64) -> ConnectedPort {
        let a = Port::new(
            FluidId::new(fluid),
            Pressure::from_bar(p_bar),
            Enthalpy::from_joules_per_kg(h_kj_kg * 1000.0),
        );
        let b = Port::new(
            FluidId::new(fluid),
            Pressure::from_bar(p_bar),
            Enthalpy::from_joules_per_kg(h_kj_kg * 1000.0),
        );
        a.connect(b).unwrap().0
    }

    fn default_curves() -> ScrewPerformanceCurves {
        // Simple bilinear model approximating a 200 kW R134a screw at 50 Hz
        // SST ≈ 276 K (+3°C), SDT ≈ 323 K (+50°C)
        ScrewPerformanceCurves::with_fixed_eco_fraction(
            Polynomial2D::bilinear(1.2, 0.003, -0.002, 0.000_01), // ṁ_suc kg/s
            Polynomial2D::bilinear(55_000.0, 200.0, -300.0, 0.5), // W shaft
            0.12,                                                 // 12% economizer fraction
        )
    }

    fn make_compressor() -> ScrewEconomizerCompressor {
        let suc = make_port("R134a", 3.2, 400.0); // ~276 K sat, superheated
        let dis = make_port("R134a", 12.8, 440.0); // ~323 K sat
        let eco = make_port("R134a", 6.4, 260.0); // ~300 K sat, flash gas

        ScrewEconomizerCompressor::new(default_curves(), "R134a", 50.0, 0.92, suc, dis, eco)
            .expect("valid compressor")
    }

    #[test]
    fn test_creation_succeeds() {
        let comp = make_compressor();
        assert_eq!(comp.fluid_id(), "R134a");
        assert_eq!(comp.nominal_frequency_hz(), 50.0);
        assert_eq!(comp.frequency_hz(), 50.0);
        assert!((comp.mechanical_efficiency() - 0.92).abs() < 1e-10);
    }

    #[test]
    fn test_n_equations() {
        let comp = make_compressor();
        // CM1.3: 5 thermo/performance + 1 mass-balance = 6
        assert_eq!(comp.n_equations(), 6);
    }

    #[test]
    fn test_get_ports_returns_three() {
        let comp = make_compressor();
        let ports = comp.get_ports();
        assert_eq!(
            ports.len(),
            3,
            "ScrewEconomizerCompressor should expose 3 ports"
        );
        // Index 0: suction, Index 1: discharge, Index 2: economizer
        assert!((ports[0].pressure().to_bar() - 3.2).abs() < 1e-10);
        assert!((ports[1].pressure().to_bar() - 12.8).abs() < 1e-10);
        assert!((ports[2].pressure().to_bar() - 6.4).abs() < 1e-10);
    }

    #[test]
    fn test_frequency_ratio_at_nominal() {
        let comp = make_compressor();
        assert!((comp.frequency_ratio() - 1.0).abs() < 1e-10);
    }

    #[test]
    fn test_set_frequency_vfd() {
        let mut comp = make_compressor();
        comp.set_frequency_hz(40.0).unwrap();
        assert_eq!(comp.frequency_hz(), 40.0);
        assert!((comp.frequency_ratio() - 0.8).abs() < 1e-10);
    }

    #[test]
    fn test_frequency_out_of_range_rejected() {
        let mut comp = make_compressor();
        // nominal = 50 Hz → max = 50 × 1.1 = 55 Hz
        assert!(comp.set_frequency_hz(-1.0).is_err());
        assert!(comp.set_frequency_hz(55.0).is_ok()); // exactly at max: ok
        assert!(comp.set_frequency_hz(55.1).is_err()); // above max: rejected
        assert!(comp.set_frequency_hz(100.0).is_err());
    }

    #[test]
    fn test_compute_residuals_ok_with_reasonable_state() {
        let mut comp = make_compressor();
        // CM1.3: stride-3 edge layout; suc=(0,1,2), eco=(3,4,5), dis=(6,7,8); W at 9
        comp.set_system_context(9, &[(0, 1, 2), (3, 4, 5), (6, 7, 8)]);
        let mut state = vec![0.0; 10];
        state[0] = 1.0; // ṁ_suc
        state[3] = 0.12; // ṁ_eco
        state[9] = 50_000.0; // W_shaft
        let mut residuals = vec![0.0; 6];
        let result = comp.compute_residuals(&state, &mut residuals);
        assert!(
            result.is_ok(),
            "compute_residuals failed: {:?}",
            result.err()
        );
        for (i, r) in residuals.iter().enumerate() {
            assert!(r.is_finite(), "residual[{}] = {} is not finite", i, r);
        }
    }

    #[test]
    fn test_compute_residuals_off_state() {
        let mut comp = make_compressor();
        comp.set_state(OperationalState::Off).unwrap();
        // CM1.3: stride-3 edge layout; W at offset 9
        comp.set_system_context(9, &[(0, 1, 2), (3, 4, 5), (6, 7, 8)]);
        let state = vec![0.0; 10];
        let mut residuals = vec![0.0; 6];
        comp.compute_residuals(&state, &mut residuals).unwrap();
        // In Off state, all residuals should be zero
        for r in &residuals {
            assert!(r.abs() < 1e-10);
        }
    }

    #[test]
    fn test_compute_residuals_wrong_size() {
        let comp = make_compressor();
        let state = vec![1.0; 5];
        let mut residuals = vec![0.0; 3]; // wrong size
        let result = comp.compute_residuals(&state, &mut residuals);
        assert!(matches!(
            result,
            Err(ComponentError::InvalidResidualDimensions { .. })
        ));
    }

    #[test]
    fn test_port_mass_flows() {
        let comp = make_compressor();
        let state = vec![1.0, 0.12, 0.0, 0.0, 0.0];
        let flows = comp.port_mass_flows(&state).unwrap();
        assert_eq!(flows.len(), 3);
        // suction in, total discharge out, eco in
        assert!((flows[0].to_kg_per_s() - 1.0).abs() < 1e-10);
        assert!((flows[1].to_kg_per_s() + 1.12).abs() < 1e-10); // negative (out)
        assert!((flows[2].to_kg_per_s() - 0.12).abs() < 1e-10);
    }

    #[test]
    fn test_energy_transfers_on() {
        let comp = make_compressor();
        // CM1.3: W_shaft is the only internal var, at global_state_offset=0 by default
        let state = vec![55_000.0]; // state[0] = W_shaft
        let (q, w) = comp.energy_transfers(&state).unwrap();
        assert_eq!(q.to_watts(), 0.0);
        assert!((w.to_watts() + 55_000.0).abs() < 1e-6);
    }

    #[test]
    fn test_energy_transfers_off() {
        let mut comp = make_compressor();
        comp.set_state(OperationalState::Off).unwrap();
        let state = vec![0.0; 5];
        let (q, w) = comp.energy_transfers(&state).unwrap();
        assert_eq!(q.to_watts(), 0.0);
        assert_eq!(w.to_watts(), 0.0);
    }

    #[test]
    fn test_jacobian_entries_ok() {
        let comp = make_compressor();
        let state = vec![1.0, 0.12, 400_000.0, 440_000.0, 55_000.0];
        let mut jac = JacobianBuilder::new();
        let result = comp.jacobian_entries(&state, &mut jac);
        assert!(result.is_ok());
        assert!(!jac.is_empty());
    }

    #[test]
    fn test_energy_jacobian_uses_mechanical_efficiency() {
        let mut comp = make_compressor();
        comp.set_system_context(3, &[(0, 0, 0), (1, 0, 0), (2, 0, 0)]);
        let state = vec![1.0, 0.12, 1.12, 55_000.0];
        let mut jac = JacobianBuilder::new();
        comp.jacobian_entries(&state, &mut jac).unwrap();

        let dw_entry = jac
            .entries()
            .iter()
            .find(|&&(row, col, _)| row == 2 && col == 3)
            .copied()
            .expect("energy row should include W derivative");
        assert!((dw_entry.2 - comp.mechanical_efficiency()).abs() < 1e-12);
    }

    #[test]
    fn test_port_names_and_to_params_match_cli_contract() {
        let comp = make_compressor();
        assert_eq!(
            comp.port_names(),
            vec![
                "suction".to_string(),
                "discharge".to_string(),
                "economizer".to_string()
            ]
        );

        let params = comp.to_params();
        assert_eq!(params.component_type, "ScrewEconomizerCompressor");
        assert!(params.get("nominal_frequency_hz").is_some());
        assert!(params.get("frequency_hz").is_some());
        assert!(params.get("mechanical_efficiency").is_some());
        assert!(params.get("nominalFrequencyHz").is_none());
    }

    #[test]
    fn test_z_flow_eco_scales_only_economizer_flow() {
        let mut base = make_compressor();
        base.set_system_context(3, &[(0, 0, 0), (1, 0, 0), (2, 0, 0)]);
        let state = vec![0.0, 0.0, 0.0, 55_000.0];

        let mut base_residuals = vec![0.0; 6];
        base.compute_residuals(&state, &mut base_residuals).unwrap();

        let mut suc_scaled = make_compressor();
        suc_scaled.set_system_context(3, &[(0, 0, 0), (1, 0, 0), (2, 0, 0)]);
        suc_scaled.set_calib(Calib {
            z_flow: 1.5,
            ..Calib::default()
        });
        let mut suc_residuals = vec![0.0; 6];
        suc_scaled
            .compute_residuals(&state, &mut suc_residuals)
            .unwrap();

        let mut eco_scaled = make_compressor();
        eco_scaled.set_system_context(3, &[(0, 0, 0), (1, 0, 0), (2, 0, 0)]);
        eco_scaled.set_calib(Calib {
            z_flow_eco: 1.5,
            ..Calib::default()
        });
        let mut eco_residuals = vec![0.0; 6];
        eco_scaled
            .compute_residuals(&state, &mut eco_residuals)
            .unwrap();

        assert!(
            (suc_residuals[1] - base_residuals[1]).abs() < 1e-12,
            "z_flow must not scale economizer residual"
        );
        assert!(
            eco_residuals[1] > base_residuals[1],
            "z_flow_eco should scale economizer residual"
        );
    }

    #[test]
    fn test_signature() {
        let comp = make_compressor();
        let sig = comp.signature();
        assert!(sig.contains("ScrewEconomizerCompressor"));
        assert!(sig.contains("R134a"));
    }

    #[test]
    fn test_curves_with_fixed_eco_fraction() {
        let curves = ScrewPerformanceCurves::with_fixed_eco_fraction(
            Polynomial2D::bilinear(1.0, 0.0, 0.0, 0.0),
            Polynomial2D::bilinear(50_000.0, 0.0, 0.0, 0.0),
            0.15,
        );
        // eco fraction should be 0.15 regardless of SST/SDT
        let frac = curves.eco_flow_fraction_curve.evaluate(270.0, 320.0);
        assert!((frac - 0.15).abs() < 1e-10);
    }

    #[test]
    fn test_invalid_mechanical_efficiency() {
        let suc = make_port("R134a", 3.2, 400.0);
        let dis = make_port("R134a", 12.8, 440.0);
        let eco = make_port("R134a", 6.4, 260.0);

        let result = ScrewEconomizerCompressor::new(
            ScrewPerformanceCurves::with_fixed_eco_fraction(
                Polynomial2D::bilinear(1.0, 0.0, 0.0, 0.0),
                Polynomial2D::bilinear(50_000.0, 0.0, 0.0, 0.0),
                0.12,
            ),
            "R134a",
            50.0,
            1.5, // invalid
            suc,
            dis,
            eco,
        );
        assert!(result.is_err());
    }

    #[test]
    fn test_internal_state_len() {
        let comp = make_compressor();
        // CM1.3: ṁ_suc and ṁ_eco moved to edge state; only W_shaft remains internal
        assert_eq!(comp.internal_state_len(), 1);
    }

    #[test]
    fn test_set_system_context_stores_offset() {
        let mut comp = make_compressor();
        assert_eq!(comp.global_state_offset, 0);
        comp.set_system_context(10, &[]);
        assert_eq!(comp.global_state_offset, 10);
    }

    #[test]
    fn test_compute_residuals_with_offset() {
        let mut comp = make_compressor();
        // CM1.3: 3 edges × 3 vars = 9 edge slots; W_shaft at offset 9
        comp.set_system_context(9, &[(0, 1, 2), (3, 4, 5), (6, 7, 8)]);

        // Build state: 9 edge vars + 1 internal (W_shaft)
        let mut state = vec![0.0; 10];
        state[0] = 1.0; // ṁ_suc at edge[0].m
        state[3] = 0.12; // ṁ_eco at edge[1].m
        state[9] = 50_000.0; // W_shaft at internal offset 9

        let mut residuals = vec![0.0; 6];
        let result = comp.compute_residuals(&state, &mut residuals);
        assert!(
            result.is_ok(),
            "compute_residuals failed: {:?}",
            result.err()
        );
        for (i, r) in residuals.iter().enumerate() {
            assert!(r.is_finite(), "residual[{}] = {} is not finite", i, r);
        }
    }

    #[test]
    fn test_jacobian_entries_with_offset() {
        let mut comp = make_compressor();
        comp.set_system_context(6, &[]);

        let mut state = vec![0.0; 9];
        state[6] = 1.0;
        state[7] = 0.12;
        state[8] = 55_000.0;

        let mut jac = JacobianBuilder::new();
        let result = comp.jacobian_entries(&state, &mut jac);
        assert!(result.is_ok());

        // All column indices in the Jacobian should be >= 6 (the offset)
        for &(_, col, _) in jac.entries() {
            assert!(col >= 6, "Jacobian column {} should be >= offset 6", col);
        }
    }

    #[test]
    fn test_port_mass_flows_with_offset() {
        let mut comp = make_compressor();
        comp.set_system_context(4, &[]);

        let mut state = vec![0.0; 7];
        state[4] = 1.0; // ṁ_suc at offset+0
        state[5] = 0.12; // ṁ_eco at offset+1
        state[6] = 0.0; // W at offset+2

        let flows = comp.port_mass_flows(&state).unwrap();
        assert_eq!(flows.len(), 3);
        assert!((flows[0].to_kg_per_s() - 1.0).abs() < 1e-10);
        assert!((flows[1].to_kg_per_s() + 1.12).abs() < 1e-10);
        assert!((flows[2].to_kg_per_s() - 0.12).abs() < 1e-10);
    }

    #[test]
    fn test_energy_transfers_with_offset() {
        let mut comp = make_compressor();
        // CM1.3: W_shaft is at global_state_offset; edge mass flows are separate
        comp.set_system_context(4, &[]);
        let mut state = vec![0.0; 5];
        state[4] = 55_000.0; // W_shaft at offset 4 (internal offset)
        comp.set_state(OperationalState::On).unwrap();
        let transfers = comp.energy_transfers(&state).unwrap();
        let w = transfers.1;
        assert!((w.to_watts() + 55_000.0).abs() < 1e-6);
    }

    #[test]
    fn test_vi_penalty_and_slide_valve() {
        assert!((ScrewEconomizerCompressor::vi_power_penalty(2.8, 2.8) - 1.0).abs() < 1e-12);
        assert!(ScrewEconomizerCompressor::vi_power_penalty(5.0, 2.5) > 1.0);
        let mut comp = make_compressor();
        comp.set_volume_index_for_test(4.0);
        comp.set_slide_for_test(0.5);
        assert!((comp.effective_volume_index() - (1.0 + 3.0 * 0.5)).abs() < 1e-12);
    }
}
