//! Port and Connection System
//!
//! This module provides the foundation for connecting thermodynamic components
//! using the Type-State pattern for compile-time connection safety.
//!
//! ## Type-State Pattern
//!
//! Ports have two states:
//! - `Disconnected`: Initial state, cannot be used in solver
//! - `Connected`: Linked to another port, ready for simulation
//!
//! State transitions are enforced at compile time:
//! ```text
//! Port<Disconnected> --connect()--> Port<Connected>
//!      ↑                                    │
//!      └───────── (no way back) ────────────┘
//! ```
//!
//! ## Connection Semantics
//!
//! Connected ports validate continuity (pressure/enthalpy match) at connection time,
//! but track values independently afterward. This allows the solver to update port
//! states during iteration without requiring synchronization.
//!
//! ## Example
//!
//! ```rust
//! use entropyk_components::port::{Port, Disconnected, Connected, FluidId, ConnectionError};
//! use entropyk_core::{Pressure, Enthalpy};
//!
//! // Create two disconnected ports
//! let port1 = Port::new(FluidId::new("R134a"), Pressure::from_bar(1.0), Enthalpy::from_joules_per_kg(400000.0));
//! let port2 = Port::new(FluidId::new("R134a"), Pressure::from_bar(1.0), Enthalpy::from_joules_per_kg(400000.0));
//!
//! // Connect them
//! let (connected1, connected2) = port1.connect(port2)?;
//!
//! // Ports track values independently for solver flexibility
//! assert_eq!(connected1.pressure().to_bar(), 1.0);
//! # Ok::<(), ConnectionError>(())
//! ```

use entropyk_core::{Enthalpy, Pressure};
pub use entropyk_fluids::FluidId;
use std::marker::PhantomData;
use thiserror::Error;

/// Default relative tolerance for pressure matching (0.01% = 100 ppm).
/// For 1 bar = 100,000 Pa, this allows 10 Pa difference.
const PRESSURE_TOLERANCE_FRACTION: f64 = 1e-4;

/// Default absolute tolerance for enthalpy matching (100 J/kg).
/// This is approximately 0.024 kJ/kg, reasonable for HVAC calculations.
const ENTHALPY_TOLERANCE_J_KG: f64 = 100.0;

/// Minimum absolute pressure tolerance (1 Pa) to avoid issues near zero.
const MIN_PRESSURE_TOLERANCE_PA: f64 = 1.0;

/// Errors that can occur during port operations.
#[derive(Error, Debug, Clone, PartialEq)]
pub enum ConnectionError {
    /// Attempted to connect ports with incompatible fluids.
    #[error("Incompatible fluids: cannot connect {from} to {to}")]
    IncompatibleFluid {
        /// Source fluid identifier
        from: String,
        /// Target fluid identifier
        to: String,
    },

    /// Attempted to connect ports of incompatible semantic kinds.
    #[error("Incompatible port kinds: cannot connect {from:?} to {to:?}")]
    IncompatiblePortKind {
        /// Source port kind
        from: PortKind,
        /// Target port kind
        to: PortKind,
    },

    /// Pressure mismatch at connection point.
    #[error(
        "Pressure mismatch: {from_pressure} Pa vs {to_pressure} Pa (tolerance: {tolerance} Pa)"
    )]
    PressureMismatch {
        /// Pressure at source port (Pa)
        from_pressure: f64,
        /// Pressure at target port (Pa)
        to_pressure: f64,
        /// Tolerance used for comparison (Pa)
        tolerance: f64,
    },

    /// Enthalpy mismatch at connection point.
    #[error("Enthalpy mismatch: {from_enthalpy} J/kg vs {to_enthalpy} J/kg (tolerance: {tolerance} J/kg)")]
    EnthalpyMismatch {
        /// Enthalpy at source port (J/kg)
        from_enthalpy: f64,
        /// Enthalpy at target port (J/kg)
        to_enthalpy: f64,
        /// Tolerance used for comparison (J/kg)
        tolerance: f64,
    },

    /// Attempted to connect a port that is already connected.
    #[error("Port is already connected and cannot be reconnected")]
    AlreadyConnected,

    /// Detected a cycle in the connection graph.
    #[error("Connection would create a cycle in the system topology")]
    CycleDetected,

    /// Invalid port index.
    #[error(
        "Invalid port index {index}: component has {port_count} ports (valid: 0..{max_index})"
    )]
    InvalidPortIndex {
        /// The invalid port index that was requested
        index: usize,
        /// Number of ports on the component
        port_count: usize,
        /// Maximum valid index (port_count - 1, or 0 if no ports)
        max_index: usize,
    },

    /// Invalid node index.
    #[error("Invalid node index: {0}")]
    InvalidNodeIndex(usize),
}

/// Semantic kind of a port, used to validate that only compatible ports are
/// wired together and to distinguish physical (acausal) from control (causal)
/// connections.
///
/// - [`PortKind::Refrigerant`] / [`PortKind::Secondary`] are **acausal physical**
///   ports: an edge asserts thermodynamic continuity/coupling (P, h).
/// - [`PortKind::Mechanical`] carries shaft power / speed.
/// - [`PortKind::Signal`] is a **causal** control connection (measurement →
///   controller → actuator); it does not carry a thermodynamic state.
///
/// Only ports of the same kind may be connected. Refrigerant and Secondary are
/// deliberately distinct so a working-fluid line cannot be wired to a
/// water/brine/air line by mistake.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum PortKind {
    /// Primary working fluid (the refrigerant loop).
    #[default]
    Refrigerant,
    /// Secondary heat-transfer fluid (water, brine, or air).
    Secondary,
    /// Mechanical connection (shaft power / rotational speed).
    Mechanical,
    /// Causal control signal (no thermodynamic state).
    Signal,
}

impl PortKind {
    /// Returns `true` if the two kinds may be connected together.
    pub fn is_compatible_with(self, other: PortKind) -> bool {
        self == other
    }
}

/// Type-state marker for disconnected ports.
///
/// Ports in this state cannot be used in the solver until connected.
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Disconnected;

/// Type-state marker for connected ports.
///
/// Ports in this state are linked to another port and ready for simulation.

#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Connected;

/// A thermodynamic port for connecting components.
///
/// Ports use the Type-State pattern to enforce connection safety at compile time.
/// A `Port<Disconnected>` must be connected before it can be used in simulations.
///
/// # Type Parameters
///
/// * `State` - Either `Disconnected` or `Connected`, tracking the port's state
///
/// # Examples
///
/// ```
/// use entropyk_components::port::{Port, Disconnected, FluidId};
/// use entropyk_core::{Pressure, Enthalpy};
///
/// // Create a disconnected port
/// let port: Port<Disconnected> = Port::new(
///     FluidId::new("R134a"),
///     Pressure::from_bar(1.0),
///     Enthalpy::from_joules_per_kg(400000.0)
/// );
/// ```
#[derive(Debug, Clone, PartialEq)]
pub struct Port<State> {
    fluid_id: FluidId,
    pressure: Pressure,
    enthalpy: Enthalpy,
    kind: PortKind,
    _state: PhantomData<State>,
}

/// Helper to validate connection parameters.
fn validate_connection_params(
    from_fluid: &FluidId,
    from_p: Pressure,
    from_h: Enthalpy,
    from_kind: PortKind,
    to_fluid: &FluidId,
    to_p: Pressure,
    to_h: Enthalpy,
    to_kind: PortKind,
) -> Result<(), ConnectionError> {
    if !from_kind.is_compatible_with(to_kind) {
        return Err(ConnectionError::IncompatiblePortKind {
            from: from_kind,
            to: to_kind,
        });
    }

    if from_fluid != to_fluid {
        return Err(ConnectionError::IncompatibleFluid {
            from: from_fluid.to_string(),
            to: to_fluid.to_string(),
        });
    }

    let pressure_tol =
        (from_p.to_pascals().abs() * PRESSURE_TOLERANCE_FRACTION).max(MIN_PRESSURE_TOLERANCE_PA);
    let pressure_diff = (from_p.to_pascals() - to_p.to_pascals()).abs();
    if pressure_diff > pressure_tol {
        return Err(ConnectionError::PressureMismatch {
            from_pressure: from_p.to_pascals(),
            to_pressure: to_p.to_pascals(),
            tolerance: pressure_tol,
        });
    }

    let enthalpy_diff = (from_h.to_joules_per_kg() - to_h.to_joules_per_kg()).abs();
    if enthalpy_diff > ENTHALPY_TOLERANCE_J_KG {
        return Err(ConnectionError::EnthalpyMismatch {
            from_enthalpy: from_h.to_joules_per_kg(),
            to_enthalpy: to_h.to_joules_per_kg(),
            tolerance: ENTHALPY_TOLERANCE_J_KG,
        });
    }

    Ok(())
}

impl Port<Disconnected> {
    /// Creates a new disconnected port.
    ///
    /// # Arguments
    ///
    /// * `fluid_id` - Identifier for the fluid flowing through this port
    /// * `pressure` - Initial pressure at the port
    /// * `enthalpy` - Initial specific enthalpy at the port
    ///
    /// # Examples
    ///
    /// ```
    /// use entropyk_components::port::{Port, FluidId};
    /// use entropyk_core::{Pressure, Enthalpy};
    ///
    /// let port = Port::new(
    ///     FluidId::new("R134a"),
    ///     Pressure::from_bar(1.0),
    ///     Enthalpy::from_joules_per_kg(400000.0)
    /// );
    /// ```
    pub fn new(fluid_id: FluidId, pressure: Pressure, enthalpy: Enthalpy) -> Self {
        Self {
            fluid_id,
            pressure,
            enthalpy,
            kind: PortKind::Refrigerant,
            _state: PhantomData,
        }
    }

    /// Sets the semantic [`PortKind`] of this port (default is
    /// [`PortKind::Refrigerant`]).
    pub fn with_kind(mut self, kind: PortKind) -> Self {
        self.kind = kind;
        self
    }

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

    /// Returns the semantic kind of this port.
    pub fn kind(&self) -> PortKind {
        self.kind
    }

    /// Returns the current pressure.
    pub fn pressure(&self) -> Pressure {
        self.pressure
    }

    /// Returns the current enthalpy.
    pub fn enthalpy(&self) -> Enthalpy {
        self.enthalpy
    }

    /// Connects two disconnected ports.
    ///
    /// Validates that:
    /// - Both ports have the same fluid type
    /// - Pressures match within relative tolerance
    /// - Enthalpies match within absolute tolerance
    ///
    /// After connection, ports track values independently, allowing the solver
    /// to update states during iteration.
    ///
    /// # Arguments
    ///
    /// * `other` - The port to connect to
    ///
    /// # Returns
    ///
    /// Returns a tuple of `(Port<Connected>, Port<Connected>)` on success,
    /// or a `ConnectionError` if validation fails.
    ///
    /// # Examples
    ///
    /// ```
    /// use entropyk_components::port::{Port, FluidId, ConnectionError};
    /// use entropyk_core::{Pressure, Enthalpy};
    ///
    /// let port1 = Port::new(
    ///     FluidId::new("R134a"),
    ///     Pressure::from_pascals(100000.0),
    ///     Enthalpy::from_joules_per_kg(400000.0)
    /// );
    /// let port2 = Port::new(
    ///     FluidId::new("R134a"),
    ///     Pressure::from_pascals(100000.0),
    ///     Enthalpy::from_joules_per_kg(400000.0)
    /// );
    ///
    /// let (connected1, connected2) = port1.connect(port2)?;
    /// # Ok::<(), ConnectionError>(())
    /// ```
    pub fn connect(
        self,
        other: Port<Disconnected>,
    ) -> Result<(Port<Connected>, Port<Connected>), ConnectionError> {
        validate_connection_params(
            &self.fluid_id,
            self.pressure,
            self.enthalpy,
            self.kind,
            &other.fluid_id,
            other.pressure,
            other.enthalpy,
            other.kind,
        )?;

        let avg_pressure = Pressure::from_pascals(
            (self.pressure.to_pascals() + other.pressure.to_pascals()) / 2.0,
        );
        let avg_enthalpy = Enthalpy::from_joules_per_kg(
            (self.enthalpy.to_joules_per_kg() + other.enthalpy.to_joules_per_kg()) / 2.0,
        );

        let connected1 = Port {
            fluid_id: self.fluid_id,
            pressure: avg_pressure,
            enthalpy: avg_enthalpy,
            kind: self.kind,
            _state: PhantomData,
        };

        let connected2 = Port {
            fluid_id: other.fluid_id,
            pressure: avg_pressure,
            enthalpy: avg_enthalpy,
            kind: other.kind,
            _state: PhantomData,
        };

        Ok((connected1, connected2))
    }
}

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

    /// Returns the semantic kind of this port.
    pub fn kind(&self) -> PortKind {
        self.kind
    }

    /// Returns the current pressure.
    pub fn pressure(&self) -> Pressure {
        self.pressure
    }

    /// Returns the current enthalpy.
    pub fn enthalpy(&self) -> Enthalpy {
        self.enthalpy
    }

    /// Updates the pressure at this port.
    ///
    /// # Arguments
    ///
    /// * `pressure` - The new pressure value
    pub fn set_pressure(&mut self, pressure: Pressure) {
        self.pressure = pressure;
    }

    /// Updates the enthalpy at this port.
    ///
    /// # Arguments
    ///
    /// * `enthalpy` - The new enthalpy value
    pub fn set_enthalpy(&mut self, enthalpy: Enthalpy) {
        self.enthalpy = enthalpy;
    }
}

/// A connected port reference that can be stored in components.
///
/// This type is object-safe and can be used in trait objects.
pub type ConnectedPort = Port<Connected>;

/// Validates that two connected ports are compatible for a flow connection.
///
/// Uses the same tolerance constants as [`Port::connect`](Port::connect):
/// - Pressure: `max(P * 1e-4, 1 Pa)`
/// - Enthalpy: 100 J/kg
///
/// # Arguments
///
/// * `outlet` - Source port (flow direction: outlet → inlet)
/// * `inlet` - Target port
///
/// # Returns
///
/// `Ok(())` if ports are compatible, `Err(ConnectionError)` otherwise.
pub fn validate_port_continuity(
    outlet: &ConnectedPort,
    inlet: &ConnectedPort,
) -> Result<(), ConnectionError> {
    validate_connection_params(
        &outlet.fluid_id,
        outlet.pressure,
        outlet.enthalpy,
        outlet.kind,
        &inlet.fluid_id,
        inlet.pressure,
        inlet.enthalpy,
        inlet.kind,
    )
}

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

    #[test]
    fn test_port_default_kind_is_refrigerant() {
        let port = Port::new(
            FluidId::new("R134a"),
            Pressure::from_bar(1.0),
            Enthalpy::from_joules_per_kg(400000.0),
        );
        assert_eq!(port.kind(), PortKind::Refrigerant);
    }

    #[test]
    fn test_with_kind_sets_and_propagates_through_connect() {
        let sec1 = Port::new(
            FluidId::new("Water"),
            Pressure::from_bar(2.0),
            Enthalpy::from_joules_per_kg(100000.0),
        )
        .with_kind(PortKind::Secondary);
        let sec2 = Port::new(
            FluidId::new("Water"),
            Pressure::from_bar(2.0),
            Enthalpy::from_joules_per_kg(100000.0),
        )
        .with_kind(PortKind::Secondary);

        let (c1, c2) = sec1
            .connect(sec2)
            .expect("matching secondary ports connect");
        assert_eq!(c1.kind(), PortKind::Secondary);
        assert_eq!(c2.kind(), PortKind::Secondary);
    }

    #[test]
    fn test_incompatible_port_kinds_rejected() {
        let refrigerant = Port::new(
            FluidId::new("Water"),
            Pressure::from_bar(1.0),
            Enthalpy::from_joules_per_kg(100000.0),
        ); // default Refrigerant
        let secondary = Port::new(
            FluidId::new("Water"),
            Pressure::from_bar(1.0),
            Enthalpy::from_joules_per_kg(100000.0),
        )
        .with_kind(PortKind::Secondary);

        let err = refrigerant.connect(secondary).unwrap_err();
        assert_eq!(
            err,
            ConnectionError::IncompatiblePortKind {
                from: PortKind::Refrigerant,
                to: PortKind::Secondary,
            }
        );
    }

    #[test]
    fn test_port_creation() {
        let port = Port::new(
            FluidId::new("R134a"),
            Pressure::from_bar(1.0),
            Enthalpy::from_joules_per_kg(400000.0),
        );

        assert_eq!(port.fluid_id().as_str(), "R134a");
        assert_relative_eq!(port.pressure().to_bar(), 1.0, epsilon = 1e-10);
        assert_relative_eq!(
            port.enthalpy().to_joules_per_kg(),
            400000.0,
            epsilon = 1e-10
        );
    }

    #[test]
    fn test_successful_connection() {
        let port1 = Port::new(
            FluidId::new("R134a"),
            Pressure::from_pascals(100000.0),
            Enthalpy::from_joules_per_kg(400000.0),
        );
        let port2 = Port::new(
            FluidId::new("R134a"),
            Pressure::from_pascals(100000.0),
            Enthalpy::from_joules_per_kg(400000.0),
        );

        let (connected1, connected2) = port1.connect(port2).unwrap();

        assert_eq!(connected1.fluid_id().as_str(), "R134a");
        assert_eq!(connected2.fluid_id().as_str(), "R134a");
        assert_relative_eq!(
            connected1.pressure().to_pascals(),
            100000.0,
            epsilon = 1e-10
        );
        assert_relative_eq!(
            connected2.pressure().to_pascals(),
            100000.0,
            epsilon = 1e-10
        );
    }

    #[test]
    fn test_incompatible_fluid_error() {
        let port1 = Port::new(
            FluidId::new("R134a"),
            Pressure::from_pascals(100000.0),
            Enthalpy::from_joules_per_kg(400000.0),
        );
        let port2 = Port::new(
            FluidId::new("Water"),
            Pressure::from_pascals(100000.0),
            Enthalpy::from_joules_per_kg(400000.0),
        );

        let result = port1.connect(port2);

        assert!(matches!(
            result,
            Err(ConnectionError::IncompatibleFluid { .. })
        ));
    }

    #[test]
    fn test_pressure_mismatch_error() {
        let port1 = Port::new(
            FluidId::new("R134a"),
            Pressure::from_pascals(100000.0),
            Enthalpy::from_joules_per_kg(400000.0),
        );
        let port2 = Port::new(
            FluidId::new("R134a"),
            Pressure::from_pascals(200000.0),
            Enthalpy::from_joules_per_kg(400000.0),
        );

        let result = port1.connect(port2);

        assert!(matches!(
            result,
            Err(ConnectionError::PressureMismatch { .. })
        ));
    }

    #[test]
    fn test_enthalpy_mismatch_error() {
        let port1 = Port::new(
            FluidId::new("R134a"),
            Pressure::from_pascals(100000.0),
            Enthalpy::from_joules_per_kg(400000.0),
        );
        let port2 = Port::new(
            FluidId::new("R134a"),
            Pressure::from_pascals(100000.0),
            Enthalpy::from_joules_per_kg(500000.0),
        );

        let result = port1.connect(port2);

        assert!(matches!(
            result,
            Err(ConnectionError::EnthalpyMismatch { .. })
        ));
    }

    #[test]
    fn test_connected_port_setters() {
        let port1 = Port::new(
            FluidId::new("R134a"),
            Pressure::from_pascals(100000.0),
            Enthalpy::from_joules_per_kg(400000.0),
        );
        let port2 = Port::new(
            FluidId::new("R134a"),
            Pressure::from_pascals(100000.0),
            Enthalpy::from_joules_per_kg(400000.0),
        );

        let (mut connected1, _) = port1.connect(port2).unwrap();

        connected1.set_pressure(Pressure::from_pascals(150000.0));
        connected1.set_enthalpy(Enthalpy::from_joules_per_kg(450000.0));

        assert_relative_eq!(
            connected1.pressure().to_pascals(),
            150000.0,
            epsilon = 1e-10
        );
        assert_relative_eq!(
            connected1.enthalpy().to_joules_per_kg(),
            450000.0,
            epsilon = 1e-10
        );
    }

    #[test]
    fn test_ports_track_independently() {
        let port1 = Port::new(
            FluidId::new("R134a"),
            Pressure::from_pascals(100000.0),
            Enthalpy::from_joules_per_kg(400000.0),
        );
        let port2 = Port::new(
            FluidId::new("R134a"),
            Pressure::from_pascals(100000.0),
            Enthalpy::from_joules_per_kg(400000.0),
        );

        let (mut connected1, connected2) = port1.connect(port2).unwrap();

        connected1.set_pressure(Pressure::from_pascals(150000.0));

        // connected2 should NOT see the change - ports are independent
        assert_relative_eq!(
            connected2.pressure().to_pascals(),
            100000.0,
            epsilon = 1e-10
        );
    }

    #[test]
    fn test_fluid_id_creation() {
        let fluid1 = FluidId::new("R134a");
        let fluid2 = FluidId::new(String::from("Water"));

        assert_eq!(fluid1.as_str(), "R134a");
        assert_eq!(fluid2.as_str(), "Water");
    }

    #[test]
    fn test_connection_error_display() {
        let err = ConnectionError::IncompatibleFluid {
            from: "R134a".to_string(),
            to: "Water".to_string(),
        };
        let msg = format!("{}", err);
        assert!(msg.contains("Incompatible fluids"));
        assert!(msg.contains("R134a"));
        assert!(msg.contains("Water"));

        let err = ConnectionError::PressureMismatch {
            from_pressure: 100000.0,
            to_pressure: 200000.0,
            tolerance: 10.0,
        };
        let msg = format!("{}", err);
        assert!(msg.contains("100000 Pa"));
        assert!(msg.contains("200000 Pa"));
        assert!(msg.contains("tolerance"));
    }

    #[test]
    fn test_pressure_averaging_on_connection() {
        let port1 = Port::new(
            FluidId::new("R134a"),
            Pressure::from_pascals(100000.0),
            Enthalpy::from_joules_per_kg(400000.0),
        );
        let port2 = Port::new(
            FluidId::new("R134a"),
            Pressure::from_pascals(100000.0),
            Enthalpy::from_joules_per_kg(400000.0),
        );

        let (connected1, connected2) = port1.connect(port2).unwrap();

        assert_relative_eq!(
            connected1.pressure().to_pascals(),
            connected2.pressure().to_pascals(),
            epsilon = 1e-10
        );
    }

    #[test]
    fn test_pressure_tolerance_with_small_difference() {
        let port1 = Port::new(
            FluidId::new("R134a"),
            Pressure::from_pascals(100000.0),
            Enthalpy::from_joules_per_kg(400000.0),
        );
        let port2 = Port::new(
            FluidId::new("R134a"),
            Pressure::from_pascals(100005.0),
            Enthalpy::from_joules_per_kg(400000.0),
        );

        let result = port1.connect(port2);

        assert!(
            result.is_ok(),
            "5 Pa difference should be within tolerance for 100 kPa pressure"
        );
    }

    #[test]
    fn test_clone_disconnected_port() {
        let port1 = Port::new(
            FluidId::new("R134a"),
            Pressure::from_pascals(100000.0),
            Enthalpy::from_joules_per_kg(400000.0),
        );
        let port2 = port1.clone();

        assert_eq!(port1, port2);
    }

    #[test]
    fn test_fluid_id_equality() {
        let f1 = FluidId::new("R134a");
        let f2 = FluidId::new("R134a");
        let f3 = FluidId::new("Water");

        assert_eq!(f1, f2);
        assert_ne!(f1, f3);
    }

    #[test]
    fn test_already_connected_error() {
        let err = ConnectionError::AlreadyConnected;
        let msg = format!("{}", err);
        assert!(msg.contains("already connected"));
    }

    #[test]
    fn test_cycle_detected_error() {
        let err = ConnectionError::CycleDetected;
        let msg = format!("{}", err);
        assert!(msg.contains("cycle"));
    }

    #[test]
    fn test_validate_port_continuity_ok() {
        let p1 = Port::new(
            FluidId::new("R134a"),
            Pressure::from_pascals(100_000.0),
            Enthalpy::from_joules_per_kg(400_000.0),
        );
        let p2 = Port::new(
            FluidId::new("R134a"),
            Pressure::from_pascals(100_000.0),
            Enthalpy::from_joules_per_kg(400_000.0),
        );
        let (c1, c2) = p1.connect(p2).unwrap();
        assert!(validate_port_continuity(&c1, &c2).is_ok());
        assert!(validate_port_continuity(&c2, &c1).is_ok());
    }

    #[test]
    fn test_validate_port_continuity_incompatible_fluid() {
        let (r134a, _) = Port::new(
            FluidId::new("R134a"),
            Pressure::from_pascals(100_000.0),
            Enthalpy::from_joules_per_kg(400_000.0),
        )
        .connect(Port::new(
            FluidId::new("R134a"),
            Pressure::from_pascals(100_000.0),
            Enthalpy::from_joules_per_kg(400_000.0),
        ))
        .unwrap();
        let (water, _) = Port::new(
            FluidId::new("Water"),
            Pressure::from_pascals(100_000.0),
            Enthalpy::from_joules_per_kg(400_000.0),
        )
        .connect(Port::new(
            FluidId::new("Water"),
            Pressure::from_pascals(100_000.0),
            Enthalpy::from_joules_per_kg(400_000.0),
        ))
        .unwrap();
        assert!(matches!(
            validate_port_continuity(&r134a, &water),
            Err(ConnectionError::IncompatibleFluid { .. })
        ));
    }
}
