//! Integration tests for SystemBuilder constraints API (Story 13-3).
//!
//! Verifies that constraints and bounded variables can be added via the builder,
//! linked for inverse control, and that the built system passes DoF validation.

use entropyk::{
    BoundedVariable, BoundedVariableId, ComponentOutput, Constraint, ConstraintId, SystemBuilder,
    ThermoError, TopologyError,
};
use entropyk_components::{
    Component, ComponentError, ConnectedPort, JacobianBuilder, ResidualVector,
};

struct MockComponent {
    n_eqs: usize,
}

impl Component for MockComponent {
    fn compute_residuals(
        &self,
        _state: &[f64],
        _residuals: &mut ResidualVector,
    ) -> Result<(), ComponentError> {
        Ok(())
    }

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

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

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

#[test]
fn test_builder_constraints_link_and_validate_dof() {
    let constraint = Constraint::new(
        ConstraintId::new("superheat"),
        ComponentOutput::Superheat {
            component_id: "evap".to_string(),
        },
        5.0,
    );
    let valve =
        BoundedVariable::new(BoundedVariableId::new("valve"), 0.5, 0.0, 1.0).expect("valid bounds");

    // Minimal topology: 2 nodes, 1 edge → 3 edge unknowns (ṁ,P,h). With 1 constraint and 1 control
    // we need 4 equations total: 3 component eqs + 1 constraint = 4 = 3 + 1 unknowns.
    let system = SystemBuilder::new()
        .component("evap", Box::new(MockComponent { n_eqs: 2 }))
        .unwrap()
        .component("other", Box::new(MockComponent { n_eqs: 1 }))
        .unwrap()
        .edge("evap", "other")
        .unwrap()
        .with_constraint(constraint)
        .unwrap()
        .with_bounded_variable(valve)
        .unwrap()
        .link_constraint_to_control(
            &ConstraintId::new("superheat"),
            &BoundedVariableId::new("valve"),
        )
        .unwrap()
        .build()
        .expect("build should succeed");

    // DoF validation should pass: 1 constraint, 1 control variable (balanced).
    let dof_result = system.validate_inverse_control_dof();
    assert!(
        dof_result.is_ok(),
        "validate_inverse_control_dof should pass when constraint and control are linked: {:?}",
        dof_result.err()
    );
}

#[test]
fn test_builder_dof_imbalance_two_constraints_one_control() {
    let c1 = Constraint::new(
        ConstraintId::new("superheat"),
        ComponentOutput::Superheat {
            component_id: "evap".to_string(),
        },
        5.0,
    );
    let c2 = Constraint::new(
        ConstraintId::new("subcooling"),
        ComponentOutput::Superheat {
            component_id: "evap".to_string(),
        },
        3.0,
    );
    let valve =
        BoundedVariable::new(BoundedVariableId::new("valve"), 0.5, 0.0, 1.0).expect("valid bounds");

    // Same topology as test 1 (3 component eqs total), but 2 constraints and 1 control →
    // 3+2=5 equations vs 3+1=4 unknowns → over-constrained. Since `System::finalize()`
    // enforces the DoF gate, `build()` itself now rejects the system with
    // `TopologyError::DofImbalance` (the post-build `validate_inverse_control_dof()`
    // check is unreachable for over-constrained systems).
    let result = SystemBuilder::new()
        .component("evap", Box::new(MockComponent { n_eqs: 2 }))
        .unwrap()
        .component("other", Box::new(MockComponent { n_eqs: 1 }))
        .unwrap()
        .edge("evap", "other")
        .unwrap()
        .with_constraint(c1)
        .unwrap()
        .with_constraint(c2)
        .unwrap()
        .with_bounded_variable(valve)
        .unwrap()
        .link_constraint_to_control(
            &ConstraintId::new("superheat"),
            &BoundedVariableId::new("valve"),
        )
        .unwrap()
        .build();

    // DoF gate should reject the build: 2 constraints but only 1 control (unbalanced).
    match result {
        Err(ThermoError::Topology(TopologyError::DofImbalance { message })) => {
            assert!(
                message.contains("over-constrained"),
                "expected an over-constrained DoF report, got: {message}"
            );
        }
        Err(other) => panic!("expected DofImbalance error from build(), got: {other}"),
        Ok(_) => panic!("build() should fail with DofImbalance (2 constraints, 1 control)"),
    }
}
