//! Integration tests for failure diagnostics propagation.
//!
//! Verifies that solver failures carry `ConvergenceDiagnostics` including
//! dominant residual index and value, satisfying spec-cli-failure-diagnostics.md AC1.

use std::sync::{
    atomic::{AtomicUsize, Ordering},
    Arc,
};

use entropyk_components::{
    Component, ComponentError, ConnectedPort, JacobianBuilder, ResidualVector, StateSlice,
};
use entropyk_core::MassFlow;
use entropyk_solver::{
    solver::{NewtonConfig, Solver, SolverError, VerboseConfig},
    system::System,
    CircuitId, PicardConfig,
};

// ── Port-reading mock (constant residuals) ──────────────────────────────────
//
// Used for Picard and no-verbose tests where state-dependent residuals are not
// needed. The residuals are constant (don't depend on the state vector) but
// non-zero — the solver iterates until max_iterations without converging.
//
// For Picard this is fine; for Newton this produces a singular Jacobian (zero
// finite-difference columns), so Newton fails at iteration 1 without recording
// any iteration diagnostics.

use entropyk_components::port::{Connected, FluidId, Port};
use entropyk_core::{Enthalpy, Pressure};

type CP = Port<Connected>;

struct PortMock {
    port_in: CP,
    port_out: CP,
    dp_pa: f64,
    dh_jkg: f64,
    /// Number of equations this mock reports to the solver.
    /// CM1.4: in a 2-edge series cycle, state_len = 1 branch + 4 P,h = 5.
    /// Use 3 for the "compressor" (pressure reference) and 2 for the "condenser"
    /// to reach 3+2=5 total equations, matching state_len.
    n_eqs: usize,
}

impl Component for PortMock {
    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() + self.dp_pa);
        r[1] = self.port_out.enthalpy().to_joules_per_kg()
            - (self.port_in.enthalpy().to_joules_per_kg() + self.dh_jkg);
        if self.n_eqs >= 3 {
            r[2] = 0.0; // mass balance trivially satisfied (mock)
        }
        Ok(())
    }
    fn jacobian_entries(
        &self,
        _s: &StateSlice,
        _j: &mut JacobianBuilder,
    ) -> Result<(), ComponentError> {
        Ok(())
    }
    fn n_equations(&self) -> usize {
        self.n_eqs
    }
    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 make_cp(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),
    ))
    .expect("port connect ok");
    connected
}

/// Build a 2-component closed loop whose residuals are constant (port-based).
/// The loop is physically inconsistent: compressor imposes +1 MPa pressure rise
/// while condenser imposes 0 pressure drop around the same loop, so the system
/// has no solution. Suitable for Picard tests (which don't need the Jacobian).
fn build_port_loop() -> System {
    let p_high = 1_200_000.0_f64;
    let p_low = 300_000.0_f64;
    let h_high = 450_000.0_f64;
    let h_low = 250_000.0_f64;

    let mut system = System::new();
    let cid = CircuitId(0);

    // CM1.4: 2-edge series cycle → 1 branch + 4 P,h = 5 state unknowns.
    // Compressor acts as the pressure-reference node (3 equations); condenser
    // is a pure series-branch component (2 equations). Total: 3+2=5 = balanced.
    let comp = PortMock {
        port_in: make_cp(p_low, h_low),
        port_out: make_cp(p_high, h_high),
        dp_pa: 900_000.0,
        dh_jkg: 200_000.0,
        n_eqs: 3,
    };
    let cond = PortMock {
        port_in: make_cp(p_high, h_high),
        port_out: make_cp(p_low, h_low),
        dp_pa: 0.0,
        dh_jkg: -200_000.0,
        n_eqs: 2,
    };

    let n0 = system
        .add_component_to_circuit(Box::new(comp), cid)
        .unwrap();
    let n1 = system
        .add_component_to_circuit(Box::new(cond), cid)
        .unwrap();
    system.add_edge(n0, n1).unwrap();
    system.add_edge(n1, n0).unwrap();
    system.finalize().unwrap();
    system
}

// ── State-reading mock with nonlinear residuals ─────────────────────────────
//
// Constrains (P, h) of an edge using a weakly nonlinear equation:
//   r[0] = state[pi] + C * state[pi]^3 - p_target
//   r[1] = state[hi] + C * state[hi]^3 - h_target
//
// The cubic perturbation (C = 1e-10) is small enough to leave the Jacobian
// well-conditioned but large enough to prevent Newton from reaching residual
// zero in one step. For p_target = 1000:
//
//   After step 1 (from state=0): state[pi] ≈ 1000,
//     residual ≈ C * target^3 = 1e-10 * 1e9 = 0.1  >> 1e-100
//   After 4-5 iterations: residual ≈ machine epsilon (1e-16) >> 1e-100
//
// Newton never meets tolerance = 1e-100, so NonConvergence is returned with a
// full iteration history and a non-zero dominant residual — satisfying AC1.

// C_NL = 1e-3: strong enough cubic perturbation so Newton converges slowly
// (residual ~7000 after 5 steps, >> 1e-100), but weak enough to avoid immediate
// divergence (each step reduces the residual monotonically toward x* ≈ 97 Pa).
const C_NL: f64 = 1e-3;

struct StateReadingMock {
    p_idx: Arc<AtomicUsize>,
    h_idx: Arc<AtomicUsize>,
    p_target: f64,
    h_target: f64,
}

impl Component for StateReadingMock {
    fn compute_residuals(
        &self,
        state: &StateSlice,
        r: &mut ResidualVector,
    ) -> Result<(), ComponentError> {
        let pi = self.p_idx.load(Ordering::Relaxed);
        let hi = self.h_idx.load(Ordering::Relaxed);
        r[0] = state[pi] + C_NL * state[pi].powi(3) - self.p_target;
        r[1] = state[hi] + C_NL * state[hi].powi(3) - self.h_target;
        Ok(())
    }

    fn jacobian_entries(
        &self,
        state: &StateSlice,
        j: &mut JacobianBuilder,
    ) -> Result<(), ComponentError> {
        let pi = self.p_idx.load(Ordering::Relaxed);
        let hi = self.h_idx.load(Ordering::Relaxed);
        j.add_entry(0, pi, 1.0 + 3.0 * C_NL * state[pi].powi(2));
        j.add_entry(1, hi, 1.0 + 3.0 * C_NL * state[hi].powi(2));
        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)])
    }
}

/// Build a 2-component, 2-edge system with nonlinear, state-dependent residuals.
///
/// Each component constrains (P, h) of one edge through a mildly nonlinear
/// equation (cubic perturbation). The Jacobian is non-singular (diagonal,
/// values ≈ 1.0003). Newton takes real steps but cannot reach tolerance 1e-100
/// before `max_iterations` — residuals bottom out at machine epsilon (~1e-16)
/// which is still >> 1e-100.
///
/// State indices are injected post-finalization via `Arc<AtomicUsize>`.
fn build_state_reading_loop() -> System {
    let p0 = Arc::new(AtomicUsize::new(0));
    let h0 = Arc::new(AtomicUsize::new(0));
    let p1 = Arc::new(AtomicUsize::new(0));
    let h1 = Arc::new(AtomicUsize::new(0));

    let comp = StateReadingMock {
        p_idx: Arc::clone(&p0),
        h_idx: Arc::clone(&h0),
        p_target: 1000.0, // 1000 Pa — small but arbitrary for the test
        h_target: 500.0,
    };
    let cond = StateReadingMock {
        p_idx: Arc::clone(&p1),
        h_idx: Arc::clone(&h1),
        p_target: 800.0,
        h_target: 300.0,
    };

    let mut system = System::new();
    let cid = CircuitId(0);
    let n0 = system
        .add_component_to_circuit(Box::new(comp), cid)
        .unwrap();
    let n1 = system
        .add_component_to_circuit(Box::new(cond), cid)
        .unwrap();
    let edge0 = system.add_edge(n0, n1).unwrap();
    let edge1 = system.add_edge(n1, n0).unwrap();
    system.finalize().unwrap();

    // Inject real state indices now that finalization has assigned them.
    let (p0_real, h0_real) = system.edge_state_indices(edge0);
    let (p1_real, h1_real) = system.edge_state_indices(edge1);
    p0.store(p0_real, Ordering::Relaxed);
    h0.store(h0_real, Ordering::Relaxed);
    p1.store(p1_real, Ordering::Relaxed);
    h1.store(h1_real, Ordering::Relaxed);

    system
}

// ── AC1: solver failure carries dominant residual diagnostics ─────────────────

/// Newton on a state-reading mock system with tolerance=1e-100:
/// - Jacobian is non-singular (permuted identity) → Newton takes real steps.
/// - After max_iterations=5, NonConvergence is returned.
/// - Error carries ConvergenceDiagnostics with non-empty iteration history.
/// - final_dominant_residual() returns Some with a positive value.
///
/// Validates AC1 of spec-cli-failure-diagnostics.md for the Newton solver.
#[test]
fn test_newton_failure_carries_dominant_residual_diagnostics() {
    let mut system = build_state_reading_loop();

    let verbose = VerboseConfig {
        enabled: true,
        log_residuals: true,
        log_jacobian_condition: false,
        log_solver_switches: false,
        dump_final_state: false,
        output_format: Default::default(),
    };

    let mut solver = NewtonConfig {
        max_iterations: 5,
        tolerance: 1e-100, // impossible — machine-epsilon residuals keep Newton spinning
        verbose_config: verbose,
        ..NewtonConfig::default()
    };

    let result = solver.solve(&mut system);
    assert!(
        result.is_err(),
        "Solver must fail to converge to tolerance 1e-100"
    );

    let err = result.unwrap_err();

    // Base error must be an iterative failure (NonConvergence or Divergence),
    // not a structural InvalidSystem error.
    let is_iterative_failure = matches!(
        err.base_error(),
        SolverError::NonConvergence { .. } | SolverError::Divergence { .. }
    );
    assert!(
        is_iterative_failure,
        "Base error must be iterative failure, got: {:?}",
        err.base_error()
    );

    // Diagnostics must be attached (verbose mode was enabled and iterations occurred).
    let diag = err
        .diagnostics()
        .expect("SolverError must carry ConvergenceDiagnostics when verbose mode is enabled");

    // At least one iteration must have been recorded.
    assert!(
        !diag.iteration_history.is_empty(),
        "Diagnostics must contain at least one iteration record (got {})",
        diag.iteration_history.len()
    );

    // final_residual must be positive (system never converged to 1e-100).
    assert!(
        diag.final_residual >= 0.0,
        "final_residual must be non-negative, got {}",
        diag.final_residual
    );

    // Dominant residual must be extractable from iteration history.
    let (dom_index, dom_value) = diag
        .final_dominant_residual()
        .expect("final_dominant_residual must return Some when iteration_history is non-empty");

    assert!(
        dom_value >= 0.0,
        "dominant residual value must be non-negative, got {}",
        dom_value
    );

    // The dominant index must be a valid equation index.
    // System: 2 components × 2 equations + 2 closure = 6 total equations.
    assert!(
        dom_index < 30,
        "dominant residual index out of expected range: {}",
        dom_index
    );
}

/// Picard on a port-loop (constant residuals, non-zero) with tolerance=1e-100:
/// - Picard doesn't use a Jacobian → iterates regardless of Jacobian singularity.
/// - After max_iterations=3, NonConvergence is returned with non-empty history.
/// - final_dominant_residual() returns Some with a positive value.
///
/// Validates AC1 for the Picard solver (mirrors Newton AC1).
#[test]
fn test_picard_failure_carries_dominant_residual_diagnostics() {
    let mut system = build_port_loop();

    let verbose = VerboseConfig {
        enabled: true,
        log_residuals: true,
        ..VerboseConfig::default()
    };

    let mut solver = PicardConfig {
        max_iterations: 3,
        tolerance: 1e-12,
        verbose_config: verbose,
        ..PicardConfig::default()
    };

    let result = solver.solve(&mut system);
    assert!(result.is_err());

    let err = result.unwrap_err();

    let diag = err
        .diagnostics()
        .expect("Picard error must carry ConvergenceDiagnostics on iterative failure");

    assert!(
        !diag.iteration_history.is_empty(),
        "Picard diagnostics must contain at least one iteration"
    );

    assert!(
        diag.final_dominant_residual().is_some(),
        "Picard diagnostics must expose dominant residual"
    );

    let (_, dom_value) = diag.final_dominant_residual().unwrap();
    assert!(dom_value >= 0.0);
}

/// Without verbose mode, solver errors carry no diagnostics regardless of
/// failure type — verifying backward compatibility for callers that opt out
/// of verbose instrumentation.
#[test]
fn test_failure_without_verbose_carries_no_diagnostics() {
    let mut system = build_port_loop();

    let mut solver = NewtonConfig {
        max_iterations: 2,
        tolerance: 1e-12,
        verbose_config: VerboseConfig::default(), // verbose disabled
        ..NewtonConfig::default()
    };

    let result = solver.solve(&mut system);
    assert!(result.is_err());

    let err = result.unwrap_err();

    assert!(
        err.diagnostics().is_none(),
        "No diagnostics expected when verbose mode is disabled"
    );
}
