//! Integration tests for Story 4.5: Time-Budgeted Solving
//!
//! Tests the timeout behavior with best-state return:
//! - Timeout returns best state instead of error
//! - Best state is the lowest residual encountered
//! - ZOH (Zero-Order Hold) fallback for HIL scenarios
//! - Configurable timeout behavior
//! - Timeout across fallback switches preserves best state

use entropyk_components::{Component, ComponentError, JacobianBuilder, ResidualVector, StateSlice};
use entropyk_solver::solver::{
    ConvergenceStatus, FallbackConfig, FallbackSolver, NewtonConfig, PicardConfig, Solver,
    SolverError, TimeoutConfig,
};
use entropyk_solver::system::{System, DEFAULT_MASS_FLOW_SEED_KG_S};
use std::time::Duration;

// ─────────────────────────────────────────────────────────────────────────────
// Mock Components for Testing
// ─────────────────────────────────────────────────────────────────────────────

/// A 2x2 linear system: r = A * x - b
struct LinearSystem2x2 {
    a: [[f64; 2]; 2],
    b: [f64; 2],
}

impl LinearSystem2x2 {
    fn well_conditioned() -> Self {
        // CM1.3: the solver clamps pressure (state[1]) to >= 10_000 Pa.  Choose
        // the linear system so that the analytical solution lands at the bound
        // (P = h = 10_000), keeping Newton convergence exact in one iteration.
        Self {
            a: [[2.0, 1.0], [1.0, 2.0]],
            b: [30_000.0, 30_000.0],
        }
    }
}

impl Component for LinearSystem2x2 {
    fn compute_residuals(
        &self,
        state: &StateSlice,
        residuals: &mut ResidualVector,
    ) -> Result<(), ComponentError> {
        // CM1.3: per-edge state is (ṁ, P, h); the 2×2 system acts on (P, h) at
        // global indices 1 and 2. The third equation pins ṁ (state[0]) to the
        // default seed so the system is square (3 equations, 3 unknowns).
        residuals[0] = self.a[0][0] * state[1] + self.a[0][1] * state[2] - self.b[0];
        residuals[1] = self.a[1][0] * state[1] + self.a[1][1] * state[2] - self.b[1];
        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, self.a[0][0]);
        jacobian.add_entry(0, 2, self.a[0][1]);
        jacobian.add_entry(1, 1, self.a[1][0]);
        jacobian.add_entry(1, 2, self.a[1][1]);
        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
}

// ─────────────────────────────────────────────────────────────────────────────
// TimeoutConfig Tests (AC: #6)
// ─────────────────────────────────────────────────────────────────────────────

#[test]
fn test_timeout_config_defaults() {
    let config = TimeoutConfig::default();
    assert!(config.return_best_state_on_timeout);
    assert!(!config.zoh_fallback);
}

#[test]
fn test_timeout_config_zoh_enabled() {
    let config = TimeoutConfig {
        return_best_state_on_timeout: true,
        zoh_fallback: true,
    };
    assert!(config.zoh_fallback);
}

#[test]
fn test_timeout_config_return_error_on_timeout() {
    let config = TimeoutConfig {
        return_best_state_on_timeout: false,
        zoh_fallback: false,
    };
    assert!(!config.return_best_state_on_timeout);
}

// ─────────────────────────────────────────────────────────────────────────────
// AC: #1, #2 - Timeout Returns Best State
// ─────────────────────────────────────────────────────────────────────────────

#[test]
fn test_timeout_returns_best_state_not_error() {
    let mut system = create_test_system(Box::new(LinearSystem2x2::well_conditioned()));
    let timeout = Duration::from_nanos(1);
    let mut solver = NewtonConfig {
        timeout: Some(timeout),
        max_iterations: 10000,
        timeout_config: TimeoutConfig {
            return_best_state_on_timeout: true,
            zoh_fallback: false,
        },
        ..Default::default()
    };

    let result = solver.solve(&mut system);
    match result {
        Ok(state) => {
            assert!(
                state.status == ConvergenceStatus::Converged
                    || state.status == ConvergenceStatus::TimedOutWithBestState
            );
        }
        Err(SolverError::Timeout { .. }) => {}
        Err(other) => panic!("Unexpected error: {:?}", other),
    }
}

#[test]
fn test_best_state_is_lowest_residual() {
    let mut system = create_test_system(Box::new(LinearSystem2x2::well_conditioned()));
    let timeout = Duration::from_micros(100);
    let mut solver = NewtonConfig {
        timeout: Some(timeout),
        max_iterations: 10000,
        timeout_config: TimeoutConfig::default(),
        ..Default::default()
    };

    let result = solver.solve(&mut system);
    if let Ok(state) = result {
        assert!(state.final_residual.is_finite());
        assert!(state.final_residual >= 0.0);
    }
}

// ─────────────────────────────────────────────────────────────────────────────
// AC: #3 - ZOH Fallback
// ─────────────────────────────────────────────────────────────────────────────

#[test]
fn test_zoh_fallback_returns_previous_state() {
    let mut system = create_test_system(Box::new(LinearSystem2x2::well_conditioned()));
    let previous_state = vec![DEFAULT_MASS_FLOW_SEED_KG_S, 1.0, 2.0];
    let timeout = Duration::from_nanos(1);

    let mut solver = NewtonConfig {
        timeout: Some(timeout),
        max_iterations: 10000,
        timeout_config: TimeoutConfig {
            return_best_state_on_timeout: true,
            zoh_fallback: true,
        },
        previous_state: Some(previous_state.clone()),
        ..Default::default()
    };

    let result = solver.solve(&mut system);
    if let Ok(state) = result {
        if state.status == ConvergenceStatus::TimedOutWithBestState {
            assert_eq!(state.state, previous_state);
        }
    }
}

#[test]
fn test_zoh_fallback_ignored_without_previous_state() {
    let mut system = create_test_system(Box::new(LinearSystem2x2::well_conditioned()));
    let timeout = Duration::from_nanos(1);

    let mut solver = NewtonConfig {
        timeout: Some(timeout),
        max_iterations: 10000,
        timeout_config: TimeoutConfig {
            return_best_state_on_timeout: true,
            zoh_fallback: true,
        },
        previous_state: None,
        ..Default::default()
    };

    let result = solver.solve(&mut system);
    if let Ok(state) = result {
        if state.status == ConvergenceStatus::TimedOutWithBestState {
            assert_eq!(state.state.len(), 3);
        }
    }
}

#[test]
fn test_zoh_fallback_picard() {
    let mut system = create_test_system(Box::new(LinearSystem2x2::well_conditioned()));
    let previous_state = vec![DEFAULT_MASS_FLOW_SEED_KG_S, 5.0, 10.0];
    let timeout = Duration::from_nanos(1);

    let mut solver = PicardConfig {
        timeout: Some(timeout),
        max_iterations: 10000,
        timeout_config: TimeoutConfig {
            return_best_state_on_timeout: true,
            zoh_fallback: true,
        },
        previous_state: Some(previous_state.clone()),
        ..Default::default()
    };

    let result = solver.solve(&mut system);
    if let Ok(state) = result {
        if state.status == ConvergenceStatus::TimedOutWithBestState {
            assert_eq!(state.state, previous_state);
        }
    }
}

#[test]
fn test_zoh_fallback_uses_previous_residual() {
    let mut system = create_test_system(Box::new(LinearSystem2x2::well_conditioned()));
    let previous_state = vec![DEFAULT_MASS_FLOW_SEED_KG_S, 1.0, 2.0];
    let previous_residual = 1e-4;
    let timeout = Duration::from_nanos(1);

    let mut solver = NewtonConfig {
        timeout: Some(timeout),
        max_iterations: 10000,
        timeout_config: TimeoutConfig {
            return_best_state_on_timeout: true,
            zoh_fallback: true,
        },
        previous_state: Some(previous_state.clone()),
        previous_residual: Some(previous_residual),
        ..Default::default()
    };

    let result = solver.solve(&mut system);
    if let Ok(state) = result {
        if state.status == ConvergenceStatus::TimedOutWithBestState {
            assert_eq!(state.state, previous_state);
            assert!((state.final_residual - previous_residual).abs() < 1e-10);
        }
    }
}

// ─────────────────────────────────────────────────────────────────────────────
// AC: #6 - return_best_state_on_timeout = false
// ─────────────────────────────────────────────────────────────────────────────

#[test]
fn test_timeout_returns_error_when_configured() {
    let mut system = create_test_system(Box::new(LinearSystem2x2::well_conditioned()));
    let timeout = Duration::from_millis(1);

    let mut solver = NewtonConfig {
        timeout: Some(timeout),
        max_iterations: 10000,
        timeout_config: TimeoutConfig {
            return_best_state_on_timeout: false,
            zoh_fallback: false,
        },
        ..Default::default()
    };

    let result = solver.solve(&mut system);
    match result {
        Err(SolverError::Timeout { .. }) | Ok(_) => {}
        Err(other) => panic!("Expected Timeout or Ok, got {:?}", other),
    }
}

#[test]
fn test_picard_timeout_returns_error_when_configured() {
    let mut system = create_test_system(Box::new(LinearSystem2x2::well_conditioned()));
    let timeout = Duration::from_millis(1);

    let mut solver = PicardConfig {
        timeout: Some(timeout),
        max_iterations: 10000,
        timeout_config: TimeoutConfig {
            return_best_state_on_timeout: false,
            zoh_fallback: false,
        },
        // CM1.3: keep the Picard seed at the analytical solution so the test
        // focuses on timeout/pre-allocation wiring, not Picard convergence.
        // Pressure must respect the 10_000 Pa lower bound, so the solution is
        // (ṁ=seed, P=10_000, h=10_000).
        initial_state: Some(vec![DEFAULT_MASS_FLOW_SEED_KG_S, 10_000.0, 10_000.0]),
        ..Default::default()
    };

    let result = solver.solve(&mut system);
    match result {
        Err(SolverError::Timeout { .. }) | Ok(_) => {}
        Err(other) => panic!("Expected Timeout or Ok, got {:?}", other),
    }
}

// ─────────────────────────────────────────────────────────────────────────────
// AC: #4 - Timeout Across Fallback Switches
// ─────────────────────────────────────────────────────────────────────────────

#[test]
fn test_timeout_across_fallback_switches_preserves_best_state() {
    let mut system = create_test_system(Box::new(LinearSystem2x2::well_conditioned()));
    let timeout = Duration::from_millis(10);

    let mut solver = FallbackSolver::new(FallbackConfig {
        fallback_enabled: true,
        max_fallback_switches: 2,
        ..Default::default()
    })
    .with_timeout(timeout)
    .with_newton_config(NewtonConfig {
        max_iterations: 500,
        timeout_config: TimeoutConfig {
            return_best_state_on_timeout: true,
            zoh_fallback: false,
        },
        ..Default::default()
    })
    .with_picard_config(PicardConfig {
        max_iterations: 500,
        timeout_config: TimeoutConfig {
            return_best_state_on_timeout: true,
            zoh_fallback: false,
        },
        ..Default::default()
    });

    let result = solver.solve(&mut system);
    match result {
        Ok(state) => {
            assert!(
                state.status == ConvergenceStatus::Converged
                    || state.status == ConvergenceStatus::TimedOutWithBestState
            );
            assert!(state.final_residual.is_finite());
        }
        Err(SolverError::Timeout { .. }) => {}
        Err(other) => panic!("Unexpected error: {:?}", other),
    }
}

#[test]
fn test_fallback_solver_total_timeout() {
    let mut system = create_test_system(Box::new(LinearSystem2x2::well_conditioned()));
    let timeout = Duration::from_millis(5);

    let mut solver = FallbackSolver::default_solver()
        .with_timeout(timeout)
        .with_newton_config(NewtonConfig {
            max_iterations: 10000,
            ..Default::default()
        })
        .with_picard_config(PicardConfig {
            max_iterations: 10000,
            ..Default::default()
        });

    let start = std::time::Instant::now();
    let result = solver.solve(&mut system);
    let elapsed = start.elapsed();

    if result.is_err()
        || matches!(result, Ok(ref s) if s.status == ConvergenceStatus::TimedOutWithBestState)
    {
        assert!(
            elapsed < timeout + Duration::from_millis(100),
            "Total solve time should respect timeout budget. Elapsed: {:?}, Timeout: {:?}",
            elapsed,
            timeout
        );
    }
}

// ─────────────────────────────────────────────────────────────────────────────
// Pre-allocation Tests (AC: #5)
// ─────────────────────────────────────────────────────────────────────────────

#[test]
fn test_newton_config_best_state_preallocated() {
    let mut system = create_test_system(Box::new(LinearSystem2x2::well_conditioned()));
    let mut solver = NewtonConfig {
        timeout: Some(Duration::from_millis(100)),
        max_iterations: 10,
        ..Default::default()
    };

    let result = solver.solve(&mut system);
    assert!(result.is_ok() || matches!(result, Err(SolverError::Timeout { .. })));
}

#[test]
fn test_picard_config_best_state_preallocated() {
    let mut system = create_test_system(Box::new(LinearSystem2x2::well_conditioned()));
    let mut solver = PicardConfig {
        timeout: Some(Duration::from_millis(100)),
        max_iterations: 10,
        // CM1.3: seed Picard at the analytical solution (ṁ=seed, P=10_000, h=10_000)
        // so the test targets pre-allocation wiring and not Picard convergence.
        initial_state: Some(vec![DEFAULT_MASS_FLOW_SEED_KG_S, 10_000.0, 10_000.0]),
        ..Default::default()
    };

    let result = solver.solve(&mut system);
    match result {
        Ok(_) | Err(SolverError::Timeout { .. }) | Err(SolverError::NonConvergence { .. }) => {}
        Err(other) => panic!("Unexpected error: {:?}", other),
    }
}
