use bone_solver::{ConstraintSystem, Point3Handle, 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 point3(x: u32, y: u32, z: u32) -> Point3Handle {
    Point3Handle {
        x: p(x),
        y: p(y),
        z: p(z),
    }
}

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

fn distance(a: Point3Handle, b: Point3Handle) -> Residual {
    Residual::Distance3 {
        a,
        b,
        value_mm: 1.0,
    }
}

#[test]
fn disjoint_residuals_yield_separate_components() {
    let system = ConstraintSystem::new(
        parameters(&[0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 10.0, 0.0, 0.0, 11.0, 0.0, 0.0]),
        vec![
            distance(point3(0, 1, 2), point3(3, 4, 5)),
            distance(point3(6, 7, 8), point3(9, 10, 11)),
        ],
    );
    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, 4, 5]
    );
    assert_eq!(
        second
            .parameters()
            .iter()
            .map(|p| p.value())
            .collect::<Vec<_>>(),
        vec![6, 7, 8, 9, 10, 11]
    );
    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, 0.0, 1.0, 0.0, 0.0, 2.0, 0.0, 0.0]),
        vec![
            distance(point3(0, 1, 2), point3(3, 4, 5)),
            distance(point3(3, 4, 5), point3(6, 7, 8)),
        ],
    );
    let decomp = decompose(&system);
    assert_eq!(decomp.components().len(), 1);
    let only = &decomp.components()[0];
    assert_eq!(only.parameters().len(), 9);
    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());
}
