//! Integration test for the Newton-homotopy continuation solver.
//!
//! Builds the same 4-component R134a refrigeration loop used by the Newton
//! integration test (`refrigeration_cycle_integration.rs`) and solves it with
//! [`HomotopyConfig`] instead of `NewtonConfig`. The purpose is to prove the
//! homotopy strategy integrates end-to-end with the real edge-based [`System`]
//! machinery (stride-3 `(ṁ, P, h)` state, finalize, mass-flow closures) and
//! returns a converged result.
//!
//! NOTE on the fixture: the mock components return `&[]` from `get_ports()`, so
//! the `System` cannot wire edges to their ports. Their residuals are therefore
//! read from the construction-time port values (set to the analytic solution)
//! and are independent of the live state vector. This mirrors the existing
//! Newton integration test, which for the same reason only asserts convergence
//! rather than specific state values. The numerical behaviour of the homotopy
//! continuation (λ-stepping, residual blending, restart-on-failure) is covered
//! by the unit tests in `strategies::homotopy`.

use entropyk_components::port::{Connected, FluidId, Port};
use entropyk_components::{
    Component, ComponentError, ConnectedPort, JacobianBuilder, ResidualVector, StateSlice,
};
use entropyk_core::{Enthalpy, MassFlow, Pressure};
use entropyk_solver::{
    solver::{Solver, SolverError},
    strategies::HomotopyConfig,
    system::{System, DEFAULT_MASS_FLOW_SEED_KG_S},
};

type CP = Port<Connected>;

// r[0] = p_disc - (p_suc + 1 MPa) ; r[1] = h_disc - (h_suc + 75 kJ/kg)
struct MockCompressor {
    port_suc: CP,
    port_disc: CP,
}
impl Component for MockCompressor {
    fn compute_residuals(
        &self,
        _s: &StateSlice,
        r: &mut ResidualVector,
    ) -> Result<(), ComponentError> {
        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() + 75_000.0);
        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),
        ])
    }
}

// r[0] = p_out - p_in ; r[1] = h_out - (h_in - 225 kJ/kg)
struct MockCondenser {
    port_in: CP,
    port_out: CP,
}
impl Component for MockCondenser {
    fn compute_residuals(
        &self,
        _s: &StateSlice,
        r: &mut ResidualVector,
    ) -> Result<(), ComponentError> {
        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() - 225_000.0);
        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),
        ])
    }
}

// r[0] = p_out - (p_in - 1 MPa) ; r[1] = h_out - h_in
struct MockValve {
    port_in: CP,
    port_out: CP,
}
impl Component for MockValve {
    fn compute_residuals(
        &self,
        _s: &StateSlice,
        r: &mut ResidualVector,
    ) -> Result<(), ComponentError> {
        r[0] = self.port_out.pressure().to_pascals()
            - (self.port_in.pressure().to_pascals() - 1_000_000.0);
        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),
        ])
    }
}

// r[0] = p_out - p_in ; r[1] = h_out - (h_in + 150 kJ/kg)
struct MockEvaporator {
    port_in: CP,
    port_out: CP,
}
impl Component for MockEvaporator {
    fn compute_residuals(
        &self,
        _s: &StateSlice,
        r: &mut ResidualVector,
    ) -> Result<(), ComponentError> {
        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() + 150_000.0);
        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 build_loop() -> System {
    let p_lp = 350_000.0_f64;
    let p_hp = 1_350_000.0_f64;

    let comp = Box::new(MockCompressor {
        port_suc: port(p_lp, 410_000.0),
        port_disc: port(p_hp, 485_000.0),
    });
    let cond = Box::new(MockCondenser {
        port_in: port(p_hp, 485_000.0),
        port_out: port(p_hp, 260_000.0),
    });
    let valv = Box::new(MockValve {
        port_in: port(p_hp, 260_000.0),
        port_out: port(p_lp, 260_000.0),
    });
    let evap = Box::new(MockEvaporator {
        port_in: port(p_lp, 260_000.0),
        port_out: port(p_lp, 410_000.0),
    });

    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();
    system
}

/// `HomotopyConfig` drives the real edge-based System machinery to a converged
/// result, just like `NewtonConfig` does on the same loop.
#[test]
fn test_homotopy_solves_refrigeration_loop() {
    let mut system = build_loop();

    let p_lp = 350_000.0_f64;
    let p_hp = 1_350_000.0_f64;
    let m = DEFAULT_MASS_FLOW_SEED_KG_S;
    // CM1.4 layout: 1 shared ṁ (single series branch) + (P, h) per edge.
    // state = [ṁ, P₀, h₀, P₁, h₁, P₂, h₂, P₃, h₃]  (9 elements)
    let initial_state = vec![
        m, // ṁ shared (branch 0)
        p_hp, 485_000.0, // edge0 comp→cond: P, h
        p_hp, 260_000.0, // edge1 cond→valve: P, h
        p_lp, 260_000.0, // edge2 valve→evap: P, h
        p_lp, 410_000.0, // edge3 evap→comp: P, h
    ];

    let mut solver = HomotopyConfig {
        use_numerical_jacobian: true, // mock analytic Jacobian is empty
        initial_state: Some(initial_state),
        ..HomotopyConfig::default()
    };

    let t0 = std::time::Instant::now();
    let result = solver
        .solve(&mut system)
        .expect("homotopy should converge on the refrigeration loop");
    let elapsed = t0.elapsed();

    assert!(
        result.final_residual < 1e-6,
        "final residual too large: {:.3e}",
        result.final_residual
    );
    assert!(elapsed.as_millis() < 5000, "should converge in < 5 s");
}

/// A caller-supplied `initial_state` whose length does not match the system
/// state vector must be rejected with `InvalidSystem` rather than silently
/// substituted by an all-zeros guess (which would hide the caller's bug).
#[test]
fn test_homotopy_rejects_mismatched_initial_state_length() {
    let mut system = build_loop();
    let n_state = system.full_state_vector_len();
    assert!(n_state > 0, "loop should have state variables");

    let mut solver = HomotopyConfig {
        use_numerical_jacobian: true,
        initial_state: Some(vec![0.0; n_state + 1]), // deliberately too long
        ..HomotopyConfig::default()
    };

    match solver.solve(&mut system) {
        Err(SolverError::InvalidSystem { message }) => {
            assert!(
                message.contains("initial_state length"),
                "unexpected message: {message}"
            );
        }
        other => panic!("expected InvalidSystem for length mismatch, got {other:?}"),
    }
}
