//! Integration tests for Story 4.7: Convergence Criteria & Validation.
//!
//! Tests cover all behaviour-level Acceptance Criteria:
//! - AC #7: ConvergenceCriteria integrates with Newton/Picard solvers
//! - AC #8: `convergence_report` field in `ConvergedState` (Some when criteria set, None by default)
//! - Backward compatibility: existing raw-tolerance workflow unchanged

use approx::assert_relative_eq;
use entropyk_solver::{
    system::{DEFAULT_MASS_FLOW_SEED_KG_S, MIN_SOLVER_PRESSURE_PA},
    CircuitConvergence, ConvergedState, ConvergenceCriteria, ConvergenceReport, ConvergenceStatus,
    FallbackSolver, NewtonConfig, PicardConfig, Solver, System,
};

// ─────────────────────────────────────────────────────────────────────────────
// AC #8: ConvergenceReport in ConvergedState
// ─────────────────────────────────────────────────────────────────────────────

/// Test that `ConvergedState::new` does NOT attach a report (backward-compat).
#[test]
fn test_converged_state_new_no_report() {
    let state = ConvergedState::new(
        vec![1.0, 2.0],
        10,
        1e-8,
        ConvergenceStatus::Converged,
        entropyk_solver::SimulationMetadata::new("".to_string()),
    );
    assert!(
        state.convergence_report.is_none(),
        "ConvergedState::new should not attach a report"
    );
}

/// Test that `ConvergedState::with_report` attaches a report.
#[test]
fn test_converged_state_with_report_attaches_report() {
    let report = ConvergenceReport {
        per_circuit: vec![CircuitConvergence {
            circuit_id: 0,
            pressure_ok: true,
            mass_ok: true,
            energy_ok: true,
            converged: true,
        }],
        globally_converged: true,
    };

    let state = ConvergedState::with_report(
        vec![1.0, 2.0],
        10,
        1e-8,
        ConvergenceStatus::Converged,
        report,
        entropyk_solver::SimulationMetadata::new("".to_string()),
    );

    assert!(
        state.convergence_report.is_some(),
        "with_report should attach a report"
    );
    assert!(state.convergence_report.unwrap().is_globally_converged());
}

// ─────────────────────────────────────────────────────────────────────────────
// AC #7: ConvergenceCriteria builder methods
// ─────────────────────────────────────────────────────────────────────────────

/// Test that `NewtonConfig::with_convergence_criteria` stores the criteria.
#[test]
fn test_newton_with_convergence_criteria_builder() {
    let criteria = ConvergenceCriteria::default();
    let cfg = NewtonConfig::default().with_convergence_criteria(criteria.clone());

    assert!(cfg.convergence_criteria.is_some());
    let stored = cfg.convergence_criteria.unwrap();
    assert_relative_eq!(stored.pressure_tolerance_pa, criteria.pressure_tolerance_pa);
}

/// Test that `PicardConfig::with_convergence_criteria` stores the criteria.
#[test]
fn test_picard_with_convergence_criteria_builder() {
    let criteria = ConvergenceCriteria {
        pressure_tolerance_pa: 0.5,
        mass_balance_tolerance_kgs: 1e-10,
        energy_balance_tolerance_w: 1e-4,
    };
    let cfg = PicardConfig::default().with_convergence_criteria(criteria.clone());

    assert!(cfg.convergence_criteria.is_some());
    let stored = cfg.convergence_criteria.unwrap();
    assert_relative_eq!(stored.pressure_tolerance_pa, 0.5);
    assert_relative_eq!(stored.mass_balance_tolerance_kgs, 1e-10);
}

/// Test that `FallbackSolver::with_convergence_criteria` delegates to both sub-solvers.
#[test]
fn test_fallback_with_convergence_criteria_delegates() {
    let criteria = ConvergenceCriteria::default();
    let solver = FallbackSolver::default_solver().with_convergence_criteria(criteria.clone());

    assert!(solver.newton_config.convergence_criteria.is_some());
    assert!(solver.picard_config.convergence_criteria.is_some());

    let newton_c = solver.newton_config.convergence_criteria.unwrap();
    let picard_c = solver.picard_config.convergence_criteria.unwrap();
    assert_relative_eq!(
        newton_c.pressure_tolerance_pa,
        criteria.pressure_tolerance_pa
    );
    assert_relative_eq!(
        picard_c.pressure_tolerance_pa,
        criteria.pressure_tolerance_pa
    );
}

/// Test backward-compat: Newton without criteria → `convergence_criteria` is `None`.
#[test]
fn test_newton_without_criteria_is_none() {
    let cfg = NewtonConfig::default();
    assert!(
        cfg.convergence_criteria.is_none(),
        "Default Newton should have no criteria"
    );
}

/// Test backward-compat: Picard without criteria → `convergence_criteria` is `None`.
#[test]
fn test_picard_without_criteria_is_none() {
    let cfg = PicardConfig::default();
    assert!(
        cfg.convergence_criteria.is_none(),
        "Default Picard should have no criteria"
    );
}

/// Test that Newton with empty system returns Err (no panic when criteria set).
#[test]
fn test_newton_with_criteria_empty_system_no_panic() {
    let mut sys = System::new();
    sys.finalize().unwrap();

    let mut solver =
        NewtonConfig::default().with_convergence_criteria(ConvergenceCriteria::default());

    // Empty system → wrapped error, no panic
    let result = solver.solve(&mut sys);
    assert!(result.is_err());
}

/// Test that Picard with empty system returns Err (no panic when criteria set).
#[test]
fn test_picard_with_criteria_empty_system_no_panic() {
    let mut sys = System::new();
    sys.finalize().unwrap();

    let mut solver =
        PicardConfig::default().with_convergence_criteria(ConvergenceCriteria::default());

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

// ─────────────────────────────────────────────────────────────────────────────
// ConvergenceCriteria type tests
// ─────────────────────────────────────────────────────────────────────────────

/// AC #1: Default pressure tolerance is 1.0 Pa.
#[test]
fn test_criteria_default_pressure_tolerance() {
    let c = ConvergenceCriteria::default();
    assert_relative_eq!(c.pressure_tolerance_pa, 1.0);
}

/// AC #2: Default mass balance tolerance is 1e-9 kg/s.
#[test]
fn test_criteria_default_mass_tolerance() {
    let c = ConvergenceCriteria::default();
    assert_relative_eq!(c.mass_balance_tolerance_kgs, 1e-9);
}

/// AC #3: Default energy balance tolerance is 1e-3 W (= 1e-6 kW).
#[test]
fn test_criteria_default_energy_tolerance() {
    let c = ConvergenceCriteria::default();
    assert_relative_eq!(c.energy_balance_tolerance_w, 1e-3);
}

/// AC #5: Global convergence only when ALL circuits converge.
#[test]
fn test_global_convergence_requires_all_circuits() {
    // 3 circuits, one fails → not globally converged
    let report = ConvergenceReport {
        per_circuit: vec![
            CircuitConvergence {
                circuit_id: 0,
                pressure_ok: true,
                mass_ok: true,
                energy_ok: true,
                converged: true,
            },
            CircuitConvergence {
                circuit_id: 1,
                pressure_ok: true,
                mass_ok: true,
                energy_ok: true,
                converged: true,
            },
            CircuitConvergence {
                circuit_id: 2,
                pressure_ok: false,
                mass_ok: true,
                energy_ok: true,
                converged: false,
            },
        ],
        globally_converged: false,
    };
    assert!(!report.is_globally_converged());
}

/// AC #5: Single-circuit system is a degenerate case of global convergence.
#[test]
fn test_single_circuit_global_convergence() {
    let report = ConvergenceReport {
        per_circuit: vec![CircuitConvergence {
            circuit_id: 0,
            pressure_ok: true,
            mass_ok: true,
            energy_ok: true,
            converged: true,
        }],
        globally_converged: true,
    };
    assert!(report.is_globally_converged());
}

// ─────────────────────────────────────────────────────────────────────────────
// AC #7: Integration Validation (Actual Solve)
// ─────────────────────────────────────────────────────────────────────────────

use entropyk_components::port::ConnectedPort;
use entropyk_components::{Component, ComponentError, JacobianBuilder, ResidualVector, StateSlice};

/// CM1.3 self-loop mock: 3 equations constraining (ṁ, P, h) on a single edge.
struct MockConvergingComponent;

impl Component for MockConvergingComponent {
    fn compute_residuals(
        &self,
        state: &StateSlice,
        residuals: &mut ResidualVector,
    ) -> Result<(), ComponentError> {
        // Pin P at the solver pressure floor (reachable under Newton clipping),
        // h at a matching abstract target, and ṁ at the canonical seed so the
        // single-edge self-loop is square (3 unknowns = 3 equations).
        residuals[0] = state[1] - MIN_SOLVER_PRESSURE_PA;
        residuals[1] = state[2] - MIN_SOLVER_PRESSURE_PA;
        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, 1.0);
        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) -> &[ConnectedPort] {
        &[]
    }
}

#[test]
fn test_newton_with_criteria_single_circuit() {
    let mut sys = System::new();
    let node1 = sys.add_component(Box::new(MockConvergingComponent));
    sys.add_edge(node1, node1).unwrap();
    sys.finalize().unwrap();

    let criteria = ConvergenceCriteria {
        pressure_tolerance_pa: 1.0,
        mass_balance_tolerance_kgs: 1e-1,
        energy_balance_tolerance_w: 1e-1,
    };

    let mut solver = NewtonConfig::default().with_convergence_criteria(criteria);
    let result = solver.solve(&mut sys).expect("Solver should converge");

    // Check that we got a report back
    assert!(result.convergence_report.is_some());
    let report = result.convergence_report.unwrap();
    assert!(report.is_globally_converged());
}

// ─────────────────────────────────────────────────────────────────────────────
// AC #7: Old tolerance field retained for backward-compat
// ─────────────────────────────────────────────────────────────────────────────

/// Test that old `tolerance` field is still accessible after setting criteria.
#[test]
fn test_backward_compat_tolerance_field_survives() {
    let criteria = ConvergenceCriteria::default();
    let cfg = NewtonConfig {
        tolerance: 1e-8,
        ..Default::default()
    }
    .with_convergence_criteria(criteria);

    // tolerance is still 1e-8 (not overwritten by criteria)
    assert_relative_eq!(cfg.tolerance, 1e-8);
    assert!(cfg.convergence_criteria.is_some());
}
