//! System-wide DoF balance tests.
//!
//! Verifies that the ledger counts equations and unknowns consistently and that
//! `finalize` hard-fails on square-system violations.

use entropyk_components::{
    Component, ComponentError, ConnectedPort, JacobianBuilder, ResidualVector, StateSlice,
};
use entropyk_solver::dof::SystemDofBalance;
use entropyk_solver::system::System;
use entropyk_solver::TopologyError;

/// Minimal mock: N residuals that are identically zero (topology bookkeeping only).
struct MockEq {
    n: usize,
}

impl Component for MockEq {
    fn compute_residuals(
        &self,
        _state: &StateSlice,
        residuals: &mut ResidualVector,
    ) -> Result<(), ComponentError> {
        for r in residuals.iter_mut().take(self.n) {
            *r = 0.0;
        }
        Ok(())
    }

    fn jacobian_entries(
        &self,
        _state: &StateSlice,
        _jacobian: &mut JacobianBuilder,
    ) -> Result<(), ComponentError> {
        Ok(())
    }

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

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

    fn flow_paths(&self) -> Vec<(usize, usize)> {
        // Single-stream pass-through so the edge pair shares one ṁ branch.
        vec![(0, 1)]
    }
}

/// Two-node cycle with matching residual count → square after CM1.4.
///
/// Unknowns: 1 ṁ + 2 edges × (P,h) = 5
/// Equations: 3 + 2 = 5
#[test]
fn two_node_cycle_is_balanced() {
    let mut sys = System::new();
    let a = sys.add_component(Box::new(MockEq { n: 3 }));
    let b = sys.add_component(Box::new(MockEq { n: 2 }));
    sys.add_edge(a, b).unwrap();
    sys.add_edge(b, a).unwrap();
    sys.finalize().expect("balanced system must finalize");

    let report = sys.dof_report();
    assert_eq!(report.n_unknowns, 5);
    assert_eq!(report.n_equations, 5);
    assert_eq!(report.balance, SystemDofBalance::Balanced);
    assert!(sys.validate_system_dof().is_ok());
}

/// One extra residual without a free unknown → over-constrained, finalize fails.
#[test]
fn overconstrained_finalize_fails() {
    let mut sys = System::new();
    let a = sys.add_component(Box::new(MockEq { n: 4 })); // +1 excess
    let b = sys.add_component(Box::new(MockEq { n: 2 }));
    sys.add_edge(a, b).unwrap();
    sys.add_edge(b, a).unwrap();
    // unknowns = 5, equations = 6
    let err = sys
        .finalize()
        .expect_err("must reject over-constrained system");
    match err {
        TopologyError::DofImbalance { message } => {
            assert!(
                message.contains("over-constrained") || message.contains("equations"),
                "unexpected message: {message}"
            );
        }
        other => panic!("expected DofImbalance, got {other:?}"),
    }
}

/// Missing residual → under-constrained: finalize warns but allows topology tests;
/// hard `validate_system_dof` still rejects (production path).
#[test]
fn underconstrained_detected_by_validate_system_dof() {
    let mut sys = System::new();
    let a = sys.add_component(Box::new(MockEq { n: 2 }));
    let b = sys.add_component(Box::new(MockEq { n: 2 }));
    sys.add_edge(a, b).unwrap();
    sys.add_edge(b, a).unwrap();
    // unknowns = 5, equations = 4
    sys.finalize()
        .expect("under-constrained allowed at finalize for topology mocks");
    let report = sys.dof_report();
    assert!(matches!(
        report.balance,
        SystemDofBalance::UnderConstrained { free_dofs: 1 }
    ));
    assert!(sys.validate_system_dof().is_err());
}
