//! Integration tests for Story 1.2: Core Crate & Typed Convergence Taxonomy.
//!
//! Covers:
//! - AC #1: non-converged terminations are reported as `ConvergenceReason`
//!   outcomes (via `SolverError::convergence_reason` and `Solver::solve_outcome`),
//!   while hard errors (invalid system, validation) remain `Err`.
//! - AC #3: existing `SolverError` behavior (incl. `WithDiagnostics`) is
//!   unchanged — the taxonomy is purely additive.

use entropyk_components::{Component, ComponentError, JacobianBuilder, ResidualVector, StateSlice};
use entropyk_solver::solver::{NewtonConfig, Solver, SolverError};
use entropyk_solver::system::{System, DEFAULT_MASS_FLOW_SEED_KG_S, MIN_SOLVER_PRESSURE_PA};
use entropyk_solver::{ConvergenceDiagnostics, ConvergenceReason, SolveOutcome};

// ─────────────────────────────────────────────────────────────────────────────
// Mock components (same fixture pattern as fallback_solver.rs)
// ─────────────────────────────────────────────────────────────────────────────

/// A well-conditioned linear system r = A·x − b: converges in one Newton step.
struct LinearSystem {
    a: Vec<Vec<f64>>,
    b: Vec<f64>,
    n: usize,
}

impl LinearSystem {
    fn well_conditioned() -> Self {
        let (p, h) = (MIN_SOLVER_PRESSURE_PA, MIN_SOLVER_PRESSURE_PA);
        Self {
            a: vec![vec![2.0, 1.0], vec![1.0, 2.0]],
            b: vec![2.0 * p + h, p + 2.0 * h],
            n: 2,
        }
    }
}

impl Component for LinearSystem {
    fn compute_residuals(
        &self,
        state: &StateSlice,
        residuals: &mut ResidualVector,
    ) -> Result<(), ComponentError> {
        for (i, residual) in residuals.iter_mut().enumerate().take(self.n) {
            let mut ax_i = 0.0;
            for j in 0..self.n {
                ax_i += self.a[i][j] * state[1 + j];
            }
            *residual = ax_i - self.b[i];
        }
        residuals[self.n] = state[0] - DEFAULT_MASS_FLOW_SEED_KG_S;
        Ok(())
    }

    fn jacobian_entries(
        &self,
        _state: &StateSlice,
        jacobian: &mut JacobianBuilder,
    ) -> Result<(), ComponentError> {
        for i in 0..self.n {
            for j in 0..self.n {
                jacobian.add_entry(i, 1 + j, self.a[i][j]);
            }
        }
        jacobian.add_entry(self.n, 0, 1.0);
        Ok(())
    }

    fn n_equations(&self) -> usize {
        self.n + 1
    }

    fn get_ports(&self) -> &[entropyk_components::ConnectedPort] {
        &[]
    }
}

/// A mildly non-linear system ((x − p₀)² = 1) parked at the pressure floor,
/// needing several Newton steps from an offset guess — deterministic
/// `MaxIters` generator when the iteration budget is 1.
struct QuadraticSystem;

impl Component for QuadraticSystem {
    fn compute_residuals(
        &self,
        state: &StateSlice,
        residuals: &mut ResidualVector,
    ) -> Result<(), ComponentError> {
        // r0 = (x − p₀)² − 1 (root at p₀ + 1, on the clip floor like the
        // established LinearSystem fixture; residual stays O(1) so neither
        // the divergence threshold nor pressure clipping interferes).
        let dx = state[1] - MIN_SOLVER_PRESSURE_PA;
        residuals[0] = dx * dx - 1.0;
        // r1 pins the second unknown.
        residuals[1] = state[2] - MIN_SOLVER_PRESSURE_PA;
        // Mass-flow row pins ṁ at the seed value.
        residuals[2] = state[0] - DEFAULT_MASS_FLOW_SEED_KG_S;
        Ok(())
    }

    fn jacobian_entries(
        &self,
        state: &StateSlice,
        jacobian: &mut JacobianBuilder,
    ) -> Result<(), ComponentError> {
        jacobian.add_entry(0, 1, 2.0 * (state[1] - MIN_SOLVER_PRESSURE_PA));
        jacobian.add_entry(1, 2, 1.0);
        jacobian.add_entry(2, 0, 1.0);
        Ok(())
    }

    fn n_equations(&self) -> usize {
        3
    }

    fn get_ports(&self) -> &[entropyk_components::ConnectedPort] {
        &[]
    }
}

fn create_test_system(component: Box<dyn Component>) -> System {
    let mut system = System::new();
    let n0 = system.add_component(component);
    system.add_edge(n0, n0).unwrap();
    system.finalize().unwrap();
    system
}

// ─────────────────────────────────────────────────────────────────────────────
// AC #1: SolverError classification into ConvergenceReason
// ─────────────────────────────────────────────────────────────────────────────

#[test]
fn non_convergence_maps_to_max_iters() {
    let err = SolverError::NonConvergence {
        iterations: 42,
        final_residual: 1.0e-3,
    };
    assert_eq!(err.convergence_reason(), Some(ConvergenceReason::MaxIters));
}

#[test]
fn timeout_maps_to_timed_out() {
    let err = SolverError::Timeout { timeout_ms: 500 };
    assert_eq!(err.convergence_reason(), Some(ConvergenceReason::TimedOut));
}

#[test]
fn divergence_maps_to_stalled() {
    let err = SolverError::Divergence {
        reason: "residual growing".to_string(),
    };
    assert_eq!(err.convergence_reason(), Some(ConvergenceReason::Stalled));
}

#[test]
fn with_diagnostics_delegates_to_inner_error() {
    let base = SolverError::NonConvergence {
        iterations: 10,
        final_residual: 1.0e-4,
    };
    let wrapped = SolverError::WithDiagnostics {
        error: Box::new(base),
        diagnostics: Box::new(ConvergenceDiagnostics::new()),
    };
    assert_eq!(
        wrapped.convergence_reason(),
        Some(ConvergenceReason::MaxIters)
    );
}

#[test]
fn hard_errors_map_to_none() {
    let invalid = SolverError::InvalidSystem {
        message: "empty system".to_string(),
    };
    assert_eq!(invalid.convergence_reason(), None);

    let validation = SolverError::Validation {
        mass_error: 1.0,
        energy_error: 2.0,
    };
    assert_eq!(validation.convergence_reason(), None);
}

// ─────────────────────────────────────────────────────────────────────────────
// AC #1: solve_outcome reports outcomes as data
// ─────────────────────────────────────────────────────────────────────────────

#[test]
fn solve_outcome_reports_converged_as_data() {
    let mut system = create_test_system(Box::new(LinearSystem::well_conditioned()));
    let mut solver = NewtonConfig::default();

    let outcome: SolveOutcome = solver
        .solve_outcome(&mut system)
        .expect("converged solve must not be a hard error");

    assert_eq!(outcome.reason, ConvergenceReason::Converged);
    assert!(outcome.is_converged());
    assert!(outcome.final_residual < 1e-6);
    assert!(outcome.state.is_some());
}

#[test]
fn solve_outcome_reports_max_iters_as_data_not_err() {
    let mut system = create_test_system(Box::new(QuadraticSystem));
    let mut solver = NewtonConfig {
        max_iterations: 1,
        tolerance: 1e-9,
        initial_state: Some(vec![
            DEFAULT_MASS_FLOW_SEED_KG_S,
            MIN_SOLVER_PRESSURE_PA + 10.0,
            MIN_SOLVER_PRESSURE_PA,
        ]),
        ..NewtonConfig::default()
    };

    let outcome = solver
        .solve_outcome(&mut system)
        .expect("MaxIters termination must be an outcome, not a hard Err");

    assert_eq!(outcome.reason, ConvergenceReason::MaxIters);
    assert!(!outcome.is_converged());
    assert!(outcome.iterations >= 1);
    assert!(outcome.final_residual.is_finite());
    assert!(outcome.state.is_none());
}

#[test]
fn solve_outcome_keeps_hard_errors_as_err() {
    // Empty system: InvalidSystem is a hard error and must remain `Err`.
    let mut system = System::new();
    system.finalize().unwrap();
    let mut solver = NewtonConfig::default();

    let result = solver.solve_outcome(&mut system);

    match result {
        Err(SolverError::InvalidSystem { .. }) => {}
        other => panic!("expected Err(InvalidSystem), got {:?}", other),
    }
}

// ─────────────────────────────────────────────────────────────────────────────
// AC #3: legacy behavior untouched
// ─────────────────────────────────────────────────────────────────────────────

#[test]
fn legacy_solve_still_returns_err_on_non_convergence() {
    // The legacy `solve` contract is unchanged: NonConvergence stays an `Err`.
    // Only the additive `solve_outcome` reports outcomes as data.
    let mut system = create_test_system(Box::new(QuadraticSystem));
    let mut solver = NewtonConfig {
        max_iterations: 1,
        tolerance: 1e-9,
        initial_state: Some(vec![
            DEFAULT_MASS_FLOW_SEED_KG_S,
            MIN_SOLVER_PRESSURE_PA + 10.0,
            MIN_SOLVER_PRESSURE_PA,
        ]),
        ..NewtonConfig::default()
    };

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

    assert!(matches!(result, Err(SolverError::NonConvergence { .. })));
}
