use bone_document::{SketchEntity, SketchEntityKind, SketchRelation};
use bone_types::{Point2, SketchEntityId};
use uom::si::angle::radian;

use crate::sketch_mode::{ClickAnchor, Pending, SketchTool};
use crate::snap::{SnapHit, SnapKind};
use crate::tools::{add_line, add_point, place, preview};

use super::{arc_sweep, count_kind, distance, fresh, only_arc};

#[test]
fn centerpoint_arc_picks_minor_arc_when_cursor_clockwise_of_start() {
    let center = Point2::from_mm(0.0, 0.0);
    let start = Point2::from_mm(5.0, 0.0);
    let cursor = Point2::from_mm(5.0, -5.0);
    let (_, pending) = place(fresh(), SketchTool::CenterpointArc, center, None, None);
    let (_, pending) = place(fresh(), SketchTool::CenterpointArc, start, pending, None);
    let (next, _) = place(fresh(), SketchTool::CenterpointArc, cursor, pending, None);
    let Some(next) = next else {
        panic!("centerpoint arc commits")
    };
    let sweep = arc_sweep(&next, only_arc(&next));
    assert!(
        sweep <= std::f64::consts::PI + 1e-9,
        "sweep {sweep} should be the minor arc when cursor is clockwise of start",
    );
}

#[test]
fn centerpoint_arc_preview_picks_minor_arc_when_cursor_clockwise_of_start() {
    let (sketch, center) = add_point(fresh(), Point2::from_mm(0.0, 0.0));
    let (sketch, start) = add_point(sketch, Point2::from_mm(5.0, 0.0));
    let pending = Some(Pending::Second(
        ClickAnchor::Endpoint(center),
        ClickAnchor::Endpoint(start),
    ));
    let cursor = Point2::from_mm(5.0, -5.0);
    let prev = preview(&sketch, SketchTool::CenterpointArc, cursor, pending, None);
    assert_eq!(prev.arcs.len(), 1, "ghost arc emitted");
    let sweep = prev.arcs[0].sweep_angle.get::<radian>();
    assert!(
        sweep <= std::f64::consts::PI + 1e-9,
        "preview sweep {sweep} should be the minor arc",
    );
}

#[test]
fn centerpoint_arc_with_off_circle_endpoint_snap_falls_back_to_projection() {
    let off_circle = Point2::from_mm(0.5, 5.2);
    let (sketch, q) = add_point(fresh(), off_circle);
    let pending = Some(Pending::Second(
        ClickAnchor::Position(Point2::from_mm(0.0, 0.0)),
        ClickAnchor::Position(Point2::from_mm(5.0, 0.0)),
    ));
    let snap = SnapHit {
        kind: SnapKind::Endpoint(q),
        world: off_circle,
    };
    let (next, _) = place(
        sketch,
        SketchTool::CenterpointArc,
        off_circle,
        pending,
        Some(snap),
    );
    let Some(next) = next else {
        panic!("centerpoint arc commits with fallback")
    };
    let arc = only_arc(&next);
    assert!(
        arc.start() != q && arc.end() != q,
        "off-circle endpoint must not be reused as arc start or end",
    );
    let resolve = |id| match next.entities().get(id) {
        Some(SketchEntity::Point(p)) => p.at(),
        _ => panic!("point"),
    };
    let center_pos = resolve(arc.center());
    let radius = distance(center_pos, resolve(arc.start()));
    let end_radial = distance(center_pos, resolve(arc.end()));
    assert!(
        (end_radial - radius).abs() < 1e-9,
        "arc end must lie on the circle through start: {end_radial} vs {radius}",
    );
}

#[test]
fn line_first_click_with_midpoint_snap_defers_commit() {
    let (sketch, a) = add_point(fresh(), Point2::from_mm(0.0, 0.0));
    let (sketch, b) = add_point(sketch, Point2::from_mm(10.0, 0.0));
    let (sketch, line) = add_line(sketch, a, b, false);
    let snap = SnapHit {
        kind: SnapKind::Midpoint(line),
        world: Point2::from_mm(5.0, 0.0),
    };
    let (next, pending) = place(
        sketch,
        SketchTool::Line,
        Point2::from_mm(5.0, 0.0),
        None,
        Some(snap),
    );
    assert!(next.is_none(), "first click must not commit any entity");
    let Some(Pending::First(ClickAnchor::MidpointOf {
        line: pending_line,
        position,
    })) = pending
    else {
        panic!("expected pending MidpointOf, got {pending:?}");
    };
    assert_eq!(pending_line, line);
    assert_eq!(position, Point2::from_mm(5.0, 0.0));
}

#[test]
fn line_second_click_after_midpoint_first_commits_in_one_step() {
    let (sketch, a) = add_point(fresh(), Point2::from_mm(0.0, 0.0));
    let (sketch, b) = add_point(sketch, Point2::from_mm(10.0, 0.0));
    let (sketch, line) = add_line(sketch, a, b, false);
    let snap1 = SnapHit {
        kind: SnapKind::Midpoint(line),
        world: Point2::from_mm(5.0, 0.0),
    };
    let (none_first, pending) = place(
        sketch.clone(),
        SketchTool::Line,
        Point2::from_mm(5.0, 0.0),
        None,
        Some(snap1),
    );
    assert!(none_first.is_none());
    let (next, _) = place(
        sketch,
        SketchTool::Line,
        Point2::from_mm(5.0, 8.0),
        pending,
        None,
    );
    let Some(next) = next else {
        panic!("second click commits")
    };
    assert_eq!(count_kind(&next, SketchEntityKind::Point), 4);
    assert_eq!(count_kind(&next, SketchEntityKind::Line), 2);
    let mids = next
        .relation_order()
        .iter()
        .filter(|r| {
            matches!(
                next.relations().get(**r),
                Some(SketchRelation::Midpoint { line: l, .. }) if *l == line,
            )
        })
        .count();
    assert_eq!(mids, 1, "midpoint relation emitted exactly once on commit");
}

#[test]
fn line_tool_stale_endpoint_pending_clears_without_panic() {
    let pending = Some(Pending::First(ClickAnchor::Endpoint(
        SketchEntityId::default(),
    )));
    let (next, kept) = place(
        fresh(),
        SketchTool::Line,
        Point2::from_mm(5.0, 5.0),
        pending,
        None,
    );
    assert!(next.is_none(), "stale pending must not commit");
    assert_eq!(kept, None, "stale pending must clear so user can re-anchor");
}

#[test]
fn three_point_corner_rectangle_holds_when_third_click_is_collinear() {
    let p1 = Point2::from_mm(0.0, 0.0);
    let p2 = Point2::from_mm(4.0, 0.0);
    let collinear = Point2::from_mm(2.0, 0.0);
    let (_, pending) = place(
        fresh(),
        SketchTool::ThreePointCornerRectangle,
        p1,
        None,
        None,
    );
    let (_, pending) = place(
        fresh(),
        SketchTool::ThreePointCornerRectangle,
        p2,
        pending,
        None,
    );
    let (next, kept) = place(
        fresh(),
        SketchTool::ThreePointCornerRectangle,
        collinear,
        pending,
        None,
    );
    assert!(next.is_none());
    assert!(matches!(kept, Some(Pending::Second(_, _))));
}

#[test]
fn three_point_center_rectangle_holds_when_third_click_is_collinear() {
    let center = Point2::from_mm(0.0, 0.0);
    let mid = Point2::from_mm(5.0, 0.0);
    let collinear = Point2::from_mm(7.0, 0.0);
    let (_, pending) = place(
        fresh(),
        SketchTool::ThreePointCenterRectangle,
        center,
        None,
        None,
    );
    let (_, pending) = place(
        fresh(),
        SketchTool::ThreePointCenterRectangle,
        mid,
        pending,
        None,
    );
    let (next, kept) = place(
        fresh(),
        SketchTool::ThreePointCenterRectangle,
        collinear,
        pending,
        None,
    );
    assert!(next.is_none());
    assert!(matches!(kept, Some(Pending::Second(_, _))));
}

#[test]
fn parallelogram_holds_when_third_corner_collinear_with_base() {
    let p1 = Point2::from_mm(0.0, 0.0);
    let p2 = Point2::from_mm(4.0, 0.0);
    let collinear = Point2::from_mm(8.0, 0.0);
    let (_, pending) = place(fresh(), SketchTool::Parallelogram, p1, None, None);
    let (_, pending) = place(fresh(), SketchTool::Parallelogram, p2, pending, None);
    let (next, kept) = place(fresh(), SketchTool::Parallelogram, collinear, pending, None);
    assert!(next.is_none());
    assert!(matches!(kept, Some(Pending::Second(_, _))));
}
