use bone_solver::{ConstraintSystem, LineHandle, PointHandle, Residual, decompose};
use bone_types::{Parameter, ParameterIndex, ParentIndex, ResidualIndex};

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

fn parent(i: u32) -> ParentIndex {
    ParentIndex::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 disjoint_residuals_yield_separate_components() {
    let system = ConstraintSystem::new(
        parameters(&[0.0, 0.0, 1.0, 0.0, 10.0, 0.0, 11.0, 0.0]),
        vec![
            Residual::Horizontal(LineHandle {
                a: point(0, 1),
                b: point(2, 3),
            }),
            Residual::Horizontal(LineHandle {
                a: point(4, 5),
                b: point(6, 7),
            }),
        ],
    );
    let decomp = decompose(&system);
    assert_eq!(decomp.components().len(), 2);
    let first = &decomp.components()[0];
    let second = &decomp.components()[1];
    assert_eq!(
        first
            .parameters()
            .iter()
            .map(|p| p.value())
            .collect::<Vec<_>>(),
        vec![0, 1, 2, 3]
    );
    assert_eq!(
        second
            .parameters()
            .iter()
            .map(|p| p.value())
            .collect::<Vec<_>>(),
        vec![4, 5, 6, 7]
    );
    assert_eq!(first.residual_parents(), &[parent(0)]);
    assert_eq!(second.residual_parents(), &[parent(1)]);
    assert_eq!(first.residual_rows(&system), vec![ResidualIndex::new(0)]);
    assert_eq!(second.residual_rows(&system), vec![ResidualIndex::new(1)]);
}

#[test]
fn shared_parameter_merges_components() {
    let system = ConstraintSystem::new(
        parameters(&[0.0, 0.0, 1.0, 0.0, 2.0, 0.0]),
        vec![
            Residual::Horizontal(LineHandle {
                a: point(0, 1),
                b: point(2, 3),
            }),
            Residual::Horizontal(LineHandle {
                a: point(2, 3),
                b: point(4, 5),
            }),
        ],
    );
    let decomp = decompose(&system);
    assert_eq!(decomp.components().len(), 1);
    let only = &decomp.components()[0];
    assert_eq!(only.parameters().len(), 6);
    assert_eq!(only.residual_parents(), &[parent(0), parent(1)]);
}

#[test]
fn isolated_parameter_forms_its_own_component() {
    let system = ConstraintSystem::new(
        parameters(&[0.0, 0.0, 3.0]),
        vec![Residual::Pin {
            param: p(0),
            target: 1.0,
        }],
    );
    let decomp = decompose(&system);
    assert_eq!(decomp.components().len(), 3);
    decomp
        .components()
        .iter()
        .enumerate()
        .for_each(|(i, comp)| {
            let Ok(iv) = u32::try_from(i) else {
                unreachable!()
            };
            assert_eq!(comp.parameters(), &[ParameterIndex::new(iv)]);
        });
    assert_eq!(decomp.components()[0].residual_parents(), &[parent(0)]);
    assert!(decomp.components()[1].residual_parents().is_empty());
    assert!(decomp.components()[2].residual_parents().is_empty());
}
