//! End-to-end saturated PI control integration test.
//!
//! The loop is co-solved with the emergent-pressure refrigeration cycle: the
//! saturated controller contributes `(u, x)` unknowns, wires compressor `f_m`
//! through `CalibIndices`, and measures real evaporator capacity from component
//! thermodynamics.
#![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, ConstraintId, SaturatedController,
    Saturation,
};
use entropyk_solver::solver::Solver;
use entropyk_solver::system::System;
use entropyk_solver::{FallbackSolver, NewtonConfig};

const N_BASE: usize = 9;

fn build_system(controller: Option<SaturatedController>) -> System {
    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();
    system.add_edge(n_cond, n_exv).unwrap();
    system.add_edge(n_exv, n_evap).unwrap();
    system.add_edge(n_evap, n_comp).unwrap();

    if let Some(ctrl) = controller {
        let bv = BoundedVariable::with_component(
            BoundedVariableId::new("compressor_f_m"),
            "compressor",
            1.0,
            ctrl.u_min(),
            ctrl.u_max(),
        )
        .unwrap();
        system.add_bounded_variable(bv).unwrap();
        system.add_saturated_controller(ctrl);
    }

    system.finalize().unwrap();
    system
}

fn seed_state(system: &System) -> Vec<f64> {
    let mut initial_state = vec![
        0.05, 11.6e5, 445e3, 11.6e5, 262e3, 3.50e5, 262e3, 3.50e5, 405e3,
    ];
    debug_assert_eq!(initial_state.len(), N_BASE);
    while initial_state.len() < system.full_state_vector_len() {
        initial_state.push(if initial_state.len() == N_BASE {
            1.0
        } else {
            0.0
        });
    }
    initial_state
}

fn solve_capacity(controller: Option<SaturatedController>) -> (f64, f64, f64, f64) {
    let mut system = build_system(controller);
    let initial_state = seed_state(&system);

    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!("saturated capacity solve must converge: {e:?}"));

    let state = &converged.state;
    let q_evap = state[0] * (state[8] - state[6]);
    let u = if system.saturated_controller_count() > 0 {
        state[N_BASE]
    } else {
        1.0
    };
    let x = if system.saturated_controller_count() > 0 {
        state[N_BASE + 1]
    } else {
        0.0
    };
    (state[0], q_evap, u, x)
}

fn capacity_controller(setpoint: f64, u_min: f64, u_max: f64) -> SaturatedController {
    SaturatedController::new(
        ConstraintId::new("capacity_sat_loop"),
        ComponentOutput::Capacity {
            component_id: "evaporator".to_string(),
        },
        BoundedVariableId::new("compressor_f_m"),
        setpoint,
        u_min,
        u_max,
    )
    .unwrap()
    .with_gain(1.0e-2)
    .unwrap()
    .with_band(1.0)
    .unwrap()
    .with_saturation(Saturation::Hard)
}

#[test]
fn saturated_lwt_control_tracks_when_unsaturated() {
    let (_m_nom, q_nom, _, _) = solve_capacity(None);
    assert!(q_nom > 0.0);

    let (_m, q, u, x) = solve_capacity(Some(capacity_controller(q_nom, 0.5, 1.5)));

    assert!(
        (q - q_nom).abs() < 0.03 * q_nom,
        "wide saturated loop should track nominal capacity: got {q:.1} W, target {q_nom:.1} W"
    );
    assert!(
        (0.5..=1.5).contains(&u) && x.abs() < 0.25,
        "controller should remain unsaturated: u={u:.4}, x={x:.4}"
    );
}

#[test]
fn saturated_lwt_control_pins_actuator_when_saturated() {
    let (_m_nom, q_nom, _, _) = solve_capacity(None);
    let target = 1.30 * q_nom;

    let (_m, q, u, x) = solve_capacity(Some(capacity_controller(target, 0.75, 1.0)));

    assert!(
        (u - 1.0).abs() < 2.0e-3,
        "tight loop should pin compressor f_m at upper bound: u={u:.6}"
    );
    assert!(
        x > 1.0,
        "anti-windup state should move beyond the saturation band: x={x:.4}"
    );
    assert!(
        (q - target).abs() > 0.10 * q_nom,
        "tracking error should be released at saturation: q={q:.1} W, target={target:.1} W"
    );
}
