//! End-to-end **closed-loop capacity control** integration test.
//!
//! This exercises the design/control vertical slice built on top of the
//! emergent-pressure cycle:
//!
//!   * The evaporator cooling capacity is measured with REAL thermodynamics
//!     (`Component::measure_output(Capacity, …)` → `energy_transfers`), NOT the
//!     legacy placeholder formula.
//!   * The compressor exposes a genuine actuator: the inverse-control variable
//!     `f_m` scales the swept mass flow in its residual `r0 = ṁ − f_m·ṁ_calc`
//!     and emits the matching Jacobian column `∂r0/∂f_m = −ṁ_calc`.
//!
//! A `Capacity` constraint on the evaporator is linked to an `f_m`
//! `BoundedVariable` on the compressor. The solver must therefore find the
//! compressor loading that makes the emergent cooling capacity meet the target —
//! this is the core "design a machine to a duty" loop, with no bricolage.
//!
//! Requires the `coolprop` feature (entropy + saturation properties):
//!   cargo test -p entropyk-solver --features coolprop --test capacity_control_integration
#![cfg(feature = "coolprop")]

use std::sync::Arc;

use entropyk_components::isentropic_compressor::VolumetricEfficiency;
use entropyk_components::{Condenser, Evaporator, IsenthalpicExpansionValve, IsentropicCompressor};
use entropyk_fluids::{CoolPropBackend, FluidBackend};
use entropyk_solver::inverse::{
    BoundedVariable, BoundedVariableId, ComponentOutput, Constraint, ConstraintId,
};
use entropyk_solver::solver::Solver;
use entropyk_solver::system::System;
use entropyk_solver::{FallbackSolver, NewtonConfig};

/// Base emergent-cycle state layout (9 unknowns, same-branch series loop):
/// `[ṁ, P0,h0, P1,h1, P2,h2, P3,h3]`.
const N_BASE: usize = 9;

/// Assembles the emergent cycle. When `capacity_target` is `Some(w)`, a
/// `Capacity` constraint on the evaporator is linked to an `f_m` actuator on the
/// compressor (closed-loop capacity control). Returns `(ṁ, q_evap, f_m)`.
fn solve(capacity_target: Option<f64>) -> (f64, f64, f64) {
    let backend: Arc<dyn FluidBackend> = Arc::new(CoolPropBackend::new());
    let fluid = "R134a";

    let comp = Box::new(
        IsentropicCompressor::new(0.70, 318.15, 278.15, 5.0)
            .with_refrigerant(fluid)
            .with_fluid_backend(backend.clone())
            .with_displacement(6.5e-5, 50.0, VolumetricEfficiency::Constant(0.92)),
    );
    let cond = Box::new(
        Condenser::new(766.0)
            .with_refrigerant(fluid)
            .with_fluid_backend(backend.clone())
            .with_secondary_stream(303.15, 1500.0)
            .with_emergent_pressure(5.0),
    );
    let exv = Box::new(
        IsenthalpicExpansionValve::new(278.15)
            .with_refrigerant(fluid)
            .with_fluid_backend(backend.clone())
            .with_emergent_pressure(),
    );
    let evap = Box::new(
        Evaporator::new(1468.0)
            .with_refrigerant(fluid)
            .with_fluid_backend(backend.clone())
            .with_secondary_stream(285.15, 2000.0)
            .with_emergent_pressure(),
    );

    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);

    system.register_component_name("compressor", n_comp);
    system.register_component_name("evaporator", n_evap);

    system.add_edge(n_comp, n_cond).unwrap(); // E0 comp→cond
    system.add_edge(n_cond, n_exv).unwrap(); // E1 cond→exv
    system.add_edge(n_exv, n_evap).unwrap(); // E2 exv→evap
    system.add_edge(n_evap, n_comp).unwrap(); // E3 evap→comp

    if let Some(target_w) = capacity_target {
        // Constraint: evaporator cooling capacity = target (real ε-NTU duty).
        system
            .add_constraint(Constraint::new(
                ConstraintId::new("capacity_control"),
                ComponentOutput::Capacity {
                    component_id: "evaporator".to_string(),
                },
                target_w,
            ))
            .unwrap();

        // Actuator: compressor mass-flow multiplier f_m ∈ [0.5, 2.0]. The `f_m`
        // suffix wires it into the compressor's CalibIndices during finalize().
        let bv = BoundedVariable::with_component(
            BoundedVariableId::new("compressor_f_m"),
            "compressor",
            1.0,
            0.5,
            2.0,
        )
        .unwrap();
        system.add_bounded_variable(bv).unwrap();

        system
            .link_constraint_to_control(
                &ConstraintId::new("capacity_control"),
                &BoundedVariableId::new("compressor_f_m"),
            )
            .unwrap();
    }

    system.finalize().unwrap();

    // Physically-consistent seed near the expected operating point.
    let mut initial_state = vec![
        0.05, // ṁ  [kg/s]
        11.6e5, 445e3, // E0 comp→cond : P_cond, h_dis
        11.6e5, 262e3, // E1 cond→exv  : P_cond, h_liq
        3.50e5, 262e3, // E2 exv→evap  : P_evap, h (isenthalpic)
        3.50e5, 405e3, // E3 evap→comp : P_evap, h_suction (superheated)
    ];
    debug_assert_eq!(initial_state.len(), N_BASE);
    // Append control / coupling slots (f_m seeded at its nominal 1.0, rest 0).
    let n_full = system.full_state_vector_len();
    while initial_state.len() < n_full {
        initial_state.push(if initial_state.len() == N_BASE {
            1.0
        } else {
            0.0
        });
    }

    let config = NewtonConfig {
        max_iterations: 300,
        tolerance: 1e-6,
        line_search: true,
        use_numerical_jacobian: false,
        initial_state: Some(initial_state.clone()),
        ..NewtonConfig::default()
    };

    let mut solver = FallbackSolver::default_solver()
        .with_newton_config(config)
        .with_initial_state(initial_state);

    let converged = solver
        .solve(&mut system)
        .unwrap_or_else(|e| panic!("solve(target={:?}) must converge: {:?}", capacity_target, e));

    let sv = &converged.state;
    let m_dot = sv[0];
    let h_evap_in = sv[6];
    let h_evap_out = sv[8];
    let q_evap = m_dot * (h_evap_out - h_evap_in);
    // f_m lives at total_state_len + 0 (first/only linked control) when present.
    let f_m = if capacity_target.is_some() {
        sv[N_BASE]
    } else {
        1.0
    };

    (m_dot, q_evap, f_m)
}

/// The compressor `f_m` actuator must genuinely drive the emergent cooling
/// capacity to a commanded target: a higher capacity target must be met by a
/// higher solved mass flow AND a higher compressor loading `f_m`.
#[test]
fn test_capacity_target_drives_compressor_loading() {
    // 1. Nominal (uncontrolled, f_m = 1) capacity of this machine.
    let (m_nom, q_nom, _) = solve(None);
    assert!(q_nom > 0.0, "nominal capacity must be positive: {}", q_nom);
    assert!(m_nom > 0.0, "nominal mass flow must be positive: {}", m_nom);

    // 2. Two achievable targets bracketing the nominal point (within f_m range).
    let target_low = 0.85 * q_nom;
    let target_high = 1.15 * q_nom;

    let (m_low, q_low, fm_low) = solve(Some(target_low));
    let (m_high, q_high, fm_high) = solve(Some(target_high));

    // The closed loop meets each commanded capacity (5 % tolerance).
    assert!(
        (q_low - target_low).abs() < 0.05 * target_low,
        "low target not met: got {:.0} W, wanted {:.0} W",
        q_low,
        target_low
    );
    assert!(
        (q_high - target_high).abs() < 0.05 * target_high,
        "high target not met: got {:.0} W, wanted {:.0} W",
        q_high,
        target_high
    );

    // Higher duty ⇒ more mass flow AND more compressor loading — the actuator
    // is doing real physical work, not being ignored.
    assert!(
        m_high > m_low,
        "higher capacity must raise solved ṁ: {:.4} → {:.4} kg/s",
        m_low,
        m_high
    );
    assert!(
        fm_high > fm_low,
        "higher capacity must raise compressor loading z_flow: {:.3} → {:.3}",
        fm_low,
        fm_high
    );
    // f_m must stay within its declared bounds.
    assert!(
        (0.5..=2.0).contains(&fm_low) && (0.5..=2.0).contains(&fm_high),
        "f_m out of bounds: {:.3}, {:.3}",
        fm_low,
        fm_high
    );
}
