//! DoF balance for a water-cooled chiller with FloodedEvaporator (4-port live secondary).
//!
//! This test is intentionally **topology + ledger only** (no Newton solve):
//! - builds the honest machine graph with named multi-port edges;
//! - finalizes and asserts `validate_system_dof()` (square system);
//! - does **not** require CoolProp (uses `TestBackend` for boundary enthalpy only).
//!
//! Budget target (CM1.4):
//!   unknowns = 3 ṁ-branches + 2×8 edges = 19
//!   equations = Comp2 + Cond4 + EXV1 + Flooded4 + 2×(Src3+Sink1) = 19
//!
//! Run:
//!   cargo test -p entropyk-solver --test flooded_4port_dof

use std::sync::Arc;

use entropyk_components::brine_boundary::{BrineSink, BrineSource};
use entropyk_components::isentropic_compressor::VolumetricEfficiency;
use entropyk_components::port::{FluidId as PortFluidId, Port};
use entropyk_components::{
    Component, Condenser, FloodedEvaporator, IsenthalpicExpansionValve, IsentropicCompressor,
};
use entropyk_core::{Concentration, Pressure, Temperature};
use entropyk_fluids::{FluidBackend, TestBackend};
use entropyk_solver::system::System;
use entropyk_solver::{EquationRole, SystemDofBalance, SystemDofError};

fn dummy_port(fluid: &str) -> entropyk_components::ConnectedPort {
    let a = Port::new(
        PortFluidId::new(fluid),
        Pressure::from_bar(2.0),
        entropyk_core::Enthalpy::from_joules_per_kg(50_000.0),
    );
    let b = Port::new(
        PortFluidId::new(fluid),
        Pressure::from_bar(2.0),
        entropyk_core::Enthalpy::from_joules_per_kg(50_000.0),
    );
    a.connect(b).expect("dummy port pair").0
}

/// Assemble the flooded water-cooled topology and return a finalized system.
fn build_flooded_watercooled() -> System {
    let backend: Arc<dyn FluidBackend> = Arc::new(TestBackend::new());
    let ref_fluid = "R134a";
    let water = "Water";

    let comp = Box::new(
        IsentropicCompressor::new(0.70, 313.15, 278.15, 5.0)
            .with_refrigerant(ref_fluid)
            .with_fluid_backend(backend.clone())
            .with_displacement(5.0e-5, 50.0, VolumetricEfficiency::Constant(0.92)),
    );

    let cond = Box::new(
        Condenser::new(2200.0)
            .with_refrigerant(ref_fluid)
            .with_secondary_fluid(water)
            .with_fluid_backend(backend.clone())
            .with_emergent_pressure(5.0),
    );

    let exv = Box::new(
        IsenthalpicExpansionValve::new(278.15)
            .with_refrigerant(ref_fluid)
            .with_fluid_backend(backend.clone())
            .with_emergent_pressure(),
    );

    // quality_control=false → saturated-vapor suction closure (default).
    let evap = Box::new(
        FloodedEvaporator::new(9000.0)
            .with_refrigerant(ref_fluid)
            .with_secondary_fluid(water)
            .with_fluid_backend(backend.clone())
            .with_quality_control(false),
    );

    // TestBackend Water P-T is only valid near 1 atm liquid — keep p ≤ 1.05 bar.
    let p_water = Pressure::from_bar(1.0);
    let cond_src = Box::new(
        BrineSource::new(
            water,
            p_water,
            Temperature::from_celsius(30.0),
            Concentration::from_percent(0.0),
            backend.clone(),
            dummy_port(water),
        )
        .expect("cond BrineSource")
        .with_imposed_mass_flow(0.45)
        .expect("cond m_flow"),
    );
    let cond_sink = Box::new(
        BrineSink::new(
            water,
            p_water,
            None,
            None,
            backend.clone(),
            dummy_port(water),
        )
        .expect("cond BrineSink"),
    );
    let evap_src = Box::new(
        BrineSource::new(
            water,
            p_water,
            Temperature::from_celsius(12.0),
            Concentration::from_percent(0.0),
            backend.clone(),
            dummy_port(water),
        )
        .expect("evap BrineSource")
        .with_imposed_mass_flow(0.55)
        .expect("evap m_flow"),
    );
    let evap_sink = Box::new(
        BrineSink::new(water, p_water, None, None, backend, dummy_port(water))
            .expect("evap BrineSink"),
    );

    let mut system = System::new();
    let n_comp = system.add_component(comp);
    let n_cond = system.add_component(cond);
    let n_exv = system.add_component(exv);
    let n_evap = system.add_component(evap);
    let n_cwi = system.add_component(cond_src);
    let n_cwo = system.add_component(cond_sink);
    let n_ewi = system.add_component(evap_src);
    let n_ewo = system.add_component(evap_sink);

    system.register_component_name("comp", n_comp);
    system.register_component_name("cond", n_cond);
    system.register_component_name("exv", n_exv);
    system.register_component_name("evap", n_evap);
    system.register_component_name("cond_water_in", n_cwi);
    system.register_component_name("cond_water_out", n_cwo);
    system.register_component_name("evap_water_in", n_ewi);
    system.register_component_name("evap_water_out", n_ewo);

    // Refrigerant loop: outlet(1) → inlet(0). get_ports() empty → indices kept as-is.
    system
        .add_edge_with_ports(n_comp, 1, n_cond, 0)
        .expect("comp→cond");
    system
        .add_edge_with_ports(n_cond, 1, n_exv, 0)
        .expect("cond→exv");
    system
        .add_edge_with_ports(n_exv, 1, n_evap, 0)
        .expect("exv→evap");
    system
        .add_edge_with_ports(n_evap, 1, n_comp, 0)
        .expect("evap→comp");

    // Condenser water: source outlet(0) → cond secondary_in(2); cond secondary_out(3) → sink(0)
    system
        .add_edge_with_ports(n_cwi, 0, n_cond, 2)
        .expect("cw in");
    system
        .add_edge_with_ports(n_cond, 3, n_cwo, 0)
        .expect("cw out");

    // Evaporator water
    system
        .add_edge_with_ports(n_ewi, 0, n_evap, 2)
        .expect("chw in");
    system
        .add_edge_with_ports(n_evap, 3, n_ewo, 0)
        .expect("chw out");

    system
        .finalize()
        .expect("finalize flooded water-cooled graph");
    system
}

#[test]
fn flooded_watercooled_4port_is_dof_balanced() {
    let system = build_flooded_watercooled();
    let report = system.dof_report();

    assert_eq!(
        report.n_unknowns,
        19,
        "unknowns: 3 branches + 2×8 edges = 19\n{}",
        report.summary()
    );
    assert_eq!(
        report.n_equations,
        19,
        "equations must match unknowns\n{}",
        report.summary()
    );
    assert_eq!(
        report.balance,
        SystemDofBalance::Balanced,
        "square system required\n{}",
        report.summary()
    );
    assert!(
        system.validate_system_dof().is_ok(),
        "hard DoF gate must pass\n{}",
        report.summary()
    );

    // Flooded block must declare saturated-vapor closure (not quality) by default.
    let evap = report
        .components
        .iter()
        .find(|c| c.component_name == "evap")
        .expect("evap in ledger");
    // Secondary isobaric P closure was added for the Modelica MassFlowSource_T
    // (Free P) pattern: the HX propagates the sink pressure to the source edge.
    assert_eq!(
        evap.n_equations, 5,
        "ΔP + energy + sat-vapor + secondary P + secondary energy"
    );
    assert!(
        evap.roles.iter().any(|r| matches!(
            r,
            EquationRole::OutletClosure {
                kind: "saturated_vapor"
            }
        )),
        "expected saturated_vapor outlet closure, got {:?}",
        evap.roles
    );
}

#[test]
fn quality_control_without_extra_free_still_same_equation_count() {
    // quality_control replaces sat-vapor residual — n_equations must stay constant
    // (no silent DoF jump). This guards against re-introducing +1 without free.
    let backend: Arc<dyn FluidBackend> = Arc::new(TestBackend::new());
    let mut with_q = FloodedEvaporator::new(9000.0)
        .with_refrigerant("R134a")
        .with_secondary_fluid("Water")
        .with_fluid_backend(backend.clone())
        .with_quality_control(true);
    let mut without_q = FloodedEvaporator::new(9000.0)
        .with_refrigerant("R134a")
        .with_secondary_fluid("Water")
        .with_fluid_backend(backend)
        .with_quality_control(false);

    // Wire same 4-port context so n_secondary matches.
    let ports = [
        Some((0, 1, 2)),
        Some((0, 3, 4)),
        Some((5, 6, 7)),
        Some((5, 8, 9)),
    ];
    with_q.set_port_context(&ports);
    without_q.set_port_context(&ports);

    assert_eq!(
        with_q.n_equations(),
        without_q.n_equations(),
        "quality_control must replace sat-vapor closure, not add a residual"
    );
    // 3 refrigerant rows (ΔP + energy + closure) + 2 secondary (P + energy, same branch).
    assert_eq!(with_q.n_equations(), 5);
}

#[test]
fn overconstrained_extra_quality_anchor_is_rejected_by_finalize_gate() {
    // If someone stacked an extra outlet closure without freeing an unknown,
    // finalize with enforce_dof_gate must refuse (over-constrained).
    // Here we only assert the public gate API rejects imbalance when equations > unknowns
    // using the already-balanced system as baseline — flip by adding a free residual mock
    // is covered in dof_balance.rs. This test documents the expected production policy.
    let system = build_flooded_watercooled();
    match system.validate_system_dof() {
        Ok(()) => {}
        Err(SystemDofError::Imbalance { .. }) => {
            panic!("balanced flooded machine must not report Imbalance")
        }
        Err(e) => panic!("unexpected DoF error: {e}"),
    }
}
