//! Test backend implementation for unit testing.
//!
//! This module provides a mock backend that returns simplified/idealized
//! property values for testing without requiring external dependencies
//! like CoolProp.
//!
//! For R134a, the backend uses tabulated saturation data from NIST REFPROP
//! (referenced in the thermodynamic test specifications document) to support
//! PressureQuality and PressureEnthalpy state inputs.

use crate::backend::FluidBackend;
use crate::errors::{FluidError, FluidResult};
#[cfg(test)]
use crate::mixture::Mixture;
use crate::types::{CriticalPoint, FluidId, FluidState, Phase, Property};
use entropyk_core::{Pressure, Temperature};
use std::collections::HashMap;

/// Saturation data point for a refrigerant.
/// Values from NIST REFPROP / thermodynamic-test-specifications.md.
struct SatPoint {
    t_celsius: f64,
    p_bar: f64,
    hf_kjkg: f64,
    hg_kjkg: f64,
    rho_f: f64,
    rho_g: f64,
}

/// Saturation table for a single fluid, sorted by pressure ascending.
struct SatTable {
    fluid: String,
    points: Vec<SatPoint>,
}

/// Test backend for unit testing.
///
/// This backend provides simplified thermodynamic property calculations
/// suitable for testing without external dependencies. Values are idealized
/// approximations and should NOT be used for real simulations.
///
/// For R134a, saturation data is interpolated from NIST reference tables,
/// enabling PressureQuality (P,x) and PressureEnthalpy (P,h) queries.
pub struct TestBackend {
    /// Map of fluid names to critical points
    critical_points: HashMap<String, CriticalPoint>,
    /// List of available test fluids
    available_fluids: Vec<String>,
    /// Saturation tables per fluid (R134a, R410A, etc.)
    sat_tables: Vec<SatTable>,
}

impl TestBackend {
    /// Creates a new TestBackend with default test fluids.
    pub fn new() -> Self {
        let mut critical_points = HashMap::new();

        // CO2 (R744)
        critical_points.insert(
            "CO2".to_string(),
            CriticalPoint::new(
                Temperature::from_kelvin(304.13),
                Pressure::from_pascals(7.3773e6),
                467.0,
            ),
        );

        // R134a
        critical_points.insert(
            "R134a".to_string(),
            CriticalPoint::new(
                Temperature::from_kelvin(374.21),
                Pressure::from_pascals(4.059e6),
                512.0,
            ),
        );

        // R410A
        critical_points.insert(
            "R410A".to_string(),
            CriticalPoint::new(
                Temperature::from_kelvin(344.49),
                Pressure::from_pascals(4.926e6),
                458.0,
            ),
        );

        // R32
        critical_points.insert(
            "R32".to_string(),
            CriticalPoint::new(
                Temperature::from_kelvin(351.25),
                Pressure::from_pascals(5.782e6),
                360.0,
            ),
        );

        // Water
        critical_points.insert(
            "Water".to_string(),
            CriticalPoint::new(
                Temperature::from_kelvin(647.096),
                Pressure::from_pascals(22.064e6),
                322.0,
            ),
        );

        let available_fluids = vec![
            "CO2".to_string(),
            "R134a".to_string(),
            "R410A".to_string(),
            "R32".to_string(),
            "Water".to_string(),
            "Nitrogen".to_string(),
            "Oxygen".to_string(),
            "Air".to_string(),
        ];

        // R134a saturation table from NIST REFPROP / thermodynamic-test-specifications.md §2.1
        let r134a_sat = SatTable {
            fluid: "R134a".to_string(),
            points: vec![
                SatPoint {
                    t_celsius: -10.0,
                    p_bar: 2.013,
                    hf_kjkg: 186.7,
                    hg_kjkg: 392.7,
                    rho_f: 1295.0,
                    rho_g: 10.2,
                },
                SatPoint {
                    t_celsius: 0.0,
                    p_bar: 2.928,
                    hf_kjkg: 200.0,
                    hg_kjkg: 398.6,
                    rho_f: 1295.0,
                    rho_g: 14.4,
                },
                SatPoint {
                    t_celsius: 7.0,
                    p_bar: 3.748,
                    hf_kjkg: 209.1,
                    hg_kjkg: 402.4,
                    rho_f: 1262.0,
                    rho_g: 18.2,
                },
                SatPoint {
                    t_celsius: 10.0,
                    p_bar: 4.150,
                    hf_kjkg: 213.0,
                    hg_kjkg: 404.0,
                    rho_f: 1251.0,
                    rho_g: 20.2,
                },
                SatPoint {
                    t_celsius: 20.0,
                    p_bar: 5.719,
                    hf_kjkg: 227.5,
                    hg_kjkg: 409.4,
                    rho_f: 1226.0,
                    rho_g: 27.8,
                },
                SatPoint {
                    t_celsius: 25.0,
                    p_bar: 6.658,
                    hf_kjkg: 234.6,
                    hg_kjkg: 412.0,
                    rho_f: 1207.0,
                    rho_g: 32.3,
                },
                SatPoint {
                    t_celsius: 35.0,
                    p_bar: 8.875,
                    hf_kjkg: 249.0,
                    hg_kjkg: 414.4,
                    rho_f: 1168.0,
                    rho_g: 43.1,
                },
                SatPoint {
                    t_celsius: 40.0,
                    p_bar: 10.170,
                    hf_kjkg: 256.4,
                    hg_kjkg: 419.4,
                    rho_f: 1148.0,
                    rho_g: 50.8,
                },
                SatPoint {
                    t_celsius: 45.0,
                    p_bar: 11.597,
                    hf_kjkg: 263.7,
                    hg_kjkg: 420.6,
                    rho_f: 1129.0,
                    rho_g: 58.9,
                },
                SatPoint {
                    t_celsius: 50.0,
                    p_bar: 13.180,
                    hf_kjkg: 271.4,
                    hg_kjkg: 421.2,
                    rho_f: 1102.0,
                    rho_g: 68.2,
                },
            ],
        };

        // R410A saturation table from thermodynamic-test-specifications.md §2.2
        // Extended with low-T points for BPHX evaporator at 4 bar (~-20°C)
        let r410a_sat = SatTable {
            fluid: "R410A".to_string(),
            points: vec![
                SatPoint {
                    t_celsius: -30.0,
                    p_bar: 2.34,
                    hf_kjkg: 156.0,
                    hg_kjkg: 422.0,
                    rho_f: 1140.0,
                    rho_g: 14.0,
                },
                SatPoint {
                    t_celsius: -20.0,
                    p_bar: 4.01,
                    hf_kjkg: 175.0,
                    hg_kjkg: 427.0,
                    rho_f: 1113.0,
                    rho_g: 23.0,
                },
                SatPoint {
                    t_celsius: -10.0,
                    p_bar: 5.85,
                    hf_kjkg: 178.0,
                    hg_kjkg: 428.0,
                    rho_f: 1100.0,
                    rho_g: 30.0,
                },
                SatPoint {
                    t_celsius: 0.0,
                    p_bar: 7.97,
                    hf_kjkg: 192.0,
                    hg_kjkg: 432.0,
                    rho_f: 1080.0,
                    rho_g: 40.0,
                },
                SatPoint {
                    t_celsius: 10.0,
                    p_bar: 10.82,
                    hf_kjkg: 207.0,
                    hg_kjkg: 436.0,
                    rho_f: 1050.0,
                    rho_g: 50.0,
                },
                SatPoint {
                    t_celsius: 20.0,
                    p_bar: 14.48,
                    hf_kjkg: 225.0,
                    hg_kjkg: 436.0,
                    rho_f: 1020.0,
                    rho_g: 65.0,
                },
                SatPoint {
                    t_celsius: 30.0,
                    p_bar: 18.95,
                    hf_kjkg: 245.0,
                    hg_kjkg: 434.0,
                    rho_f: 985.0,
                    rho_g: 82.0,
                },
                SatPoint {
                    t_celsius: 40.0,
                    p_bar: 24.27,
                    hf_kjkg: 268.0,
                    hg_kjkg: 432.0,
                    rho_f: 950.0,
                    rho_g: 100.0,
                },
                SatPoint {
                    t_celsius: 50.0,
                    p_bar: 30.47,
                    hf_kjkg: 290.0,
                    hg_kjkg: 427.0,
                    rho_f: 900.0,
                    rho_g: 130.0,
                },
            ],
        };

        TestBackend {
            critical_points,
            available_fluids,
            sat_tables: vec![r134a_sat, r410a_sat],
        }
    }

    /// Interpolate saturation properties for any fluid with a table.
    /// Returns (T_sat_C, h_f_kJkg, h_g_kJkg, rho_f, rho_g) or None.
    fn sat_at_p(&self, fluid: &str, p_pa: f64) -> Option<(f64, f64, f64, f64, f64)> {
        let table = self.sat_tables.iter().find(|t| t.fluid == fluid)?;
        let p_bar = p_pa / 1e5;
        let sat = &table.points;

        if p_bar < sat.first()?.p_bar || p_bar > sat.last()?.p_bar {
            return None;
        }

        let idx = sat.iter().position(|s| s.p_bar >= p_bar)?;
        if idx == 0 {
            let s = &sat[0];
            return Some((s.t_celsius, s.hf_kjkg, s.hg_kjkg, s.rho_f, s.rho_g));
        }

        let lo = &sat[idx - 1];
        let hi = &sat[idx];
        let t = (p_bar - lo.p_bar) / (hi.p_bar - lo.p_bar);

        Some((
            lo.t_celsius + t * (hi.t_celsius - lo.t_celsius),
            lo.hf_kjkg + t * (hi.hf_kjkg - lo.hf_kjkg),
            lo.hg_kjkg + t * (hi.hg_kjkg - lo.hg_kjkg),
            lo.rho_f + t * (hi.rho_f - lo.rho_f),
            lo.rho_g + t * (hi.rho_g - lo.rho_g),
        ))
    }

    /// Property from (P, quality) for any fluid with a saturation table.
    fn property_px(&self, fluid: &str, property: Property, p_pa: f64, x: f64) -> FluidResult<f64> {
        let (t_sat, hf, hg, rho_f, rho_g) =
            self.sat_at_p(fluid, p_pa).ok_or(FluidError::InvalidState {
                reason: format!(
                    "{} pressure {:.2} bar outside TestBackend table range",
                    fluid,
                    p_pa / 1e5
                ),
            })?;

        let h = hf + x * (hg - hf); // kJ/kg
        match property {
            Property::Enthalpy => Ok(h * 1000.0),        // J/kg
            Property::Temperature => Ok(t_sat + 273.15), // K
            Property::Density => {
                let vf = 1.0 / rho_f;
                let vg = 1.0 / rho_g;
                let v = vf + x * (vg - vf);
                Ok(1.0 / v)
            }
            Property::Pressure => Ok(p_pa),
            Property::Cp => Ok(1500.0),
            // Order-of-magnitude R134a-like transport for two-phase ΔP unit tests.
            Property::Viscosity => Ok(if x <= 0.5 { 2.0e-4 } else { 1.2e-5 }),
            Property::SurfaceTension => Ok(0.008),
            _ => Err(FluidError::UnsupportedProperty {
                property: property.to_string(),
            }),
        }
    }

    /// Property from (P, h) for any fluid with a saturation table.
    fn property_ph(
        &self,
        fluid: &str,
        property: Property,
        p_pa: f64,
        h_jkg: f64,
    ) -> FluidResult<f64> {
        let (t_sat, hf, hg, rho_f, rho_g) =
            self.sat_at_p(fluid, p_pa).ok_or(FluidError::InvalidState {
                reason: format!(
                    "{} pressure {:.2} bar outside TestBackend table range",
                    fluid,
                    p_pa / 1e5
                ),
            })?;

        let h_kjkg = h_jkg / 1000.0;
        let hf_j = hf * 1000.0;
        let hg_j = hg * 1000.0;

        match property {
            Property::Temperature => {
                if h_jkg <= hf_j {
                    // Subcooled liquid: T ≈ T_sat - (hf - h) / cp_liquid
                    Ok(t_sat + 273.15 - (hf - h_kjkg) / 1.5)
                } else if h_jkg >= hg_j {
                    // Superheated vapor: T ≈ T_sat + (h - hg) / cp_vapor
                    Ok(t_sat + 273.15 + (h_kjkg - hg) / 1.2)
                } else {
                    // Two-phase: T = T_sat
                    Ok(t_sat + 273.15)
                }
            }
            Property::Quality => {
                if h_jkg <= hf_j {
                    Ok(0.0) // Subcooled
                } else if h_jkg >= hg_j {
                    Ok(1.0) // Superheated
                } else {
                    Ok((h_kjkg - hf) / (hg - hf))
                }
            }
            Property::Density => {
                if h_jkg <= hf_j {
                    Ok(rho_f)
                } else if h_jkg >= hg_j {
                    // Superheated: ideal gas approx
                    let t_k = t_sat + 273.15 + (h_kjkg - hg) / 1.2;
                    Ok(p_pa / (t_k * 100.0)) // rough
                } else {
                    let x = (h_kjkg - hf) / (hg - hf);
                    let vf = 1.0 / rho_f;
                    let vg = 1.0 / rho_g;
                    Ok(1.0 / (vf + x * (vg - vf)))
                }
            }
            Property::Enthalpy => Ok(h_jkg),
            Property::Pressure => Ok(p_pa),
            Property::Cp => Ok(1500.0),
            // Smooth, invertible entropy surrogate for the (P,h)→S→(P,S) isentropic
            // path used by the compressor: s = h/300 − 50·ln(P/1e5).
            Property::Entropy => Ok(h_jkg / 300.0 - 50.0 * (p_pa / 1e5).ln()),
            _ => Err(FluidError::UnsupportedProperty {
                property: property.to_string(),
            }),
        }
    }

    /// Inverse of the [`property_ph`] entropy surrogate: recovers enthalpy from
    /// (P, S) so the isentropic compression path is consistent and smooth.
    /// h = 300·(s + 50·ln(P/1e5)).
    fn property_ps(&self, _fluid: &str, property: Property, p_pa: f64, s: f64) -> FluidResult<f64> {
        match property {
            Property::Enthalpy => Ok(300.0 * (s + 50.0 * (p_pa / 1e5).ln())),
            Property::Pressure => Ok(p_pa),
            Property::Entropy => Ok(s),
            _ => Err(FluidError::UnsupportedProperty {
                property: property.to_string(),
            }),
        }
    }

    /// Simplified ideal gas property calculation.
    fn ideal_property(
        &self,
        fluid: &str,
        property: Property,
        state: FluidState,
    ) -> FluidResult<f64> {
        // Simple ideal gas approximations for testing
        // Real implementation would use proper equations of state
        match fluid {
            "Nitrogen" | "Oxygen" | "Air" => self.ideal_gas_property(property, state, 29.0),
            "Water" => self.water_property(property, state),
            _ => {
                // For refrigerants, use simplified correlations
                self.refrigerant_property(fluid, property, state)
            }
        }
    }

    fn ideal_gas_property(
        &self,
        property: Property,
        state: FluidState,
        _molar_mass: f64,
    ) -> FluidResult<f64> {
        let (p, t) = match state {
            FluidState::PressureTemperature(p, t) => (p.to_pascals(), t.to_kelvin()),
            _ => {
                return Err(FluidError::InvalidState {
                    reason: "TestBackend only supports P-T state for ideal gases".to_string(),
                })
            }
        };

        // Simplified ideal gas: R = 8.314 J/(mol·K), approximate
        let r_specific = 287.0; // J/(kg·K) for air

        match property {
            Property::Density => Ok(p / (r_specific * t)),
            Property::Enthalpy => Ok(1005.0 * t), // Cp * T, Cp ≈ 1005 J/(kg·K) for air
            Property::Entropy => Ok(r_specific * t.ln()), // Simplified
            Property::Cp => Ok(1005.0),           // Constant pressure specific heat
            Property::Cv => Ok(718.0),            // Constant volume specific heat
            Property::Temperature => Ok(t),
            Property::Pressure => Ok(p),
            Property::ThermalConductivity => Ok(0.025), // W/(m·K) for air
            Property::Viscosity => Ok(1.8e-5),          // Pa·s for air
            _ => Err(FluidError::UnsupportedProperty {
                property: property.to_string(),
            }),
        }
    }

    fn water_property(&self, property: Property, state: FluidState) -> FluidResult<f64> {
        // Liquid-water idealization (Cp ≈ const) for unit tests without CoolProp.
        // Supports P-T and P-h so four-port secondary edges can query T(P,h)/Cp.
        let (p, t) = match state {
            FluidState::PressureTemperature(p, t) => (p.to_pascals(), t.to_kelvin()),
            FluidState::PressureEnthalpy(p, h) => {
                let p_pa = p.to_pascals();
                // h ≈ 4200·(T−273.15)  ⇒  T ≈ 273.15 + h/4200
                let t_k = 273.15 + h.to_joules_per_kg() / 4200.0;
                (p_pa, t_k)
            }
            _ => {
                return Err(FluidError::InvalidState {
                    reason: "TestBackend water supports P-T and P-h liquid states only".to_string(),
                })
            }
        };

        // Simplified liquid water near ambient pressure
        if p > 0.0 && p < 5.0e5 && t > 273.15 && t < 373.15 {
            match property {
                Property::Density => Ok(1000.0),                 // kg/m³
                Property::Enthalpy => Ok(4200.0 * (t - 273.15)), // Cp * ΔT
                Property::Cp => Ok(4184.0),                      // J/(kg·K)
                Property::ThermalConductivity => Ok(0.6),        // W/(m·K)
                Property::Viscosity => Ok(0.001),                // Pa·s
                Property::Temperature => Ok(t),
                Property::Pressure => Ok(p),
                _ => Err(FluidError::UnsupportedProperty {
                    property: property.to_string(),
                }),
            }
        } else {
            Err(FluidError::InvalidState {
                reason: "Water property only valid in liquid region".to_string(),
            })
        }
    }

    fn refrigerant_property(
        &self,
        fluid: &str,
        property: Property,
        state: FluidState,
    ) -> FluidResult<f64> {
        // Use tabulated saturation data for P-h and P-x queries
        match state {
            FluidState::PressureQuality(p, x) => {
                return self.property_px(fluid, property, p.to_pascals(), x.0);
            }
            FluidState::PressureEnthalpy(p, h) => {
                return self.property_ph(fluid, property, p.to_pascals(), h.to_joules_per_kg());
            }
            FluidState::PressureEntropy(p, s) => {
                return self.property_ps(fluid, property, p.to_pascals(), s.0);
            }
            _ => {} // fall through to P-T handling below
        }

        let (p, t) = match state {
            FluidState::PressureTemperature(p, t) => (p.to_pascals(), t.to_kelvin()),
            _ => {
                return Err(FluidError::InvalidState {
                    reason: format!(
                    "TestBackend only supports P-T state for {} (P-x and P-h available for R134a)",
                    fluid
                ),
                })
            }
        };

        // Simplified refrigerant properties
        match property {
            Property::Density => {
                // Rough approximation for liquid (~1000 kg/m³) vs vapor (~10-50 kg/m³)
                if p > 1e6 {
                    Ok(1000.0) // Liquid
                } else {
                    Ok(30.0) // Vapor
                }
            }
            Property::Enthalpy => {
                if p > 1e6 {
                    Ok(200000.0) // Liquid region
                } else {
                    Ok(400000.0) // Vapor region
                }
            }
            Property::Cp => Ok(1500.0), // Approximate
            Property::Temperature => Ok(t),
            Property::Pressure => Ok(p),
            Property::ThermalConductivity => Ok(0.015),
            Property::Viscosity => Ok(1.5e-5),
            _ => Err(FluidError::UnsupportedProperty {
                property: property.to_string(),
            }),
        }
    }

    fn determine_phase(&self, fluid: &str, state: FluidState) -> Phase {
        let (p, t) = match state {
            FluidState::PressureTemperature(p, t) => (p.to_pascals(), t.to_kelvin()),
            _ => return Phase::Unknown,
        };

        // Get critical point if available
        if let Some(cp) = self.critical_points.get(fluid) {
            let pc = cp.pressure_pascals();
            let tc = cp.temperature_kelvin();

            if p > pc && t > tc {
                return Phase::Supercritical;
            }
            if (p - pc).abs() / pc < 0.05 || (t - tc).abs() / tc < 0.05 {
                return Phase::Supercritical;
            }
        }

        // Simplified phase determination
        if p > 5e5 {
            Phase::Liquid
        } else {
            Phase::Vapor
        }
    }
}

impl Default for TestBackend {
    fn default() -> Self {
        Self::new()
    }
}

impl FluidBackend for TestBackend {
    fn property(&self, fluid: FluidId, property: Property, state: FluidState) -> FluidResult<f64> {
        if !self.is_fluid_available(&fluid) {
            return Err(FluidError::UnknownFluid {
                fluid: fluid.0.clone(),
            });
        }

        self.ideal_property(&fluid.0, property, state)
    }

    fn critical_point(&self, fluid: FluidId) -> FluidResult<CriticalPoint> {
        self.critical_points
            .get(&fluid.0)
            .copied()
            .ok_or(FluidError::NoCriticalPoint { fluid: fluid.0 })
    }

    fn is_fluid_available(&self, fluid: &FluidId) -> bool {
        self.available_fluids.iter().any(|f| f == &fluid.0)
    }

    fn phase(&self, fluid: FluidId, state: FluidState) -> FluidResult<Phase> {
        if !self.is_fluid_available(&fluid) {
            return Err(FluidError::UnknownFluid { fluid: fluid.0 });
        }

        Ok(self.determine_phase(&fluid.0, state))
    }

    fn list_fluids(&self) -> Vec<FluidId> {
        self.available_fluids
            .iter()
            .map(|s| FluidId::new(s.clone()))
            .collect()
    }

    fn full_state(
        &self,
        fluid: FluidId,
        p: entropyk_core::Pressure,
        h: entropyk_core::Enthalpy,
    ) -> FluidResult<crate::types::ThermoState> {
        let t_k = self.property(
            fluid.clone(),
            Property::Temperature,
            FluidState::from_ph(p, h),
        )?;
        Err(FluidError::UnsupportedProperty {
            property: format!("full_state for TestBackend: Temperature is {:.2} K", t_k),
        })
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::types::Quality;
    use entropyk_core::Enthalpy;

    #[test]
    fn test_backend_available_fluids() {
        let backend = TestBackend::new();

        assert!(backend.is_fluid_available(&FluidId::new("CO2")));
        assert!(backend.is_fluid_available(&FluidId::new("R134a")));
        assert!(!backend.is_fluid_available(&FluidId::new("R999")));
    }

    #[test]
    fn test_list_fluids() {
        let backend = TestBackend::new();
        let fluids = backend.list_fluids();

        assert!(!fluids.is_empty());
        assert!(fluids.iter().any(|f| f.0 == "CO2"));
    }

    #[test]
    fn test_critical_point() {
        let backend = TestBackend::new();

        let cp = backend.critical_point(FluidId::new("CO2")).unwrap();
        assert!((cp.temperature_kelvin() - 304.13).abs() < 0.1);
        assert!((cp.pressure_pascals() - 7.3773e6).abs() < 1e4);
    }

    #[test]
    fn test_critical_point_not_available() {
        let backend = TestBackend::new();

        let result = backend.critical_point(FluidId::new("UnknownFluid"));
        assert!(result.is_err());
    }

    #[test]
    fn test_property_nitrogen() {
        let backend = TestBackend::new();

        let state = FluidState::from_pt(
            Pressure::from_pascals(101325.0),
            Temperature::from_kelvin(300.0),
        );

        let density = backend
            .property(FluidId::new("Nitrogen"), Property::Density, state)
            .unwrap();

        assert!(density > 1.0); // Should be ~1.16 kg/m³
    }

    #[test]
    fn test_property_water() {
        let backend = TestBackend::new();

        let state = FluidState::from_pt(
            Pressure::from_pascals(101325.0),
            Temperature::from_celsius(25.0),
        );

        let density = backend
            .property(FluidId::new("Water"), Property::Density, state)
            .unwrap();

        assert!((density - 1000.0).abs() < 1.0);
    }

    #[test]
    fn test_property_unknown_fluid() {
        let backend = TestBackend::new();

        let state = FluidState::from_pt(
            Pressure::from_pascals(101325.0),
            Temperature::from_kelvin(300.0),
        );

        let result = backend.property(FluidId::new("R999"), Property::Density, state);

        assert!(result.is_err());
    }

    #[test]
    fn test_phase_co2_supercritical() {
        let backend = TestBackend::new();

        // Above critical point
        let state =
            FluidState::from_pt(Pressure::from_pascals(8e6), Temperature::from_kelvin(320.0));

        let phase = backend.phase(FluidId::new("CO2"), state).unwrap();
        assert_eq!(phase, Phase::Supercritical);
    }

    #[test]
    fn test_phase_liquid() {
        let backend = TestBackend::new();

        let state = FluidState::from_pt(
            Pressure::from_pascals(10e5),
            Temperature::from_celsius(25.0),
        );

        let phase = backend.phase(FluidId::new("Water"), state).unwrap();
        assert_eq!(phase, Phase::Liquid);
    }

    #[test]
    fn test_thermo_state_is_mixture() {
        let mix = Mixture::from_mass_fractions(&[("R32", 0.5), ("R1234yf", 0.5)]).unwrap();

        let state_pure =
            FluidState::from_pt(Pressure::from_bar(1.0), Temperature::from_celsius(25.0));
        assert!(!state_pure.is_mixture());

        let state_mix = FluidState::from_pt_mix(
            Pressure::from_bar(1.0),
            Temperature::from_celsius(25.0),
            mix,
        );
        assert!(state_mix.is_mixture());
    }

    // ─── R134a saturation table tests (from thermodynamic-test-specifications.md §2.1) ───

    /// T-COMP-BACKEND-01: R134a saturation enthalpy from quality
    /// At P=2.928 bar (T_sat=0°C), x=0 → h_f=200 kJ/kg, x=1 → h_g=398.6 kJ/kg
    #[test]
    fn test_r134a_sat_enthalpy_quality_0_at_0c() {
        let backend = TestBackend::new();
        let state = FluidState::from_px(Pressure::from_bar(2.928), Quality(0.0));
        let h = backend
            .property(FluidId::new("R134a"), Property::Enthalpy, state)
            .unwrap();
        // h_f at 0°C = 200 kJ/kg = 200000 J/kg
        assert!(
            (h - 200_000.0).abs() < 500.0,
            "h_f at 0°C: expected ~200000 J/kg, got {:.0}",
            h
        );
    }

    #[test]
    fn test_r134a_sat_enthalpy_quality_1_at_0c() {
        let backend = TestBackend::new();
        let state = FluidState::from_px(Pressure::from_bar(2.928), Quality(1.0));
        let h = backend
            .property(FluidId::new("R134a"), Property::Enthalpy, state)
            .unwrap();
        // h_g at 0°C = 398.6 kJ/kg = 398600 J/kg
        assert!(
            (h - 398_600.0).abs() < 500.0,
            "h_g at 0°C: expected ~398600 J/kg, got {:.0}",
            h
        );
    }

    /// T-COMP-BACKEND-02: R134a quality from (P, h) — isenthalpic expansion
    /// Saturated liquid at 40°C (h_f=256.4 kJ/kg) expanded to 0°C (P=2.928 bar)
    /// x = (h - h_f_evap) / h_fg_evap = (256.4 - 200.0) / (398.6 - 200.0) = 0.284
    #[test]
    fn test_r134a_quality_from_ph_after_expansion() {
        let backend = TestBackend::new();
        let state = FluidState::from_ph(
            Pressure::from_bar(2.928),
            Enthalpy::from_kilojoules_per_kg(256.4),
        );
        let x = backend
            .property(FluidId::new("R134a"), Property::Quality, state)
            .unwrap();
        assert!(
            (x - 0.284).abs() < 0.01,
            "quality after isenthalpic expansion: expected ~0.284, got {:.4}",
            x
        );
    }

    /// T-COMP-BACKEND-03: R134a T_sat from (P, h) in two-phase region
    /// At P=10.17 bar (40°C), two-phase should return T_sat ≈ 40°C = 313.15 K
    #[test]
    fn test_r134a_tsat_from_ph_twophase() {
        let backend = TestBackend::new();
        let state = FluidState::from_ph(
            Pressure::from_bar(10.17),
            Enthalpy::from_kilojoules_per_kg(350.0), // mid two-phase
        );
        let t = backend
            .property(FluidId::new("R134a"), Property::Temperature, state)
            .unwrap();
        assert!(
            (t - 313.15).abs() < 1.0,
            "T_sat at 10.17 bar: expected ~313.15 K, got {:.2} K",
            t
        );
    }

    /// T-COMP-BACKEND-04: R134a density in two-phase from (P, x)
    /// At P=2.928 bar (0°C), x=0.5: should be between rho_f and rho_g
    #[test]
    fn test_r134a_density_twophase() {
        let backend = TestBackend::new();
        let state = FluidState::from_px(Pressure::from_bar(2.928), Quality(0.5));
        let rho = backend
            .property(FluidId::new("R134a"), Property::Density, state)
            .unwrap();
        // rho_f=1295, rho_g=14.4 at 0°C. At x=0.5, should be much closer to rho_g
        assert!(
            rho > 14.4 && rho < 1295.0,
            "density at x=0.5: expected between 14.4 and 1295, got {:.1}",
            rho
        );
    }

    /// T-COMP-BACKEND-05: R134a interpolated enthalpy between table points
    /// At P=5.719 bar (20°C), x=0 → h_f=227.5 kJ/kg
    #[test]
    fn test_r134a_sat_20c_liquid() {
        let backend = TestBackend::new();
        let state = FluidState::from_px(Pressure::from_bar(5.719), Quality(0.0));
        let h = backend
            .property(FluidId::new("R134a"), Property::Enthalpy, state)
            .unwrap();
        assert!(
            (h - 227_500.0).abs() < 500.0,
            "h_f at 20°C: expected ~227500 J/kg, got {:.0}",
            h
        );
    }
}
