use bone_kernel::{Arc2, Circle2, Curve2, Line2};
use bone_types::{Angle, Length, Parameter, Point2, Tolerance};
use uom::si::angle::radian;
use uom::si::length::millimeter;

const TOL: Tolerance = Tolerance::new(1e-9);

fn samples() -> [Parameter; 5] {
    [
        Parameter::new(0.0),
        Parameter::new(0.25),
        Parameter::new(0.5),
        Parameter::new(0.75),
        Parameter::new(1.0),
    ]
}

fn fmt_point(p: Point2) -> String {
    let (x, y) = p.coords_mm();
    format!("({x:.6}, {y:.6})")
}

fn fmt_curve<C: Curve2 + core::fmt::Display + core::fmt::Debug>(name: &str, c: &C) -> String {
    let samples = samples();
    let points = samples
        .iter()
        .map(|t| {
            format!(
                "t={:.2} -> p={} v={}",
                t.value(),
                fmt_point(c.evaluate(*t)),
                c.derivative(*t)
            )
        })
        .collect::<Vec<_>>()
        .join("\n    ");
    let k_start = c.curvature(Parameter::new(0.0));
    let k_mid = c.curvature(Parameter::new(0.5));
    let tangent_mid = c.tangent(Parameter::new(0.5));
    let bbox = c.bounding_box();
    let closest = c.closest_point(Point2::from_mm(0.0, 0.0), TOL);
    format!(
        "{name}_display = {c}\n\
         {name}_debug   = {c:?}\n\
         {name}_samples:\n    {points}\n\
         {name}_tangent_mid = {tangent_mid}\n\
         {name}_curvature_start = {k_start}\n\
         {name}_curvature_mid   = {k_mid}\n\
         {name}_bbox    = {bbox}\n\
         {name}_closest_origin = {closest}",
    )
}

#[test]
fn line2_surface() {
    let Ok(line) = Line2::new(Point2::from_mm(1.0, 2.0), Point2::from_mm(7.0, 6.0), TOL) else {
        panic!("endpoints distinct");
    };
    insta::assert_snapshot!(fmt_curve("line2", &line));
}

#[test]
fn circle2_surface() {
    let Ok(circle) = Circle2::new(
        Point2::from_mm(2.0, -1.0),
        Length::new::<millimeter>(3.0),
        TOL,
    ) else {
        panic!("radius positive");
    };
    insta::assert_snapshot!(fmt_curve("circle2", &circle));
}

#[test]
fn arc2_quarter_ccw_surface() {
    let Ok(arc) = Arc2::new(
        Point2::from_mm(0.0, 0.0),
        Length::new::<millimeter>(5.0),
        Angle::new::<radian>(0.0),
        Angle::new::<radian>(core::f64::consts::FRAC_PI_2),
        TOL,
    ) else {
        panic!("sweep nonzero");
    };
    insta::assert_snapshot!(fmt_curve("arc2_q", &arc));
}

#[test]
fn arc2_half_cw_surface() {
    let Ok(arc) = Arc2::new(
        Point2::from_mm(1.0, 1.0),
        Length::new::<millimeter>(2.0),
        Angle::new::<radian>(core::f64::consts::PI),
        Angle::new::<radian>(-core::f64::consts::PI),
        TOL,
    ) else {
        panic!("sweep nonzero");
    };
    insta::assert_snapshot!(fmt_curve("arc2_h", &arc));
}

#[test]
fn line2_rejects_degenerate_endpoints() {
    let err = Line2::new(
        Point2::from_mm(1.0, 1.0),
        Point2::from_mm(1.0, 1.0 + 1e-12),
        TOL,
    );
    assert!(err.is_err());
}

#[test]
fn circle2_rejects_zero_radius() {
    let err = Circle2::new(
        Point2::from_mm(0.0, 0.0),
        Length::new::<millimeter>(0.0),
        TOL,
    );
    assert!(err.is_err());
}

#[test]
fn arc2_rejects_zero_sweep() {
    let err = Arc2::new(
        Point2::from_mm(0.0, 0.0),
        Length::new::<millimeter>(1.0),
        Angle::new::<radian>(0.0),
        Angle::new::<radian>(0.0),
        TOL,
    );
    assert!(err.is_err());
}

#[test]
fn arc2_closest_point_wide_sweep_returns_midarc_t() {
    let Ok(arc) = Arc2::new(
        Point2::from_mm(0.0, 0.0),
        Length::new::<millimeter>(1.0),
        Angle::new::<radian>(0.0),
        Angle::new::<radian>(3.0 * core::f64::consts::FRAC_PI_2),
        TOL,
    ) else {
        panic!("3π/2 sweep within bounds");
    };
    let px = -0.7_f64;
    let py = -0.8_f64;
    let closest = arc.closest_point(Point2::from_mm(px, py), TOL);
    let theta = py.atan2(px).rem_euclid(core::f64::consts::TAU);
    let expected_t = theta / (3.0 * core::f64::consts::FRAC_PI_2);
    let got_t = closest.parameter().value();
    assert!(
        (got_t - expected_t).abs() < 1e-12,
        "wide-sweep closest-point: expected t≈{expected_t}, got {got_t}"
    );
}
