use bone_solver::{ConstraintSystem, DofConfig, LineHandle, PointHandle, Residual, analyze_dof};
use bone_types::{Parameter, ParameterIndex, ResidualIndex};

fn p(i: u32) -> ParameterIndex {
    ParameterIndex::new(i)
}

fn point(x: u32, y: u32) -> PointHandle {
    PointHandle { x: p(x), y: p(y) }
}

fn parameters(values: &[f64]) -> Vec<Parameter> {
    values.iter().copied().map(Parameter::new).collect()
}

#[test]
fn fully_constrained_pin_has_zero_dof_and_no_flags() {
    let system = ConstraintSystem::new(
        parameters(&[0.0]),
        vec![Residual::Pin {
            param: p(0),
            target: 5.0,
        }],
    );
    let report = analyze_dof(&system, DofConfig::DEFAULT);
    assert_eq!(report.dof().value(), 0);
    assert!(report.under_constrained().is_empty());
    assert!(report.over_constrained().is_empty());
    assert!(report.redundant_consistent().is_empty());
}

#[test]
fn under_constrained_horizontal_reports_free_parameters() {
    let system = ConstraintSystem::new(
        parameters(&[0.0, 0.0, 1.0, 0.0]),
        vec![Residual::Horizontal(LineHandle {
            a: point(0, 1),
            b: point(2, 3),
        })],
    );
    let report = analyze_dof(&system, DofConfig::DEFAULT);
    assert_eq!(report.dof().value(), 3);
    assert_eq!(report.under_constrained().len(), 3);
    assert!(report.over_constrained().is_empty());
    assert!(report.redundant_consistent().is_empty());
}

#[test]
fn redundant_pin_is_consistent_not_conflict() {
    let system = ConstraintSystem::new(
        parameters(&[5.0]),
        vec![
            Residual::Pin {
                param: p(0),
                target: 5.0,
            },
            Residual::Pin {
                param: p(0),
                target: 5.0,
            },
        ],
    );
    let report = analyze_dof(&system, DofConfig::DEFAULT);
    assert_eq!(report.dof().value(), 0);
    assert!(report.over_constrained().is_empty());
    assert_eq!(report.redundant_consistent().len(), 1);
    let flagged = report.redundant_consistent()[0];
    assert!(flagged == ResidualIndex::new(0) || flagged == ResidualIndex::new(1));
}

#[test]
fn dof_fixture_matrix_snapshot() {
    let fully_constrained = ConstraintSystem::new(
        parameters(&[0.0]),
        vec![Residual::Pin {
            param: p(0),
            target: 5.0,
        }],
    );
    let under_constrained = ConstraintSystem::new(
        parameters(&[0.0, 0.0, 1.0, 0.0]),
        vec![Residual::Horizontal(LineHandle {
            a: point(0, 1),
            b: point(2, 3),
        })],
    );
    let redundant_consistent = ConstraintSystem::new(
        parameters(&[5.0]),
        vec![
            Residual::Pin {
                param: p(0),
                target: 5.0,
            },
            Residual::Pin {
                param: p(0),
                target: 5.0,
            },
        ],
    );
    let conflicting = ConstraintSystem::new(
        parameters(&[3.0]),
        vec![
            Residual::Pin {
                param: p(0),
                target: 3.0,
            },
            Residual::Pin {
                param: p(0),
                target: 7.0,
            },
        ],
    );
    let bundle = [
        ("fully_constrained", fully_constrained),
        ("under_constrained", under_constrained),
        ("redundant_consistent", redundant_consistent),
        ("conflicting", conflicting),
    ];
    let rendered: String = bundle
        .iter()
        .map(|(label, system)| {
            format!(
                "{label}: {}",
                analyze_dof(system, DofConfig::DEFAULT).display()
            )
        })
        .collect::<Vec<_>>()
        .join("\n");
    insta::assert_snapshot!("dof_matrix", rendered);
}

#[test]
fn conflicting_pins_yield_over_constrained_row() {
    let system = ConstraintSystem::new(
        parameters(&[3.0]),
        vec![
            Residual::Pin {
                param: p(0),
                target: 3.0,
            },
            Residual::Pin {
                param: p(0),
                target: 7.0,
            },
        ],
    );
    let report = analyze_dof(&system, DofConfig::DEFAULT);
    assert_eq!(report.dof().value(), 0);
    assert_eq!(report.over_constrained().len(), 1);
    assert!(report.redundant_consistent().is_empty());
    let flagged = report.over_constrained()[0];
    assert!(flagged == ResidualIndex::new(0) || flagged == ResidualIndex::new(1));
}
