use bone_solver::{ConstraintSystem, DofConfig, Line3Handle, Point3Handle, Residual, analyze_dof};
use bone_types::{Parameter, ParameterIndex, ResidualIndex};

fn p(i: u32) -> ParameterIndex {
    ParameterIndex::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 on_plane(point: Point3Handle) -> Residual {
    Residual::OnPlane {
        point,
        normal: [0.0, 1.0, 0.0],
        offset_mm: 0.0,
    }
}

#[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_on_plane_reports_free_parameters() {
    let system = ConstraintSystem::new(
        parameters(&[0.0, 0.0, 0.0]),
        vec![on_plane(point3(0, 1, 2))],
    );
    let report = analyze_dof(&system, DofConfig::DEFAULT);
    assert_eq!(report.dof().value(), 2);
    assert_eq!(report.under_constrained().len(), 2);
    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));
    assert_eq!(
        report.redundant_parents().len(),
        1,
        "exactly one of the duplicate pins is the redundant parent",
    );
}

#[test]
fn a_lone_parallel_is_not_a_redundant_parent() {
    let system = ConstraintSystem::new(
        parameters(&[0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 2.0, 0.0, 3.0, 6.0, 0.0]),
        vec![Residual::Parallel3(
            Line3Handle {
                a: point3(0, 1, 2),
                b: point3(3, 4, 5),
            },
            Line3Handle {
                a: point3(6, 7, 8),
                b: point3(9, 10, 11),
            },
        )],
    );
    let report = analyze_dof(&system, DofConfig::DEFAULT);
    assert!(
        report.redundant_parents().is_empty(),
        "the structural rank-deficient row of a single parallel is not a redundant relation",
    );
}

#[test]
fn a_duplicated_parallel_flags_exactly_one_redundant_parent() {
    let line_a = Line3Handle {
        a: point3(0, 1, 2),
        b: point3(3, 4, 5),
    };
    let line_b = Line3Handle {
        a: point3(6, 7, 8),
        b: point3(9, 10, 11),
    };
    let system = ConstraintSystem::new(
        parameters(&[0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 2.0, 0.0, 1.0, 2.0, 0.0]),
        vec![
            Residual::Parallel3(line_a, line_b),
            Residual::Parallel3(line_a, line_b),
        ],
    );
    let report = analyze_dof(&system, DofConfig::DEFAULT);
    assert_eq!(
        report.redundant_parents().len(),
        1,
        "the second copy of a cross-product relation is the redundant parent, not both and not neither",
    );
    assert!(
        report.over_constrained().is_empty(),
        "a consistent duplicate is redundant, not a conflict",
    );
}

#[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, 0.0]),
        vec![on_plane(point3(0, 1, 2))],
    );
    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));
}
