use bone_document::{SketchEntity, SketchEntityKind, SketchRelation};
use bone_types::Point2;
use uom::si::length::millimeter;

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

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

#[test]
fn point_tool_commits_a_point_per_click() {
    let (next, pending) = place(
        fresh(),
        SketchTool::Point,
        Point2::from_mm(3.0, 4.0),
        None,
        None,
    );
    let Some(next) = next else {
        panic!("point tool must commit")
    };
    assert_eq!(count_kind(&next, SketchEntityKind::Point), 1);
    assert_eq!(pending, None);
}

#[test]
fn point_tool_endpoint_snap_suppresses_duplicate() {
    let (sketch, existing) = add_point(fresh(), Point2::from_mm(1.0, 1.0));
    let snap = SnapHit {
        kind: SnapKind::Endpoint(existing),
        world: Point2::from_mm(1.0, 1.0),
    };
    let (next, pending) = place(
        sketch,
        SketchTool::Point,
        Point2::from_mm(1.05, 1.0),
        None,
        Some(snap),
    );
    assert!(next.is_none());
    assert_eq!(pending, None);
}

#[test]
fn line_first_click_pends_anchor() {
    let world = Point2::from_mm(1.0, 2.0);
    let (next, pending) = place(fresh(), SketchTool::Line, world, None, None);
    assert!(next.is_none());
    assert_eq!(pending, Some(Pending::First(ClickAnchor::Position(world))),);
}

#[test]
fn line_second_click_emits_line() {
    let start = Point2::from_mm(0.0, 0.0);
    let end = Point2::from_mm(5.0, 5.0);
    let (next, pending) = place(
        fresh(),
        SketchTool::Line,
        end,
        Some(Pending::First(ClickAnchor::Position(start))),
        None,
    );
    let Some(next) = next else {
        panic!("second click commits")
    };
    assert_eq!(count_kind(&next, SketchEntityKind::Point), 2);
    assert_eq!(count_kind(&next, SketchEntityKind::Line), 1);
    assert!(matches!(
        pending,
        Some(Pending::First(ClickAnchor::Endpoint(_)))
    ));
}

#[test]
fn circle_first_click_pends_center() {
    let center = Point2::from_mm(1.0, 1.0);
    let (next, pending) = place(fresh(), SketchTool::Circle, center, None, None);
    assert!(next.is_none());
    assert_eq!(pending, Some(Pending::First(ClickAnchor::Position(center))),);
}

#[test]
fn circle_second_click_emits_center_point_and_circle() {
    let center = Point2::from_mm(0.0, 0.0);
    let radius_click = Point2::from_mm(3.0, 4.0);
    let (next, pending) = place(
        fresh(),
        SketchTool::Circle,
        radius_click,
        Some(Pending::First(ClickAnchor::Position(center))),
        None,
    );
    let Some(next) = next else {
        panic!("circle tool commits on second click")
    };
    assert_eq!(count_kind(&next, SketchEntityKind::Point), 1);
    assert_eq!(count_kind(&next, SketchEntityKind::Circle), 1);
    assert_eq!(pending, None);
    let Some((_, circle)) = next
        .entities()
        .iter()
        .find(|(_, e)| matches!(e, SketchEntity::Circle(_)))
    else {
        panic!("circle entity");
    };
    let SketchEntity::Circle(data) = circle else {
        panic!("kind");
    };
    let r = data.radius().get::<millimeter>();
    assert!((r - 5.0).abs() < 1e-9, "r={r}");
}

#[test]
fn perimeter_circle_emits_after_three_clicks() {
    let p1 = Point2::from_mm(1.0, 0.0);
    let p2 = Point2::from_mm(-1.0, 0.0);
    let p3 = Point2::from_mm(0.0, 1.0);
    let (s, pending) = place(fresh(), SketchTool::PerimeterCircle, p1, None, None);
    assert!(s.is_none());
    assert_eq!(pending, Some(Pending::First(ClickAnchor::Position(p1))),);
    let (s, pending) = place(fresh(), SketchTool::PerimeterCircle, p2, pending, None);
    assert!(s.is_none());
    let Some(Pending::Second(_, _)) = pending else {
        panic!("two anchors after second click");
    };
    let (next, pending) = place(fresh(), SketchTool::PerimeterCircle, p3, pending, None);
    let Some(next) = next else {
        panic!("third click commits")
    };
    assert_eq!(count_kind(&next, SketchEntityKind::Circle), 1);
    assert_eq!(count_kind(&next, SketchEntityKind::Point), 1);
    assert_eq!(pending, None);
}

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

#[test]
fn centerpoint_arc_emits_three_clicks() {
    let center = Point2::from_mm(0.0, 0.0);
    let start = Point2::from_mm(5.0, 0.0);
    let end = Point2::from_mm(0.0, 5.0);
    let (_, pending) = place(fresh(), SketchTool::CenterpointArc, center, None, None);
    let (_, pending) = place(fresh(), SketchTool::CenterpointArc, start, pending, None);
    let (next, pending) = place(fresh(), SketchTool::CenterpointArc, end, pending, None);
    let Some(next) = next else {
        panic!("arc commits on third click")
    };
    assert_eq!(count_kind(&next, SketchEntityKind::Arc), 1);
    assert_eq!(count_kind(&next, SketchEntityKind::Point), 3);
    assert_eq!(pending, None);
}

#[test]
fn three_point_arc_emits_four_entities() {
    let start = Point2::from_mm(-5.0, 0.0);
    let end = Point2::from_mm(5.0, 0.0);
    let mid = Point2::from_mm(0.0, 5.0);
    let (_, pending) = place(fresh(), SketchTool::ThreePointArc, start, None, None);
    let (_, pending) = place(fresh(), SketchTool::ThreePointArc, end, pending, None);
    let (next, pending) = place(fresh(), SketchTool::ThreePointArc, mid, pending, None);
    let Some(next) = next else {
        panic!("3-point arc commits")
    };
    assert_eq!(count_kind(&next, SketchEntityKind::Arc), 1);
    assert_eq!(count_kind(&next, SketchEntityKind::Point), 3);
    assert_eq!(pending, None);
}

#[test]
fn tangent_arc_requires_endpoint_anchor() {
    let world = Point2::from_mm(1.0, 1.0);
    let (next, pending) = place(fresh(), SketchTool::TangentArc, world, None, None);
    assert!(next.is_none());
    assert_eq!(pending, None);
}

#[test]
fn tangent_arc_preview_with_no_pending_emits_snap_dot_when_snap_fires() {
    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 snap_b = SnapHit {
        kind: SnapKind::Endpoint(b),
        world: Point2::from_mm(10.0, 0.0),
    };
    let prev = preview(
        &sketch,
        SketchTool::TangentArc,
        Point2::from_mm(10.0, 0.0),
        None,
        Some(snap_b),
    );
    assert_eq!(
        prev.snap,
        Some(Point2::from_mm(10.0, 0.0)),
        "snap dot must surface for hover over endpoint",
    );
    assert!(prev.anchors.is_empty(), "no anchor before first click");
    assert!(prev.arcs.is_empty(), "no ghost arc before first click");
}

#[test]
fn tangent_arc_picks_minor_arc_when_cursor_on_other_side() {
    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_b = SnapHit {
        kind: SnapKind::Endpoint(b),
        world: Point2::from_mm(10.0, 0.0),
    };
    let (_, pending) = place(
        sketch.clone(),
        SketchTool::TangentArc,
        Point2::from_mm(10.0, 0.0),
        None,
        Some(snap_b),
    );
    let (next, _) = place(
        sketch,
        SketchTool::TangentArc,
        Point2::from_mm(15.0, -3.0),
        pending,
        None,
    );
    let Some(next) = next else {
        panic!("tangent arc commits");
    };
    let sweep = arc_sweep(&next, super::only_arc(&next));
    assert!(
        sweep <= std::f64::consts::PI + 1e-9,
        "sweep {sweep} should be minor arc",
    );
}

#[test]
fn tangent_arc_preview_picks_minor_arc_when_cursor_on_other_side() {
    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 pending = Some(Pending::First(ClickAnchor::Endpoint(b)));
    let cursor = Point2::from_mm(15.0, -3.0);
    let prev = preview(&sketch, SketchTool::TangentArc, cursor, pending, None);
    assert_eq!(prev.arcs.len(), 1, "ghost arc emitted");
    let sweep = prev.arcs[0].sweep_angle.get::<uom::si::angle::radian>();
    assert!(
        sweep <= std::f64::consts::PI + 1e-9,
        "preview sweep {sweep} should be minor arc",
    );
}

#[test]
fn tangent_arc_rejects_first_click_on_orphan_point() {
    let (sketch, orphan) = add_point(fresh(), Point2::from_mm(0.0, 0.0));
    let snap = SnapHit {
        kind: SnapKind::Endpoint(orphan),
        world: Point2::from_mm(0.0, 0.0),
    };
    let (next, pending) = place(
        sketch,
        SketchTool::TangentArc,
        Point2::from_mm(0.0, 0.0),
        None,
        Some(snap),
    );
    assert!(next.is_none());
    assert_eq!(pending, None, "no pending when host curve is missing");
}

#[test]
fn tangent_arc_accepts_first_click_on_arc_endpoint() {
    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 (sketch, end) = add_point(sketch, Point2::from_mm(0.0, 5.0));
    let (sketch, _arc) = add_arc(sketch, center, start, end, false);
    let snap = SnapHit {
        kind: SnapKind::Endpoint(start),
        world: Point2::from_mm(5.0, 0.0),
    };
    let (_, pending) = place(
        sketch,
        SketchTool::TangentArc,
        Point2::from_mm(5.0, 0.0),
        None,
        Some(snap),
    );
    assert!(matches!(
        pending,
        Some(Pending::First(ClickAnchor::Endpoint(id))) if id == start
    ));
}

#[test]
fn tangent_arc_continues_minor_arc_off_arc_end_host() {
    let (sketch, host_center) = add_point(fresh(), Point2::from_mm(0.0, 0.0));
    let (sketch, host_start) = add_point(sketch, Point2::from_mm(5.0, 0.0));
    let (sketch, host_end) = add_point(sketch, Point2::from_mm(0.0, 5.0));
    let (sketch, host_arc) = add_arc(sketch, host_center, host_start, host_end, false);
    let snap_join = SnapHit {
        kind: SnapKind::Endpoint(host_end),
        world: Point2::from_mm(0.0, 5.0),
    };
    let (_, pending) = place(
        sketch.clone(),
        SketchTool::TangentArc,
        Point2::from_mm(0.0, 5.0),
        None,
        Some(snap_join),
    );
    let (next, _) = place(
        sketch,
        SketchTool::TangentArc,
        Point2::from_mm(-3.0, 8.0),
        pending,
        None,
    );
    let Some(next) = next else {
        panic!("tangent arc commits");
    };
    let Some((_, SketchEntity::Arc(arc))) = next
        .entities()
        .iter()
        .find(|(id, e)| *id != host_arc && matches!(e, SketchEntity::Arc(_)))
    else {
        panic!("new arc entity");
    };
    let sweep = arc_sweep(&next, *arc);
    assert!(
        sweep <= std::f64::consts::PI + 1e-9,
        "sweep {sweep} should be the minor arc, tangent continuity at host arc.end",
    );
}

#[test]
fn tangent_arc_drops_pending_on_no_op_second_click_at_start() {
    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_b = SnapHit {
        kind: SnapKind::Endpoint(b),
        world: Point2::from_mm(10.0, 0.0),
    };
    let (_, pending) = place(
        sketch.clone(),
        SketchTool::TangentArc,
        Point2::from_mm(10.0, 0.0),
        None,
        Some(snap_b),
    );
    let (next, kept) = place(
        sketch,
        SketchTool::TangentArc,
        Point2::from_mm(10.0, 0.0),
        pending,
        Some(snap_b),
    );
    assert!(
        next.is_none(),
        "second-click on starting endpoint must not commit",
    );
    assert!(
        matches!(kept, Some(Pending::First(ClickAnchor::Endpoint(id))) if id == b),
        "pending kept so the user can click again",
    );
}

#[test]
fn tangent_arc_emits_arc_and_relation() {
    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_b = SnapHit {
        kind: SnapKind::Endpoint(b),
        world: Point2::from_mm(10.0, 0.0),
    };
    let (_, pending) = place(
        sketch.clone(),
        SketchTool::TangentArc,
        Point2::from_mm(10.0, 0.0),
        None,
        Some(snap_b),
    );
    assert!(matches!(
        pending,
        Some(Pending::First(ClickAnchor::Endpoint(_)))
    ));
    let end = Point2::from_mm(10.0, 6.0);
    let (next, pending) = place(sketch, SketchTool::TangentArc, end, pending, None);
    let Some(next) = next else {
        panic!("tangent arc commits")
    };
    assert_eq!(count_kind(&next, SketchEntityKind::Arc), 1);
    assert_eq!(pending, None);
    assert!(
        next.relation_order().iter().any(|rid| matches!(
            next.relations().get(*rid),
            Some(SketchRelation::Tangent(_, _)),
        )),
        "tangent relation emitted",
    );
}
