use entropyk_components::port::{Connected, FluidId, Port};
/// Integration test: calibrated refrigeration cycle vs synthetic test data.
///
/// Validates that Calib factors correctly scale component outputs and that
/// the solver converges on a calibrated cycle matching expected targets
/// within configurable tolerances (capacity ±2%, power ±3%).
///
/// The mock components form a self-consistent cycle for any Calib values:
///   Compressor : dp = +1 MPa,    dh = +75kJ × f_m × f_power
///   Condenser  : dp = -20kPa×f_dp, dh = -(75kJ×f_m×f_power + 150kJ×f_ua)
///   Valve      : dp = -(1MPa - 20kPa×f_dp), dh = 0  (isenthalpic)
///   Evaporator : dp = 0,         dh = +150kJ × f_ua
///
/// Energy balance: compressor_work + evaporator_absorption = condenser_rejection ✓
/// Pressure balance: closes for any f_dp ✓
use entropyk_components::{
    Component, ComponentError, ConnectedPort, JacobianBuilder, ResidualVector, StateSlice,
};
use entropyk_core::{Calib, MassFlow};
use entropyk_core::{Enthalpy, Pressure};
use entropyk_solver::{
    solver::{NewtonConfig, Solver},
    system::System,
    system::DEFAULT_MASS_FLOW_SEED_KG_S,
};

type CP = Port<Connected>;

// ─── Calibrated mock components ────────────────────────────────────────────────

struct CalibCompressor {
    port_suc: CP,
    port_disc: CP,
    calib: Calib,
}
impl Component for CalibCompressor {
    fn compute_residuals(
        &self,
        _s: &StateSlice,
        r: &mut ResidualVector,
    ) -> Result<(), ComponentError> {
        let dh_eff = 75_000.0 * self.calib.z_flow * self.calib.z_power;
        r[0] = self.port_disc.pressure().to_pascals()
            - (self.port_suc.pressure().to_pascals() + 1_000_000.0);
        r[1] = self.port_disc.enthalpy().to_joules_per_kg()
            - (self.port_suc.enthalpy().to_joules_per_kg() + dh_eff);
        Ok(())
    }
    fn jacobian_entries(
        &self,
        _s: &StateSlice,
        _j: &mut JacobianBuilder,
    ) -> Result<(), ComponentError> {
        Ok(())
    }
    fn n_equations(&self) -> usize {
        2
    }
    fn get_ports(&self) -> &[ConnectedPort] {
        &[]
    }
    fn port_mass_flows(&self, _: &StateSlice) -> Result<Vec<MassFlow>, ComponentError> {
        Ok(vec![
            MassFlow::from_kg_per_s(0.05),
            MassFlow::from_kg_per_s(-0.05),
        ])
    }
}

struct CalibCondenser {
    port_in: CP,
    port_out: CP,
    calib: Calib,
}
impl Component for CalibCondenser {
    fn compute_residuals(
        &self,
        _s: &StateSlice,
        r: &mut ResidualVector,
    ) -> Result<(), ComponentError> {
        let dp_eff = 20_000.0 * self.calib.z_dp;
        // Condenser rejects compressor work + evaporator load (energy balance)
        let dh_reject =
            75_000.0 * self.calib.z_flow * self.calib.z_power + 150_000.0 * self.calib.z_ua;
        r[0] =
            self.port_out.pressure().to_pascals() - (self.port_in.pressure().to_pascals() - dp_eff);
        r[1] = self.port_out.enthalpy().to_joules_per_kg()
            - (self.port_in.enthalpy().to_joules_per_kg() - dh_reject);
        Ok(())
    }
    fn jacobian_entries(
        &self,
        _s: &StateSlice,
        _j: &mut JacobianBuilder,
    ) -> Result<(), ComponentError> {
        Ok(())
    }
    fn n_equations(&self) -> usize {
        2
    }
    fn get_ports(&self) -> &[ConnectedPort] {
        &[]
    }
    fn port_mass_flows(&self, _: &StateSlice) -> Result<Vec<MassFlow>, ComponentError> {
        Ok(vec![
            MassFlow::from_kg_per_s(0.05),
            MassFlow::from_kg_per_s(-0.05),
        ])
    }
}

struct CalibValve {
    port_in: CP,
    port_out: CP,
    calib: Calib,
}
impl Component for CalibValve {
    fn compute_residuals(
        &self,
        _s: &StateSlice,
        r: &mut ResidualVector,
    ) -> Result<(), ComponentError> {
        let dp_eff = 1_000_000.0 - 20_000.0 * self.calib.z_dp;
        r[0] =
            self.port_out.pressure().to_pascals() - (self.port_in.pressure().to_pascals() - dp_eff);
        r[1] = self.port_out.enthalpy().to_joules_per_kg()
            - self.port_in.enthalpy().to_joules_per_kg();
        Ok(())
    }
    fn jacobian_entries(
        &self,
        _s: &StateSlice,
        _j: &mut JacobianBuilder,
    ) -> Result<(), ComponentError> {
        Ok(())
    }
    fn n_equations(&self) -> usize {
        2
    }
    fn get_ports(&self) -> &[ConnectedPort] {
        &[]
    }
    fn port_mass_flows(&self, _: &StateSlice) -> Result<Vec<MassFlow>, ComponentError> {
        Ok(vec![
            MassFlow::from_kg_per_s(0.05),
            MassFlow::from_kg_per_s(-0.05),
        ])
    }
}

struct CalibEvaporator {
    port_in: CP,
    port_out: CP,
    calib: Calib,
}
impl Component for CalibEvaporator {
    fn compute_residuals(
        &self,
        _s: &StateSlice,
        r: &mut ResidualVector,
    ) -> Result<(), ComponentError> {
        let dh_eff = 150_000.0 * self.calib.z_ua;
        r[0] = self.port_out.pressure().to_pascals() - self.port_in.pressure().to_pascals();
        r[1] = self.port_out.enthalpy().to_joules_per_kg()
            - (self.port_in.enthalpy().to_joules_per_kg() + dh_eff);
        Ok(())
    }
    fn jacobian_entries(
        &self,
        _s: &StateSlice,
        _j: &mut JacobianBuilder,
    ) -> Result<(), ComponentError> {
        Ok(())
    }
    fn n_equations(&self) -> usize {
        2
    }
    fn get_ports(&self) -> &[ConnectedPort] {
        &[]
    }
    fn port_mass_flows(&self, _: &StateSlice) -> Result<Vec<MassFlow>, ComponentError> {
        Ok(vec![
            MassFlow::from_kg_per_s(0.05),
            MassFlow::from_kg_per_s(-0.05),
        ])
    }
}

fn port(p_pa: f64, h_j_kg: f64) -> CP {
    let (connected, _) = Port::new(
        FluidId::new("R134a"),
        Pressure::from_pascals(p_pa),
        Enthalpy::from_joules_per_kg(h_j_kg),
    )
    .connect(Port::new(
        FluidId::new("R134a"),
        Pressure::from_pascals(p_pa),
        Enthalpy::from_joules_per_kg(h_j_kg),
    ))
    .unwrap();
    connected
}

fn make_calib() -> Calib {
    Calib {
        z_flow: 1.0,
        z_flow_eco: 1.0,
        z_dp: 1.0,
        z_ua: 1.0,
        z_power: 1.0,
        z_etav: 1.0,
        calibration_source: None,
    }
}

/// Compute the analytical solution for the calibrated cycle.
fn analytical_solution(calib: &Calib) -> [f64; 8] {
    let p3 = 350_000.0;
    let h3 = 410_000.0;
    let p0 = p3 + 1_000_000.0;
    let h0 = h3 + 75_000.0 * calib.z_flow * calib.z_power;
    let p1 = p0 - 20_000.0 * calib.z_dp;
    let h1 = h0 - 75_000.0 * calib.z_flow * calib.z_power - 150_000.0 * calib.z_ua;
    let p2 = p3;
    let h2 = h1;
    [p0, h0, p1, h1, p2, h2, p3, h3]
}

fn solve_calibrated_cycle(calib: &Calib) -> Vec<f64> {
    let sol = analytical_solution(calib);
    let comp = Box::new(CalibCompressor {
        port_suc: port(sol[6], sol[7]),
        port_disc: port(sol[0], sol[1]),
        calib: calib.clone(),
    });
    let cond = Box::new(CalibCondenser {
        port_in: port(sol[0], sol[1]),
        port_out: port(sol[2], sol[3]),
        calib: calib.clone(),
    });
    let valv = Box::new(CalibValve {
        port_in: port(sol[2], sol[3]),
        port_out: port(sol[4], sol[5]),
        calib: calib.clone(),
    });
    let evap = Box::new(CalibEvaporator {
        port_in: port(sol[4], sol[5]),
        port_out: port(sol[6], sol[7]),
        calib: calib.clone(),
    });

    let mut system = System::new();
    let n_comp = system.add_component(comp);
    let n_cond = system.add_component(cond);
    let n_valv = system.add_component(valv);
    let n_evap = system.add_component(evap);

    system.add_edge(n_comp, n_cond).unwrap();
    system.add_edge(n_cond, n_valv).unwrap();
    system.add_edge(n_valv, n_evap).unwrap();
    system.add_edge(n_evap, n_comp).unwrap();
    system.finalize().unwrap();

    // CM1.2: state layout is now (ṁ, P, h) per edge (stride 3). Map the analytical
    // (P, h) pairs onto the correct slots and seed each edge's mass flow.
    let mut initial_state = vec![0.0; system.full_state_vector_len()];
    for (i, edge_idx) in system.edge_indices().enumerate() {
        let (m_idx, p_idx, h_idx) = system.edge_state_indices_full(edge_idx);
        initial_state[m_idx] = DEFAULT_MASS_FLOW_SEED_KG_S;
        initial_state[p_idx] = sol[2 * i];
        initial_state[h_idx] = sol[2 * i + 1];
    }

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

    let result = config.solve(&mut system).unwrap().state;

    // CM1.2: re-extract the (P, h) pairs per edge so downstream assertions keep
    // the historical [p0, h0, p1, h1, ...] layout independent of the ṁ slots.
    let mut ph = vec![0.0; 2 * system.edge_count()];
    for (i, edge_idx) in system.edge_indices().enumerate() {
        let (p_idx, h_idx) = system.edge_state_indices(edge_idx);
        ph[2 * i] = result[p_idx];
        ph[2 * i + 1] = result[h_idx];
    }
    ph
}

/// Baseline: all Calib = 1.0 → results match nominal analytical solution.
#[test]
fn test_calibrated_cycle_nominal_baseline() {
    let calib = make_calib();
    let sv = solve_calibrated_cycle(&calib);
    let expected = analytical_solution(&calib);

    for i in 0..8 {
        let diff = (sv[i] - expected[i]).abs();
        assert!(
            diff < 10.0,
            "sv[{}]: got {}, expected {}, diff {}",
            i,
            sv[i],
            expected[i],
            diff
        );
    }

    // Energy balance check
    let dh_comp = sv[1] - sv[7];
    let dh_cond = sv[3] - sv[1];
    let dh_valve = sv[5] - sv[3];
    let dh_evap = sv[7] - sv[5];
    let imbalance = dh_comp + dh_cond + dh_valve + dh_evap;
    assert!(imbalance.abs() < 10.0, "Energy imbalance: {imbalance}");
}

/// f_ua = 1.1 on evaporator → capacity increases by 10% (±2% tolerance).
#[test]
fn test_calibrated_cycle_fua_increases_capacity() {
    let nom = make_calib();
    let cal = Calib {
        z_ua: 1.1,
        calibration_source: Some("synthetic-fua".into()),
        ..make_calib()
    };

    let sv_nom = solve_calibrated_cycle(&nom);
    let sv_cal = solve_calibrated_cycle(&cal);

    let dh_evap_nom = sv_nom[7] - sv_nom[5];
    let dh_evap_cal = sv_cal[7] - sv_cal[5];
    let capacity_ratio = dh_evap_cal / dh_evap_nom;

    assert!(
        (capacity_ratio - 1.10).abs() < 0.02,
        "Capacity ratio: {capacity_ratio:.4}, expected ~1.10 ±2%"
    );
}

/// f_m * f_power on compressor → compressor work scales accordingly (±3% tolerance).
#[test]
fn test_calibrated_cycle_fm_fpower_scales_compressor_work() {
    let nom = make_calib();
    let cal = Calib {
        z_flow: 1.05,
        z_power: 1.03,
        calibration_source: Some("test-bench-2024-A".into()),
        ..make_calib()
    };

    let sv_nom = solve_calibrated_cycle(&nom);
    let sv_cal = solve_calibrated_cycle(&cal);

    let dh_comp_nom = sv_nom[1] - sv_nom[7];
    let dh_comp_cal = sv_cal[1] - sv_cal[7];
    let power_ratio = dh_comp_cal / dh_comp_nom;
    let expected = 1.05 * 1.03;

    assert!(
        (power_ratio - expected).abs() < 0.03,
        "Power ratio: {power_ratio:.4}, expected ~{expected:.4} ±3%"
    );
}

/// f_dp on condenser → pressure drop scales by f_dp factor.
#[test]
fn test_calibrated_cycle_fdp_scales_pressure_drop() {
    let nom = make_calib();
    let cal = Calib {
        z_dp: 1.5,
        calibration_source: Some("dp-test-synthetic".into()),
        ..make_calib()
    };

    let sv_nom = solve_calibrated_cycle(&nom);
    let sv_cal = solve_calibrated_cycle(&cal);

    let dp_nom = sv_nom[2] - sv_nom[0]; // negative (pressure drop)
    let dp_cal = sv_cal[2] - sv_cal[0];
    let dp_ratio = dp_cal / dp_nom;

    assert!(
        (dp_ratio - 1.5).abs() < 0.05,
        "Pressure drop ratio: {dp_ratio:.4}, expected ~1.50 ±5%"
    );
}

/// Calib with calibration_source roundtrips through JSON and still produces correct results.
#[test]
fn test_calibrated_cycle_with_calibration_source_metadata() {
    let calib_json = r#"{
        "f_m": 1.0,
        "f_dp": 1.0,
        "f_ua": 1.1,
        "f_power": 1.0,
        "f_etav": 1.0,
        "calibration_source": "manufacturer-test-report-2024-TR-001"
    }"#;

    let calib: Calib = serde_json::from_str(calib_json).unwrap();
    assert_eq!(
        calib.calibration_source.as_deref(),
        Some("manufacturer-test-report-2024-TR-001")
    );
    assert_eq!(calib.z_ua, 1.1);

    let sv = solve_calibrated_cycle(&calib);

    // f_ua=1.1 → evaporator Δh = 150kJ × 1.1 = 165 kJ/kg
    let dh_evap = sv[7] - sv[5];
    assert!(
        (dh_evap - 165_000.0).abs() < 1_000.0,
        "Evaporator Δh with f_ua=1.1: {dh_evap:.0}, expected ~165000"
    );
}
