//! Brine Boundary Condition Components
//!
//! This module provides [`BrineSource`] and [`BrineSink`] components for
//! water-glycol (brine) heat transfer circuits with native glycol concentration
//! support via the [`Concentration`](entropyk_core::Concentration) NewType.
//!
//! ## Design
//!
//! Unlike the generic [`FlowSource`](crate::FlowSource)/[`FlowSink`](crate::FlowSink)
//! which use (Pressure, Enthalpy), these components use (Pressure, Temperature,
//! Concentration) for type-safe brine state specification.
//!
//! ## Equations
//!
//! **BrineSource** (always 2):
//! ```text
//! r₀ = P_edge − P_set = 0
//! r₁ = h_edge − h(P_set, T_set, c) = 0
//! ```
//!
//! **BrineSink** (1 or 2 depending on whether temperature is set):
//! ```text
//! r₀ = P_edge − P_back = 0                     (always)
//! r₁ = h_edge − h(P_back, T_back, c) = 0       (only when temperature is set)
//! ```
//!
//! ## Example
//!
//! ```ignore
//! use entropyk_components::{BrineSource, BrineSink};
//! use entropyk_core::{Pressure, Temperature, Concentration};
//! use std::sync::Arc;
//!
//! let backend = Arc::new(coolprop_backend);
//!
//! // Evaporator brine outlet: 3 bar, 20 °C, 30% MEG
//! let source = BrineSource::new(
//!     "MEG",
//!     Pressure::from_bar(3.0),
//!     Temperature::from_celsius(20.0),
//!     Concentration::from_percent(30.0),
//!     backend.clone(),
//!     outlet_port,
//! ).unwrap();
//!
//! // Free-enthalpy sink: only back-pressure constrained
//! let sink = BrineSink::new(
//!     "MEG",
//!     Pressure::from_bar(2.0),
//!     None,
//!     None,
//!     backend,
//!     inlet_port,
//! ).unwrap();
//! ```

use crate::flow_junction::is_incompressible;
use crate::{
    Component, ComponentError, ConnectedPort, JacobianBuilder, ResidualVector, StateSlice,
};
use entropyk_core::{Concentration, Enthalpy, MassFlow, Power, Pressure, Temperature};
use entropyk_fluids::{FluidBackend, FluidId, FluidState, Property};
use std::sync::Arc;

fn pt_concentration_to_enthalpy(
    backend: &dyn FluidBackend,
    fluid: &str,
    p: Pressure,
    t: Temperature,
    concentration: Concentration,
) -> Result<Enthalpy, ComponentError> {
    // Encode the glycol concentration into the fluid name using CoolProp's
    // incompressible mixture syntax: "INCOMP::MEG-30" for 30% MEG by mass.
    // Pure water (concentration ≈ 0) is passed as-is.
    let fluid_with_conc = if concentration.to_fraction() < 1e-10 {
        fluid.to_string()
    } else {
        format!("INCOMP::{}-{:.0}", fluid, concentration.to_percent())
    };

    let fluid_id = FluidId::new(&fluid_with_conc);
    let state = FluidState::from_pt(p, t);

    backend
        .property(fluid_id, Property::Enthalpy, state)
        .map(Enthalpy::from_joules_per_kg)
        .map_err(|e| {
            ComponentError::CalculationFailed(format!(
                "P-T-Concentration to enthalpy (fluid='{}', c={:.1}%): {}",
                fluid_with_conc,
                concentration.to_percent(),
                e
            ))
        })
}

/// A boundary source that imposes fixed pressure, temperature and glycol concentration
/// on its outlet edge.
///
/// Contributes **2 equations** to the system:
/// - `r₀ = P_edge − P_set = 0`
/// - `r₁ = h_edge − h(P_set, T_set, c) = 0`
///
/// Only accepts incompressible fluids (MEG, PEG, Water, Glycol, Brine).
/// Use [`RefrigerantSource`](crate::RefrigerantSource) for refrigerant circuits.
pub struct BrineSource {
    fluid_id: String,
    p_set_pa: f64,
    t_set_k: f64,
    concentration: Concentration,
    h_set_jkg: f64,
    backend: Arc<dyn FluidBackend>,
    outlet: ConnectedPort,
}

impl BrineSource {
    /// Creates a new `BrineSource`.
    ///
    /// # Arguments
    ///
    /// * `fluid`         — Fluid name accepted by the backend (e.g. `"MEG"`, `"Water"`)
    /// * `p_set`         — Set-point pressure
    /// * `t_set`         — Set-point temperature
    /// * `concentration` — Glycol mass fraction; `Concentration::from_percent(30.0)` = 30 % MEG
    /// * `backend`       — Shared fluid property backend
    /// * `outlet`        — Already-connected outlet port
    ///
    /// # Errors
    ///
    /// Returns [`ComponentError::InvalidState`] if:
    /// - `fluid` is not an incompressible brine fluid
    /// - `p_set` ≤ 0
    /// - The backend fails to evaluate enthalpy at (P, T, c)
    pub fn new(
        fluid: impl Into<String>,
        p_set: Pressure,
        t_set: Temperature,
        concentration: Concentration,
        backend: Arc<dyn FluidBackend>,
        outlet: ConnectedPort,
    ) -> Result<Self, ComponentError> {
        let fluid = fluid.into();

        if !is_incompressible(&fluid) {
            return Err(ComponentError::InvalidState(format!(
                "BrineSource: '{}' is not an incompressible fluid. Use RefrigerantSource instead.",
                fluid
            )));
        }

        let p_set_pa = p_set.to_pascals();
        if p_set_pa <= 0.0 {
            return Err(ComponentError::InvalidState(
                "BrineSource: set-point pressure must be positive".into(),
            ));
        }

        let h_set =
            pt_concentration_to_enthalpy(backend.as_ref(), &fluid, p_set, t_set, concentration)?;

        Ok(Self {
            fluid_id: fluid,
            p_set_pa,
            t_set_k: t_set.to_kelvin(),
            concentration,
            h_set_jkg: h_set.to_joules_per_kg(),
            backend,
            outlet,
        })
    }

    /// Returns the fluid identifier string (e.g. `"MEG"`, `"Water"`).
    pub fn fluid_id(&self) -> &str {
        &self.fluid_id
    }

    /// Returns the set-point pressure.
    pub fn p_set(&self) -> Pressure {
        Pressure::from_pascals(self.p_set_pa)
    }

    /// Returns the set-point temperature.
    pub fn t_set(&self) -> Temperature {
        Temperature::from_kelvin(self.t_set_k)
    }

    /// Returns the glycol concentration.
    pub fn concentration(&self) -> Concentration {
        self.concentration
    }

    /// Returns the pre-computed set-point enthalpy.
    pub fn h_set(&self) -> Enthalpy {
        Enthalpy::from_joules_per_kg(self.h_set_jkg)
    }

    /// Returns a reference to the outlet connected port.
    pub fn outlet(&self) -> &ConnectedPort {
        &self.outlet
    }

    /// Updates the set-point pressure and recomputes the cached enthalpy.
    ///
    /// # Errors
    ///
    /// Returns [`ComponentError::InvalidState`] if `p` ≤ 0, or
    /// [`ComponentError::CalculationFailed`] if backend evaluation fails.
    pub fn set_pressure(&mut self, p: Pressure) -> Result<(), ComponentError> {
        let p_pa = p.to_pascals();
        if p_pa <= 0.0 {
            return Err(ComponentError::InvalidState(
                "BrineSource: pressure must be positive".into(),
            ));
        }
        self.p_set_pa = p_pa;
        self.recompute_enthalpy()
    }

    /// Updates the set-point temperature and recomputes the cached enthalpy.
    ///
    /// # Errors
    ///
    /// Returns [`ComponentError::CalculationFailed`] if backend evaluation fails.
    pub fn set_temperature(&mut self, t: Temperature) -> Result<(), ComponentError> {
        self.t_set_k = t.to_kelvin();
        self.recompute_enthalpy()
    }

    /// Updates the glycol concentration and recomputes the cached enthalpy.
    ///
    /// # Errors
    ///
    /// Returns [`ComponentError::CalculationFailed`] if backend evaluation fails.
    pub fn set_concentration(
        &mut self,
        concentration: Concentration,
    ) -> Result<(), ComponentError> {
        self.concentration = concentration;
        self.recompute_enthalpy()
    }

    fn recompute_enthalpy(&mut self) -> Result<(), ComponentError> {
        let h_set = pt_concentration_to_enthalpy(
            self.backend.as_ref(),
            &self.fluid_id,
            Pressure::from_pascals(self.p_set_pa),
            Temperature::from_kelvin(self.t_set_k),
            self.concentration,
        )?;
        self.h_set_jkg = h_set.to_joules_per_kg();
        Ok(())
    }
}

impl Component for BrineSource {
    fn n_equations(&self) -> usize {
        2
    }

    fn compute_residuals(
        &self,
        _state: &StateSlice,
        residuals: &mut ResidualVector,
    ) -> Result<(), ComponentError> {
        if residuals.len() < 2 {
            return Err(ComponentError::InvalidResidualDimensions {
                expected: 2,
                actual: residuals.len(),
            });
        }
        residuals[0] = self.outlet.pressure().to_pascals() - self.p_set_pa;
        residuals[1] = self.outlet.enthalpy().to_joules_per_kg() - self.h_set_jkg;
        Ok(())
    }

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

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

    fn port_mass_flows(&self, _state: &StateSlice) -> Result<Vec<MassFlow>, ComponentError> {
        Ok(vec![MassFlow::from_kg_per_s(0.0)])
    }

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

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

    fn signature(&self) -> String {
        format!(
            "BrineSource({}:P={:.0}Pa,T={:.1}K,c={:.0}%)",
            self.fluid_id,
            self.p_set_pa,
            self.t_set_k,
            self.concentration.to_percent()
        )
    }
}

/// A boundary sink that imposes back-pressure, and optionally a fixed enthalpy via
/// (temperature, concentration) on its inlet edge.
///
/// **Equation count is dynamic:**
/// - 1 equation (free enthalpy): `r₀ = P_edge − P_back = 0`
/// - 2 equations (temperature set): additionally `r₁ = h_edge − h(P_back, T_back, c) = 0`
///
/// Toggle with [`set_temperature`](Self::set_temperature) /
/// [`clear_temperature`](Self::clear_temperature) without rebuilding the component.
pub struct BrineSink {
    fluid_id: String,
    p_back_pa: f64,
    t_opt_k: Option<f64>,
    concentration_opt: Option<Concentration>,
    h_back_jkg: Option<f64>,
    backend: Arc<dyn FluidBackend>,
    inlet: ConnectedPort,
}

impl BrineSink {
    /// Creates a new `BrineSink`.
    ///
    /// # Arguments
    ///
    /// * `fluid`             — Fluid name accepted by the backend (e.g. `"MEG"`, `"Water"`)
    /// * `p_back`            — Back-pressure imposed on the inlet edge
    /// * `t_opt`             — Optional set-point temperature; `None` = free enthalpy (1 equation)
    /// * `concentration_opt` — Required when `t_opt` is `Some`; ignored otherwise
    /// * `backend`           — Shared fluid property backend
    /// * `inlet`             — Already-connected inlet port
    ///
    /// # Errors
    ///
    /// Returns [`ComponentError::InvalidState`] if:
    /// - `fluid` is not an incompressible brine fluid
    /// - `p_back` ≤ 0
    /// - `t_opt` is `Some` but `concentration_opt` is `None`
    /// - The backend fails to evaluate enthalpy
    pub fn new(
        fluid: impl Into<String>,
        p_back: Pressure,
        t_opt: Option<Temperature>,
        concentration_opt: Option<Concentration>,
        backend: Arc<dyn FluidBackend>,
        inlet: ConnectedPort,
    ) -> Result<Self, ComponentError> {
        let fluid = fluid.into();

        if !is_incompressible(&fluid) {
            return Err(ComponentError::InvalidState(format!(
                "BrineSink: '{}' is not an incompressible fluid. Use RefrigerantSink instead.",
                fluid
            )));
        }

        let p_back_pa = p_back.to_pascals();
        if p_back_pa <= 0.0 {
            return Err(ComponentError::InvalidState(
                "BrineSink: back-pressure must be positive".into(),
            ));
        }

        if t_opt.is_some() && concentration_opt.is_none() {
            return Err(ComponentError::InvalidState(
                "BrineSink: concentration must be specified when temperature is set".into(),
            ));
        }

        let (t_opt_k, h_back_jkg) = if let (Some(t), Some(c)) = (t_opt, concentration_opt) {
            let h = pt_concentration_to_enthalpy(backend.as_ref(), &fluid, p_back, t, c)?;
            (Some(t.to_kelvin()), Some(h.to_joules_per_kg()))
        } else {
            (None, None)
        };

        Ok(Self {
            fluid_id: fluid,
            p_back_pa,
            t_opt_k,
            concentration_opt,
            h_back_jkg,
            backend,
            inlet,
        })
    }

    /// Returns the fluid identifier string (e.g. `"MEG"`, `"Water"`).
    pub fn fluid_id(&self) -> &str {
        &self.fluid_id
    }

    /// Returns the back-pressure.
    pub fn p_back(&self) -> Pressure {
        Pressure::from_pascals(self.p_back_pa)
    }

    /// Returns the optional set-point temperature.
    pub fn t_opt(&self) -> Option<Temperature> {
        self.t_opt_k.map(Temperature::from_kelvin)
    }

    /// Returns the optional glycol concentration.
    pub fn concentration(&self) -> Option<Concentration> {
        self.concentration_opt
    }

    /// Returns the optional pre-computed back enthalpy.
    pub fn h_back(&self) -> Option<Enthalpy> {
        self.h_back_jkg.map(Enthalpy::from_joules_per_kg)
    }

    /// Returns a reference to the inlet connected port.
    pub fn inlet(&self) -> &ConnectedPort {
        &self.inlet
    }

    /// Updates the back-pressure and recomputes enthalpy if temperature is set.
    ///
    /// # Errors
    ///
    /// Returns [`ComponentError::InvalidState`] if `p` ≤ 0, or
    /// [`ComponentError::CalculationFailed`] if backend evaluation fails.
    pub fn set_pressure(&mut self, p: Pressure) -> Result<(), ComponentError> {
        let p_pa = p.to_pascals();
        if p_pa <= 0.0 {
            return Err(ComponentError::InvalidState(
                "BrineSink: back-pressure must be positive".into(),
            ));
        }
        self.p_back_pa = p_pa;
        if let (Some(t_k), Some(c)) = (self.t_opt_k, self.concentration_opt) {
            self.h_back_jkg = Some(
                pt_concentration_to_enthalpy(
                    self.backend.as_ref(),
                    &self.fluid_id,
                    p,
                    Temperature::from_kelvin(t_k),
                    c,
                )?
                .to_joules_per_kg(),
            );
        }
        Ok(())
    }

    /// Sets a temperature (and required concentration) constraint, switching the sink to
    /// 2-equation mode.  Recomputes the back enthalpy from (P, T, c).
    ///
    /// # Errors
    ///
    /// Returns [`ComponentError::CalculationFailed`] if backend evaluation fails.
    pub fn set_temperature(
        &mut self,
        t: Temperature,
        concentration: Concentration,
    ) -> Result<(), ComponentError> {
        self.t_opt_k = Some(t.to_kelvin());
        self.concentration_opt = Some(concentration);
        self.h_back_jkg = Some(
            pt_concentration_to_enthalpy(
                self.backend.as_ref(),
                &self.fluid_id,
                Pressure::from_pascals(self.p_back_pa),
                t,
                concentration,
            )?
            .to_joules_per_kg(),
        );
        Ok(())
    }

    /// Removes the temperature constraint, switching the sink back to 1-equation mode
    /// (free enthalpy).
    pub fn clear_temperature(&mut self) {
        self.t_opt_k = None;
        self.concentration_opt = None;
        self.h_back_jkg = None;
    }
}

impl Component for BrineSink {
    fn n_equations(&self) -> usize {
        if self.h_back_jkg.is_some() {
            2
        } else {
            1
        }
    }

    fn compute_residuals(
        &self,
        _state: &StateSlice,
        residuals: &mut ResidualVector,
    ) -> Result<(), ComponentError> {
        let n = self.n_equations();
        if residuals.len() < n {
            return Err(ComponentError::InvalidResidualDimensions {
                expected: n,
                actual: residuals.len(),
            });
        }
        residuals[0] = self.inlet.pressure().to_pascals() - self.p_back_pa;
        if let Some(h_back) = self.h_back_jkg {
            residuals[1] = self.inlet.enthalpy().to_joules_per_kg() - h_back;
        }
        Ok(())
    }

    fn jacobian_entries(
        &self,
        _state: &StateSlice,
        jacobian: &mut JacobianBuilder,
    ) -> Result<(), ComponentError> {
        let n = self.n_equations();
        for i in 0..n {
            jacobian.add_entry(i, i, 1.0);
        }
        Ok(())
    }

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

    fn port_mass_flows(&self, _state: &StateSlice) -> Result<Vec<MassFlow>, ComponentError> {
        Ok(vec![MassFlow::from_kg_per_s(0.0)])
    }

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

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

    fn signature(&self) -> String {
        match self.t_opt_k {
            Some(t_k) => format!(
                "BrineSink({}:P={:.0}Pa,T={:.1}K,c={:.0}%)",
                self.fluid_id,
                self.p_back_pa,
                t_k,
                self.concentration_opt.map_or(0.0, |c| c.to_percent())
            ),
            None => format!(
                "BrineSink({}:P={:.0}Pa,T=free)",
                self.fluid_id, self.p_back_pa
            ),
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::port::{FluidId, Port};
    use entropyk_fluids::{
        CriticalPoint, FluidBackend, FluidError, FluidResult, FluidState, Phase, Property,
    };

    struct MockBrineBackend;

    impl MockBrineBackend {
        fn new() -> Self {
            Self
        }
    }

    impl FluidBackend for MockBrineBackend {
        fn property(
            &self,
            _fluid: FluidId,
            property: Property,
            state: FluidState,
        ) -> FluidResult<f64> {
            match state {
                FluidState::PressureTemperature(p, t) => {
                    let _p_pa = p.to_pascals();
                    let t_k = t.to_kelvin();
                    match property {
                        Property::Enthalpy => {
                            let h = 3500.0 * (t_k - 273.15);
                            Ok(h)
                        }
                        Property::Temperature => Ok(t_k),
                        Property::Pressure => Ok(_p_pa),
                        _ => Err(FluidError::UnsupportedProperty {
                            property: property.to_string(),
                        }),
                    }
                }
                _ => Err(FluidError::InvalidState {
                    reason: "MockBrineBackend only supports P-T state".to_string(),
                }),
            }
        }

        fn critical_point(&self, _fluid: FluidId) -> FluidResult<CriticalPoint> {
            Ok(CriticalPoint::new(
                Temperature::from_kelvin(373.0),
                Pressure::from_pascals(22.06e6),
                322.0,
            ))
        }

        fn is_fluid_available(&self, fluid: &FluidId) -> bool {
            let s = fluid.as_str();
            s == "Water" || s == "Glycol" || s == "MEG" || s == "PEG" || s.starts_with("INCOMP::")
        }

        fn phase(&self, _fluid: FluidId, _state: FluidState) -> FluidResult<Phase> {
            Ok(Phase::Liquid)
        }

        fn full_state(
            &self,
            _fluid: FluidId,
            _p: Pressure,
            _h: entropyk_core::Enthalpy,
        ) -> FluidResult<entropyk_fluids::ThermoState> {
            Err(FluidError::UnsupportedProperty {
                property: "full_state".to_string(),
            })
        }

        fn list_fluids(&self) -> Vec<FluidId> {
            vec![
                FluidId::new("Glycol"),
                FluidId::new("MEG"),
                FluidId::new("PEG"),
                FluidId::new("Water"),
            ]
        }
    }

    fn make_port(fluid: &str, p_pa: f64, h_jkg: f64) -> ConnectedPort {
        let a = Port::new(
            FluidId::new(fluid),
            Pressure::from_pascals(p_pa),
            Enthalpy::from_joules_per_kg(h_jkg),
        );
        let b = Port::new(
            FluidId::new(fluid),
            Pressure::from_pascals(p_pa),
            Enthalpy::from_joules_per_kg(h_jkg),
        );
        a.connect(b).unwrap().0
    }

    #[test]
    fn test_brine_source_creation() {
        let backend = Arc::new(MockBrineBackend::new());
        let port = make_port("Glycol", 3.0e5, 70_000.0);
        let source = BrineSource::new(
            "Glycol",
            Pressure::from_pascals(3.0e5),
            Temperature::from_celsius(20.0),
            Concentration::from_percent(30.0),
            backend,
            port,
        )
        .unwrap();

        assert_eq!(source.n_equations(), 2);
        assert_eq!(source.fluid_id(), "Glycol");
    }

    #[test]
    fn test_brine_source_rejects_refrigerant() {
        let backend = Arc::new(MockBrineBackend::new());
        let port = make_port("R410A", 8.5e5, 260_000.0);
        let result = BrineSource::new(
            "R410A",
            Pressure::from_pascals(8.5e5),
            Temperature::from_celsius(10.0),
            Concentration::from_percent(30.0),
            backend,
            port,
        );

        assert!(result.is_err());
    }

    #[test]
    fn test_brine_sink_creation() {
        let backend = Arc::new(MockBrineBackend::new());
        let port = make_port("Glycol", 2.0e5, 60_000.0);
        let sink = BrineSink::new(
            "Glycol",
            Pressure::from_pascals(2.0e5),
            None,
            None,
            backend,
            port,
        )
        .unwrap();

        assert_eq!(sink.n_equations(), 1);
        assert_eq!(sink.fluid_id(), "Glycol");
    }

    #[test]
    fn test_brine_sink_dynamic_toggle() {
        let backend = Arc::new(MockBrineBackend::new());
        let port = make_port("Glycol", 2.0e5, 60_000.0);
        let mut sink = BrineSink::new(
            "Glycol",
            Pressure::from_pascals(2.0e5),
            None,
            None,
            backend,
            port,
        )
        .unwrap();

        assert_eq!(sink.n_equations(), 1);

        sink.set_temperature(
            Temperature::from_celsius(15.0),
            Concentration::from_percent(30.0),
        )
        .unwrap();
        assert_eq!(sink.n_equations(), 2);

        sink.clear_temperature();
        assert_eq!(sink.n_equations(), 1);
    }

    // Task 4.3 — Residual validation: residuals must be zero at set-point.
    #[test]
    fn test_brine_source_residuals_zero_at_setpoint() {
        let p_pa = 3.0e5_f64;
        let t_c = 20.0_f64;
        let backend = Arc::new(MockBrineBackend::new());

        // The mock computes h = 3500 * (T_celsius) J/kg
        let h_expected = 3500.0 * t_c;
        let port = make_port("Glycol", p_pa, h_expected);

        let source = BrineSource::new(
            "Glycol",
            Pressure::from_pascals(p_pa),
            Temperature::from_celsius(t_c),
            Concentration::from_percent(30.0),
            backend,
            port,
        )
        .unwrap();

        let state: Vec<f64> = vec![];
        let mut residuals = vec![0.0_f64; 2];
        source.compute_residuals(&state, &mut residuals).unwrap();

        // Port is initialised at set-point → residuals must be zero
        assert!(
            residuals[0].abs() < 1e-6,
            "Pressure residual should be zero at set-point, got {}",
            residuals[0]
        );
        assert!(
            residuals[1].abs() < 1e-6,
            "Enthalpy residual should be zero at set-point, got {}",
            residuals[1]
        );
    }

    #[test]
    fn test_brine_sink_residuals_zero_at_setpoint() {
        let p_pa = 2.0e5_f64;
        let t_c = 15.0_f64;
        let backend = Arc::new(MockBrineBackend::new());

        let h_expected = 3500.0 * t_c;
        let port = make_port("Glycol", p_pa, h_expected);

        let mut sink = BrineSink::new(
            "Glycol",
            Pressure::from_pascals(p_pa),
            None,
            None,
            backend,
            port,
        )
        .unwrap();

        let state: Vec<f64> = vec![];

        // 1-equation mode: only pressure residual
        let mut residuals_1 = vec![0.0_f64; 1];
        sink.compute_residuals(&state, &mut residuals_1).unwrap();
        assert!(
            residuals_1[0].abs() < 1e-6,
            "Pressure residual (1-eq) should be zero at set-point, got {}",
            residuals_1[0]
        );

        // 2-equation mode after setting temperature
        sink.set_temperature(
            Temperature::from_celsius(t_c),
            Concentration::from_percent(30.0),
        )
        .unwrap();

        let mut residuals_2 = vec![0.0_f64; 2];
        sink.compute_residuals(&state, &mut residuals_2).unwrap();
        assert!(
            residuals_2[0].abs() < 1e-6,
            "Pressure residual (2-eq) should be zero, got {}",
            residuals_2[0]
        );
        assert!(
            residuals_2[1].abs() < 1e-6,
            "Enthalpy residual (2-eq) should be zero, got {}",
            residuals_2[1]
        );
    }

    // Task 4.4 — Trait object compatibility: both components usable as Box<dyn Component>.
    #[test]
    fn test_brine_source_trait_object() {
        let backend = Arc::new(MockBrineBackend::new());
        let port = make_port("Glycol", 3.0e5, 70_000.0);
        let source = BrineSource::new(
            "Glycol",
            Pressure::from_pascals(3.0e5),
            Temperature::from_celsius(20.0),
            Concentration::from_percent(30.0),
            backend,
            port,
        )
        .unwrap();

        let boxed: Box<dyn Component> = Box::new(source);
        assert_eq!(boxed.n_equations(), 2);
        assert!(boxed.get_ports().is_empty());
        assert_eq!(
            boxed.energy_transfers(&[]),
            Some((Power::from_watts(0.0), Power::from_watts(0.0)))
        );
    }

    #[test]
    fn test_brine_sink_trait_object() {
        let backend = Arc::new(MockBrineBackend::new());
        let port = make_port("Glycol", 2.0e5, 60_000.0);
        let sink = BrineSink::new(
            "Glycol",
            Pressure::from_pascals(2.0e5),
            None,
            None,
            backend,
            port,
        )
        .unwrap();

        let boxed: Box<dyn Component> = Box::new(sink);
        assert_eq!(boxed.n_equations(), 1);
        assert!(boxed.get_ports().is_empty());
        assert_eq!(
            boxed.energy_transfers(&[]),
            Some((Power::from_watts(0.0), Power::from_watts(0.0)))
        );
    }

    // Task 4.5 — Energy methods: Q=0, W=0 for boundary components.
    #[test]
    fn test_brine_source_energy_transfers_zero() {
        let backend = Arc::new(MockBrineBackend::new());
        let port = make_port("Glycol", 3.0e5, 70_000.0);
        let source = BrineSource::new(
            "Glycol",
            Pressure::from_pascals(3.0e5),
            Temperature::from_celsius(20.0),
            Concentration::from_percent(0.0),
            backend,
            port,
        )
        .unwrap();

        let transfers = source.energy_transfers(&[]);
        assert_eq!(
            transfers,
            Some((Power::from_watts(0.0), Power::from_watts(0.0)))
        );
    }

    #[test]
    fn test_brine_source_accepts_meg_fluid() {
        let backend = Arc::new(MockBrineBackend::new());
        let port = make_port("MEG", 3.0e5, 70_000.0);
        let result = BrineSource::new(
            "MEG",
            Pressure::from_pascals(3.0e5),
            Temperature::from_celsius(20.0),
            Concentration::from_percent(30.0),
            backend,
            port,
        );
        assert!(
            result.is_ok(),
            "MEG should be accepted as an incompressible fluid"
        );
    }

    #[test]
    fn test_brine_source_accepts_peg_fluid() {
        let backend = Arc::new(MockBrineBackend::new());
        let port = make_port("PEG", 3.0e5, 70_000.0);
        let result = BrineSource::new(
            "PEG",
            Pressure::from_pascals(3.0e5),
            Temperature::from_celsius(20.0),
            Concentration::from_percent(40.0),
            backend,
            port,
        );
        assert!(
            result.is_ok(),
            "PEG should be accepted as an incompressible fluid"
        );
    }
}
