//! Drum - Recirculation Drum for Flooded Evaporators
//!
//! This module provides a recirculation drum component that separates a two-phase
//! mixture into saturated liquid and saturated vapor. Used in recirculation
//! evaporator systems to improve heat transfer.
//!
//! ## Physical Description
//!
//! A recirculation drum receives:
//! 1. Feed from economizer (typically subcooled or two-phase)
//! 2. Return from evaporator (enriched two-phase)
//!
//! And separates into:
//! - Saturated liquid (x=0) to the recirculation pump
//! - Saturated vapor (x=1) to the compressor
//!
//! ## Equations (8 total)
//!
//! | # | Equation | Description |
//! |---|----------|-------------|
//! | 1 | `m_liq + m_vap = m_feed + m_return` | Mass balance |
//! | 2 | `m_liq * h_liq + m_vap * h_vap = m_total * h_mixed` | Energy balance |
//! | 3 | `P_liq - P_feed = 0` | Pressure equality (liquid) |
//! | 4 | `P_vap - P_feed = 0` | Pressure equality (vapor) |
//! | 5 | `h_liq - h_sat(P, x=0) = 0` | Saturated liquid |
//! | 6 | `h_vap - h_sat(P, x=1) = 0` | Saturated vapor |
//! | 7-8 | Fluid continuity | Implicit via FluidId |
//!
//! ## Example
//!
//! ```ignore
//! use entropyk_components::Drum;
//! use entropyk_components::port::{Port, FluidId};
//! use entropyk_core::{Pressure, Enthalpy};
//! use entropyk_fluids::CoolPropBackend;
//! use std::sync::Arc;
//!
//! let backend = Arc::new(CoolPropBackend::new());
//!
//! let feed_inlet = Port::new(FluidId::new("R410A"), Pressure::from_bar(10.0), Enthalpy::from_kj_per_kg(250.0));
//! let evaporator_return = Port::new(FluidId::new("R410A"), Pressure::from_bar(10.0), Enthalpy::from_kj_per_kg(350.0));
//! let liquid_outlet = Port::new(FluidId::new("R410A"), Pressure::from_bar(10.0), Enthalpy::from_kj_per_kg(200.0));
//! let vapor_outlet = Port::new(FluidId::new("R410A"), Pressure::from_bar(10.0), Enthalpy::from_kj_per_kg(420.0));
//!
//! let drum = Drum::new("R410A", feed_inlet, evaporator_return, liquid_outlet, vapor_outlet, backend)?;
//! ```

use crate::port::{ConnectedPort, FluidId};
use crate::state_machine::{CircuitId, OperationalState, StateManageable};
use crate::{Component, ComponentError, JacobianBuilder, ResidualVector, StateSlice};
use entropyk_core::{Enthalpy, MassFlow, Power, Pressure};
use entropyk_fluids::{FluidBackend, FluidState, Property, Quality};
use std::sync::Arc;

/// Drum - Recirculation drum for flooded evaporator systems.
///
/// Separates a two-phase mixture (2 inlets) into:
/// - Saturated liquid (x=0) to the recirculation pump
/// - Saturated vapor (x=1) to the compressor
///
/// The drum requires a [`FluidBackend`] to calculate saturation properties.
pub struct Drum {
    /// Fluid identifier (must be pure or pseudo-pure for saturation calculations)
    fluid_id: String,
    /// Feed inlet (from economizer)
    feed_inlet: ConnectedPort,
    /// Evaporator return (two-phase enriched)
    evaporator_return: ConnectedPort,
    /// Liquid outlet (saturated, x=0) to pump
    liquid_outlet: ConnectedPort,
    /// Vapor outlet (saturated, x=1) to compressor
    vapor_outlet: ConnectedPort,
    /// Fluid backend for saturation calculations
    fluid_backend: Arc<dyn FluidBackend>,
    /// Circuit identifier
    circuit_id: CircuitId,
    /// Operational state
    operational_state: OperationalState,

    /// When true, the drum is wired into the (P,h) graph solver and anchors its
    /// two outgoing edges (liquid + vapor) to the design saturation state.
    edge_coupled: bool,
    /// Design saturation temperature (K) used to anchor the drum pressure when
    /// edge-coupled. Mirrors the fixed-saturation philosophy of the EXV.
    t_sat_k: f64,
    /// Liquid outlet edge state indices (set by `set_system_context`).
    liq_p_idx: Option<usize>,
    liq_h_idx: Option<usize>,
    /// Vapor outlet edge state indices (set by `set_system_context`).
    vap_p_idx: Option<usize>,
    vap_h_idx: Option<usize>,
}

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

impl Drum {
    /// Creates a new recirculation drum.
    ///
    /// # Arguments
    ///
    /// * `fluid` - Fluid identifier (e.g., "R410A", "R134a")
    /// * `feed_inlet` - Feed inlet port (from economizer)
    /// * `evaporator_return` - Evaporator return port (two-phase)
    /// * `liquid_outlet` - Liquid outlet port (to pump)
    /// * `vapor_outlet` - Vapor outlet port (to compressor)
    /// * `backend` - Fluid backend for saturation calculations
    ///
    /// # Errors
    ///
    /// Returns an error if ports have incompatible fluids.
    pub fn new(
        fluid: impl Into<String>,
        feed_inlet: ConnectedPort,
        evaporator_return: ConnectedPort,
        liquid_outlet: ConnectedPort,
        vapor_outlet: ConnectedPort,
        backend: Arc<dyn FluidBackend>,
    ) -> Result<Self, ComponentError> {
        let fluid_id = fluid.into();

        Self::validate_fluids(
            &fluid_id,
            &feed_inlet,
            &evaporator_return,
            &liquid_outlet,
            &vapor_outlet,
        )?;

        Ok(Self {
            fluid_id,
            feed_inlet,
            evaporator_return,
            liquid_outlet,
            vapor_outlet,
            fluid_backend: backend,
            circuit_id: CircuitId::default(),
            operational_state: OperationalState::default(),
            edge_coupled: false,
            t_sat_k: 0.0,
            liq_p_idx: None,
            liq_h_idx: None,
            vap_p_idx: None,
            vap_h_idx: None,
        })
    }

    fn validate_fluids(
        expected: &str,
        feed: &ConnectedPort,
        ret: &ConnectedPort,
        liq: &ConnectedPort,
        vap: &ConnectedPort,
    ) -> Result<(), ComponentError> {
        let expected_fluid = FluidId::new(expected);

        if feed.fluid_id() != &expected_fluid {
            return Err(ComponentError::InvalidState(format!(
                "Drum feed_inlet fluid mismatch: expected {}, got {}",
                expected,
                feed.fluid_id().as_str()
            )));
        }
        if ret.fluid_id() != &expected_fluid {
            return Err(ComponentError::InvalidState(format!(
                "Drum evaporator_return fluid mismatch: expected {}, got {}",
                expected,
                ret.fluid_id().as_str()
            )));
        }
        if liq.fluid_id() != &expected_fluid {
            return Err(ComponentError::InvalidState(format!(
                "Drum liquid_outlet fluid mismatch: expected {}, got {}",
                expected,
                liq.fluid_id().as_str()
            )));
        }
        if vap.fluid_id() != &expected_fluid {
            return Err(ComponentError::InvalidState(format!(
                "Drum vapor_outlet fluid mismatch: expected {}, got {}",
                expected,
                vap.fluid_id().as_str()
            )));
        }

        Ok(())
    }

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

    /// Enables (P,h) graph coupling and sets the design saturation temperature (K).
    ///
    /// When enabled, the drum anchors both outgoing edges (liquid + vapor) to the
    /// saturation state at `t_sat_k`, contributing 4 equations (2 per outgoing edge).
    pub fn with_edge_coupling(mut self, t_sat_k: f64) -> Self {
        self.edge_coupled = true;
        self.t_sat_k = t_sat_k;
        self
    }

    /// Returns the feed inlet port.
    pub fn feed_inlet(&self) -> &ConnectedPort {
        &self.feed_inlet
    }

    /// Returns the evaporator return port.
    pub fn evaporator_return(&self) -> &ConnectedPort {
        &self.evaporator_return
    }

    /// Returns the liquid outlet port.
    pub fn liquid_outlet(&self) -> &ConnectedPort {
        &self.liquid_outlet
    }

    /// Returns the vapor outlet port.
    pub fn vapor_outlet(&self) -> &ConnectedPort {
        &self.vapor_outlet
    }

    /// Returns the recirculation ratio (m_liquid / m_feed).
    ///
    /// Requires mass flow information to be available in the state vector.
    /// Returns 0.0 if mass flow cannot be determined (e.g., zero feed flow).
    ///
    /// # Arguments
    ///
    /// * `state` - State vector containing mass flows at indices 0-3:
    ///   - state[0]: m_feed (feed inlet mass flow)
    ///   - state[1]: m_return (evaporator return mass flow)
    ///   - state[2]: m_liq (liquid outlet mass flow, positive = out)
    ///   - state[3]: m_vap (vapor outlet mass flow, positive = out)
    pub fn recirculation_ratio(&self, state: &StateSlice) -> f64 {
        if state.len() < 4 {
            return 0.0;
        }

        let m_feed = state[0];
        let m_liq = state[2]; // Liquid outlet flow (positive = leaving drum)

        if m_feed.abs() < 1e-10 {
            0.0
        } else {
            m_liq / m_feed
        }
    }

    /// Gets saturated liquid enthalpy at a given pressure.
    fn saturated_liquid_enthalpy(&self, pressure_pa: f64) -> Result<f64, ComponentError> {
        let fluid = FluidId::new(&self.fluid_id);
        let state = FluidState::from_px(Pressure::from_pascals(pressure_pa), Quality(0.0));

        self.fluid_backend
            .property(fluid, Property::Enthalpy, state)
            .map_err(|e| {
                ComponentError::CalculationFailed(format!(
                    "Failed to get saturated liquid enthalpy: {}",
                    e
                ))
            })
    }

    /// Gets saturated vapor enthalpy at a given pressure.
    fn saturated_vapor_enthalpy(&self, pressure_pa: f64) -> Result<f64, ComponentError> {
        let fluid = FluidId::new(&self.fluid_id);
        let state = FluidState::from_px(Pressure::from_pascals(pressure_pa), Quality(1.0));

        self.fluid_backend
            .property(fluid, Property::Enthalpy, state)
            .map_err(|e| {
                ComponentError::CalculationFailed(format!(
                    "Failed to get saturated vapor enthalpy: {}",
                    e
                ))
            })
    }
}

impl Clone for Drum {
    fn clone(&self) -> Self {
        Self {
            fluid_id: self.fluid_id.clone(),
            feed_inlet: self.feed_inlet.clone(),
            evaporator_return: self.evaporator_return.clone(),
            liquid_outlet: self.liquid_outlet.clone(),
            vapor_outlet: self.vapor_outlet.clone(),
            fluid_backend: Arc::clone(&self.fluid_backend),
            circuit_id: self.circuit_id,
            operational_state: self.operational_state,
            edge_coupled: self.edge_coupled,
            t_sat_k: self.t_sat_k,
            liq_p_idx: self.liq_p_idx,
            liq_h_idx: self.liq_h_idx,
            vap_p_idx: self.vap_p_idx,
            vap_h_idx: self.vap_h_idx,
        }
    }
}

impl Component for Drum {
    fn set_system_context(
        &mut self,
        _state_offset: usize,
        external_edge_state_indices: &[(usize, usize, usize)],
    ) {
        // Layout: incoming edges first (feed, evaporator_return), then outgoing
        // edges (liquid, vapor) in declaration order. The drum owns only its two
        // outgoing edges → take the last two entries.
        // Triple: (m_idx, p_idx, h_idx)
        let n = external_edge_state_indices.len();
        if n >= 2 {
            let (_liq_m, liq_p, liq_h) = external_edge_state_indices[n - 2];
            let (_vap_m, vap_p, vap_h) = external_edge_state_indices[n - 1];
            self.liq_p_idx = Some(liq_p);
            self.liq_h_idx = Some(liq_h);
            self.vap_p_idx = Some(vap_p);
            self.vap_h_idx = Some(vap_h);
        }
    }

    /// Legacy mass-flow model: 8 equations.
    /// Edge-coupled (P,h) model: 4 equations (2 per outgoing edge: liquid + vapor).
    fn n_equations(&self) -> usize {
        if self.edge_coupled {
            4
        } else {
            8
        }
    }

    fn compute_residuals(
        &self,
        state: &StateSlice,
        residuals: &mut ResidualVector,
    ) -> Result<(), ComponentError> {
        let n_eqs = self.n_equations();
        if residuals.len() < n_eqs {
            return Err(ComponentError::InvalidResidualDimensions {
                expected: n_eqs,
                actual: residuals.len(),
            });
        }

        // Edge-coupled (P,h) model: anchor both outgoing edges to the design
        // saturation state. The drum acts as a pressure anchor (like the EXV),
        // separating into saturated liquid (x=0) and saturated vapor (x=1).
        if self.edge_coupled {
            if let (Some(liq_p), Some(liq_h), Some(vap_p), Some(vap_h)) = (
                self.liq_p_idx,
                self.liq_h_idx,
                self.vap_p_idx,
                self.vap_h_idx,
            ) {
                let fluid = FluidId::new(&self.fluid_id);
                let p_sat = self
                    .fluid_backend
                    .saturation_pressure_t(fluid, self.t_sat_k)
                    .map_err(|e| ComponentError::CalculationFailed(e.to_string()))?;

                let h_sat_l = self.saturated_liquid_enthalpy(p_sat)?;
                let h_sat_v = self.saturated_vapor_enthalpy(p_sat)?;

                // Liquid outlet (x=0)
                residuals[0] = state[liq_p] - p_sat;
                residuals[1] = state[liq_h] - h_sat_l;
                // Vapor outlet (x=1)
                residuals[2] = state[vap_p] - p_sat;
                residuals[3] = state[vap_h] - h_sat_v;
                return Ok(());
            }
            return Err(ComponentError::InvalidState(
                "Drum edge-coupled model requires live liquid and vapor outlet edge indices"
                    .to_string(),
            ));
        }

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

        // State variables:
        // state[0]: m_feed (feed inlet mass flow, kg/s)
        // state[1]: m_return (evaporator return mass flow, kg/s)
        // state[2]: m_liq (liquid outlet mass flow, kg/s, positive = leaving drum)
        // state[3]: m_vap (vapor outlet mass flow, kg/s, positive = leaving drum)

        let m_feed = state[0];
        let m_return = state[1];
        let m_liq = state[2];
        let m_vap = state[3];

        let p_feed = self.feed_inlet.pressure().to_pascals();
        let h_feed = self.feed_inlet.enthalpy().to_joules_per_kg();
        let h_return = self.evaporator_return.enthalpy().to_joules_per_kg();

        let p_liq = self.liquid_outlet.pressure().to_pascals();
        let h_liq = self.liquid_outlet.enthalpy().to_joules_per_kg();

        let p_vap = self.vapor_outlet.pressure().to_pascals();
        let h_vap = self.vapor_outlet.enthalpy().to_joules_per_kg();

        let h_sat_l = self.saturated_liquid_enthalpy(p_feed)?;
        let h_sat_v = self.saturated_vapor_enthalpy(p_feed)?;

        let mut idx = 0;

        // Equation 1: Pressure equality (liquid)
        // P_liq - P_feed = 0
        residuals[idx] = p_liq - p_feed;
        idx += 1;

        // Equation 2: Pressure equality (vapor)
        // P_vap - P_feed = 0
        residuals[idx] = p_vap - p_feed;
        idx += 1;

        // Equation 3: Saturated liquid constraint
        // h_liq - h_sat(P, x=0) = 0
        residuals[idx] = h_liq - h_sat_l;
        idx += 1;

        // Equation 4: Saturated vapor constraint
        // h_vap - h_sat(P, x=1) = 0
        residuals[idx] = h_vap - h_sat_v;
        idx += 1;

        // Equation 5: Mass balance
        // m_liq + m_vap = m_feed + m_return
        // Residual: (m_liq + m_vap) - (m_feed + m_return) = 0
        residuals[idx] = (m_liq + m_vap) - (m_feed + m_return);
        idx += 1;

        // Equation 6: Energy balance
        // m_liq * h_liq + m_vap * h_vap = m_feed * h_feed + m_return * h_return
        // Residual: (m_liq * h_liq + m_vap * h_vap) - (m_feed * h_feed + m_return * h_return) = 0
        let energy_out = m_liq * h_liq + m_vap * h_vap;
        let energy_in = m_feed * h_feed + m_return * h_return;
        residuals[idx] = energy_out - energy_in;
        idx += 1;

        // Equations 7-8: Fluid continuity (implicit, enforced by using same fluid_id)
        // These are satisfied by construction since all ports use the same fluid
        residuals[idx] = 0.0;
        idx += 1;
        residuals[idx] = 0.0;

        Ok(())
    }

    fn jacobian_entries(
        &self,
        _state: &StateSlice,
        jacobian: &mut JacobianBuilder,
    ) -> Result<(), ComponentError> {
        if self.edge_coupled {
            if let (Some(liq_p), Some(liq_h), Some(vap_p), Some(vap_h)) = (
                self.liq_p_idx,
                self.liq_h_idx,
                self.vap_p_idx,
                self.vap_h_idx,
            ) {
                // Saturation state is fixed by t_sat_k → derivatives are diagonal.
                jacobian.add_entry(0, liq_p, 1.0);
                jacobian.add_entry(1, liq_h, 1.0);
                jacobian.add_entry(2, vap_p, 1.0);
                jacobian.add_entry(3, vap_h, 1.0);
            }
            return Ok(());
        }
        let n_eqs = self.n_equations();
        for i in 0..n_eqs {
            jacobian.add_entry(i, i, 1.0);
        }
        Ok(())
    }

    fn get_ports(&self) -> &[ConnectedPort] {
        // Note: This is a temporary implementation that returns an empty slice.
        // To properly return the ports, we would need to store them in a Vec
        // or use a different approach. For now, we document the ports here:
        // - Port 0: feed_inlet (from economizer)
        // - Port 1: evaporator_return (two-phase enriched)
        // - Port 2: liquid_outlet (to pump)
        // - Port 3: vapor_outlet (to compressor)
        &[]
    }

    fn energy_transfers(&self, _state: &StateSlice) -> Option<(Power, Power)> {
        Some((Power::from_watts(0.0), Power::from_watts(0.0)))
    }

    fn port_mass_flows(&self, state: &StateSlice) -> Result<Vec<MassFlow>, ComponentError> {
        if state.len() < 4 {
            return Err(ComponentError::InvalidStateDimensions {
                expected: 4,
                actual: state.len(),
            });
        }

        Ok(vec![
            MassFlow::from_kg_per_s(state[0]),
            MassFlow::from_kg_per_s(state[1]),
            MassFlow::from_kg_per_s(-state[2]),
            MassFlow::from_kg_per_s(-state[3]),
        ])
    }

    fn port_enthalpies(&self, _state: &StateSlice) -> Result<Vec<Enthalpy>, ComponentError> {
        Ok(vec![
            self.feed_inlet.enthalpy(),
            self.evaporator_return.enthalpy(),
            self.liquid_outlet.enthalpy(),
            self.vapor_outlet.enthalpy(),
        ])
    }

    fn signature(&self) -> String {
        format!("Drum({})", self.fluid_id)
    }

    fn to_params(&self) -> crate::ComponentParams {
        crate::ComponentParams::new("Drum")
            .with_param("fluid", self.fluid_id.as_str())
            .with_param("circuitId", self.circuit_id.0)
    }

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

impl StateManageable for Drum {
    fn state(&self) -> OperationalState {
        self.operational_state
    }

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

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

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

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

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

    fn create_connected_port(fluid: &str, pressure_pa: f64, enthalpy_j_kg: f64) -> ConnectedPort {
        let p1 = Port::new(
            FluidId::new(fluid),
            Pressure::from_pascals(pressure_pa),
            Enthalpy::from_joules_per_kg(enthalpy_j_kg),
        );
        let p2 = Port::new(
            FluidId::new(fluid),
            Pressure::from_pascals(pressure_pa),
            Enthalpy::from_joules_per_kg(enthalpy_j_kg),
        );
        let (c1, _c2) = p1.connect(p2).expect("ports should connect");
        c1
    }

    fn create_test_drum() -> Drum {
        let backend = Arc::new(entropyk_fluids::TestBackend::new());

        let feed_inlet = create_connected_port("R410A", 1_000_000.0, 250_000.0);
        let evaporator_return = create_connected_port("R410A", 1_000_000.0, 350_000.0);
        let liquid_outlet = create_connected_port("R410A", 1_000_000.0, 200_000.0);
        let vapor_outlet = create_connected_port("R410A", 1_000_000.0, 400_000.0);

        Drum::new(
            "R410A",
            feed_inlet,
            evaporator_return,
            liquid_outlet,
            vapor_outlet,
            backend,
        )
        .expect("drum should be created")
    }

    #[test]
    fn test_drum_equations_count() {
        let drum = create_test_drum();
        assert_eq!(drum.n_equations(), 8);
    }

    #[test]
    fn test_drum_fluid_id() {
        let drum = create_test_drum();
        assert_eq!(drum.fluid_id(), "R410A");
    }

    #[test]
    fn test_drum_energy_transfers() {
        let drum = create_test_drum();
        let state: Vec<f64> = vec![];
        let (heat, work) = drum.energy_transfers(&state).unwrap();
        assert_eq!(heat.to_watts(), 0.0);
        assert_eq!(work.to_watts(), 0.0);
    }

    #[test]
    fn test_drum_state_manageable() {
        let drum = create_test_drum();
        assert_eq!(drum.state(), OperationalState::On);
        assert!(drum.can_transition_to(OperationalState::Off));
        assert!(drum.can_transition_to(OperationalState::Bypass));
    }

    #[test]
    fn test_drum_compute_residuals() {
        let drum = create_test_drum();
        let state: Vec<f64> = vec![0.1, 0.2, 0.15, 0.05];
        let mut residuals = vec![0.0; 8];

        let result = drum.compute_residuals(&state, &mut residuals);

        // TestBackend now supports P-x queries for R410A via saturation tables,
        // so compute_residuals should succeed and produce finite residuals.
        assert!(
            result.is_ok(),
            "compute_residuals should succeed: {:?}",
            result
        );
        for (i, &r) in residuals.iter().enumerate() {
            assert!(r.is_finite(), "residual[{}] should be finite, got {}", i, r);
        }
    }

    #[test]
    fn test_drum_jacobian_entries() {
        let drum = create_test_drum();
        let state: Vec<f64> = vec![];
        let mut jacobian = JacobianBuilder::new();

        let result = drum.jacobian_entries(&state, &mut jacobian);
        assert!(result.is_ok());
        assert_eq!(jacobian.len(), 8);
    }

    #[test]
    fn test_drum_invalid_state_dimensions() {
        let drum = create_test_drum();
        let state: Vec<f64> = vec![];
        let mut residuals = vec![0.0; 4];

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

    #[test]
    fn test_drum_port_mass_flows() {
        let drum = create_test_drum();
        let state: Vec<f64> = vec![0.1, 0.2, 0.15, 0.05];

        let flows = drum.port_mass_flows(&state).unwrap();
        assert_eq!(flows.len(), 4);
        assert!((flows[0].to_kg_per_s() - 0.1).abs() < 1e-10);
        assert!((flows[1].to_kg_per_s() - 0.2).abs() < 1e-10);
        assert!((flows[2].to_kg_per_s() - (-0.15)).abs() < 1e-10);
        assert!((flows[3].to_kg_per_s() - (-0.05)).abs() < 1e-10);
    }

    #[test]
    fn test_drum_port_enthalpies() {
        let drum = create_test_drum();
        let state: Vec<f64> = vec![];

        let enthalpies = drum.port_enthalpies(&state).unwrap();
        assert_eq!(enthalpies.len(), 4);
        assert!((enthalpies[0].to_joules_per_kg() - 250_000.0).abs() < 1e-10);
        assert!((enthalpies[1].to_joules_per_kg() - 350_000.0).abs() < 1e-10);
        assert!((enthalpies[2].to_joules_per_kg() - 200_000.0).abs() < 1e-10);
        assert!((enthalpies[3].to_joules_per_kg() - 400_000.0).abs() < 1e-10);
    }

    #[test]
    fn test_drum_signature() {
        let drum = create_test_drum();
        assert_eq!(drum.signature(), "Drum(R410A)");
    }

    #[test]
    fn test_drum_fluid_mismatch() {
        let backend = Arc::new(entropyk_fluids::TestBackend::new());

        let feed_inlet = create_connected_port("R410A", 1_000_000.0, 250_000.0);
        let evaporator_return = create_connected_port("R134a", 1_000_000.0, 350_000.0);
        let liquid_outlet = create_connected_port("R410A", 1_000_000.0, 200_000.0);
        let vapor_outlet = create_connected_port("R410A", 1_000_000.0, 400_000.0);

        let result = Drum::new(
            "R410A",
            feed_inlet,
            evaporator_return,
            liquid_outlet,
            vapor_outlet,
            backend,
        );

        assert!(result.is_err());
    }

    #[test]
    fn test_drum_state_transition() {
        let mut drum = create_test_drum();

        assert!(drum.set_state(OperationalState::Off).is_ok());
        assert_eq!(drum.state(), OperationalState::Off);

        assert!(drum.set_state(OperationalState::Bypass).is_ok());
        assert_eq!(drum.state(), OperationalState::Bypass);
    }

    #[test]
    fn test_drum_circuit_id() {
        let mut drum = create_test_drum();
        let new_id = CircuitId::from_number(42);
        drum.set_circuit_id(new_id);
        assert_eq!(drum.circuit_id(), &new_id);
    }

    #[test]
    fn test_drum_clone() {
        let drum = create_test_drum();
        let cloned = drum.clone();
        assert_eq!(drum.fluid_id(), cloned.fluid_id());
        assert_eq!(drum.n_equations(), cloned.n_equations());
    }

    #[test]
    fn test_drum_debug() {
        let drum = create_test_drum();
        let debug_str = format!("{:?}", drum);
        assert!(debug_str.contains("Drum"));
        assert!(debug_str.contains("R410A"));
    }

    #[test]
    fn test_drum_recirculation_ratio_basic() {
        let drum = create_test_drum();
        // state[0] = m_feed, state[2] = m_liq
        // ratio = m_liq / m_feed
        let state = vec![0.1, 0.2, 0.25, 0.05]; // m_feed=0.1, m_liq=0.25
        let ratio = drum.recirculation_ratio(&state);
        assert!(
            (ratio - 2.5).abs() < 1e-10,
            "Expected ratio 2.5, got {}",
            ratio
        );
    }

    #[test]
    fn test_drum_recirculation_ratio_zero_feed() {
        let drum = create_test_drum();
        // Zero feed flow should return 0.0 (avoid division by zero)
        let state = vec![0.0, 0.2, 0.15, 0.05];
        let ratio = drum.recirculation_ratio(&state);
        assert_eq!(ratio, 0.0, "Expected ratio 0.0 for zero feed flow");
    }

    #[test]
    fn test_drum_recirculation_ratio_small_feed() {
        let drum = create_test_drum();
        // Very small feed flow should return 0.0 (avoid numerical issues)
        let state = vec![1e-12, 0.2, 0.15, 0.05];
        let ratio = drum.recirculation_ratio(&state);
        assert_eq!(ratio, 0.0, "Expected ratio 0.0 for very small feed flow");
    }

    #[test]
    fn test_drum_recirculation_ratio_empty_state() {
        let drum = create_test_drum();
        // Empty state should return 0.0
        let state: Vec<f64> = vec![];
        let ratio = drum.recirculation_ratio(&state);
        assert_eq!(ratio, 0.0, "Expected ratio 0.0 for empty state");
    }

    #[test]
    fn test_drum_recirculation_ratio_insufficient_state() {
        let drum = create_test_drum();
        // State with less than 4 elements should return 0.0
        let state = vec![0.1, 0.2, 0.15]; // Only 3 elements
        let ratio = drum.recirculation_ratio(&state);
        assert_eq!(ratio, 0.0, "Expected ratio 0.0 for insufficient state");
    }

    #[test]
    fn test_drum_recirculation_ratio_unity() {
        let drum = create_test_drum();
        // When m_liq = m_feed, ratio should be 1.0
        let state = vec![0.1, 0.1, 0.1, 0.1]; // m_feed=0.1, m_liq=0.1
        let ratio = drum.recirculation_ratio(&state);
        assert!(
            (ratio - 1.0).abs() < 1e-10,
            "Expected ratio 1.0, got {}",
            ratio
        );
    }
}
