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

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

use super::{count_kind, fresh};

fn only_line(sketch: &Sketch) -> (SketchEntityId, LineData) {
    let Some((id, SketchEntity::Line(line))) = sketch
        .entities()
        .iter()
        .find(|(_, e)| matches!(e, SketchEntity::Line(_)))
    else {
        panic!("line entity");
    };
    (id, *line)
}

fn line_relations(sketch: &Sketch, line: SketchEntityId) -> Vec<SketchRelation> {
    sketch
        .relation_order()
        .iter()
        .filter_map(|rid| sketch.relations().get(*rid).copied())
        .filter(|r| r.references().into_iter().any(|id| id == line))
        .collect()
}

fn end_point(sketch: &Sketch, line: LineData) -> Point2 {
    let Some(SketchEntity::Point(p)) = sketch.entities().get(line.b()) else {
        panic!("end point");
    };
    p.at()
}

#[test]
fn line_first_click_endpoint_snap_reuses_existing_point() {
    let (sketch, existing) = add_point(fresh(), Point2::from_mm(0.0, 0.0));
    let snap = SnapHit {
        kind: SnapKind::Endpoint(existing),
        world: Point2::from_mm(0.0, 0.0),
    };
    let (next, pending) = place(
        sketch,
        SketchTool::Line,
        Point2::from_mm(0.05, 0.0),
        None,
        Some(snap),
    );
    assert!(next.is_none(), "first click does not commit");
    assert_eq!(
        pending,
        Some(Pending::First(ClickAnchor::Endpoint(existing))),
        "endpoint inference recorded into pending anchor",
    );
}

#[test]
fn line_end_click_endpoint_snap_reuses_existing_point_and_adds_no_axis_relation() {
    let (sketch, a) = add_point(fresh(), Point2::from_mm(0.0, 0.0));
    let (sketch, b) = add_point(sketch, Point2::from_mm(7.0, 3.0));
    let snap = SnapHit {
        kind: SnapKind::Endpoint(b),
        world: Point2::from_mm(7.0, 3.0),
    };
    let (next, _) = place(
        sketch,
        SketchTool::Line,
        Point2::from_mm(7.05, 3.0),
        Some(Pending::First(ClickAnchor::Endpoint(a))),
        Some(snap),
    );
    let Some(next) = next else {
        panic!("line commits with endpoint snap on end click");
    };
    assert_eq!(
        count_kind(&next, SketchEntityKind::Point),
        2,
        "no new point materialised; existing endpoints reused",
    );
    let (line_id, line) = only_line(&next);
    assert_eq!(line.a(), a);
    assert_eq!(line.b(), b);
    assert!(
        line_relations(&next, line_id).is_empty(),
        "endpoint snap fires no extra relation; coincidence is structural",
    );
}

#[test]
fn line_end_click_horizontal_snap_adds_horizontal_relation() {
    let (sketch, a) = add_point(fresh(), Point2::from_mm(0.0, 0.0));
    let snap = SnapHit {
        kind: SnapKind::Horizontal,
        world: Point2::from_mm(10.0, 0.0),
    };
    let (next, _) = place(
        sketch,
        SketchTool::Line,
        Point2::from_mm(10.0, 0.05),
        Some(Pending::First(ClickAnchor::Endpoint(a))),
        Some(snap),
    );
    let Some(next) = next else {
        panic!("line commits");
    };
    let (line_id, line) = only_line(&next);
    let relations = line_relations(&next, line_id);
    assert!(
        relations
            .iter()
            .any(|r| matches!(r, SketchRelation::Horizontal(id) if *id == line_id)),
        "horizontal inference must add Horizontal relation: {relations:?}",
    );
    let (_, ey) = end_point(&next, line).coords_mm();
    assert!(
        ey.abs() < 1e-9,
        "end point projected onto axis (y == anchor.y), got {ey}",
    );
}

#[test]
fn line_end_click_vertical_snap_adds_vertical_relation() {
    let (sketch, a) = add_point(fresh(), Point2::from_mm(0.0, 0.0));
    let snap = SnapHit {
        kind: SnapKind::Vertical,
        world: Point2::from_mm(0.0, 10.0),
    };
    let (next, _) = place(
        sketch,
        SketchTool::Line,
        Point2::from_mm(0.05, 10.0),
        Some(Pending::First(ClickAnchor::Endpoint(a))),
        Some(snap),
    );
    let Some(next) = next else {
        panic!("line commits");
    };
    let (line_id, line) = only_line(&next);
    let relations = line_relations(&next, line_id);
    assert!(
        relations
            .iter()
            .any(|r| matches!(r, SketchRelation::Vertical(id) if *id == line_id)),
        "vertical inference must add Vertical relation: {relations:?}",
    );
    let (ex, _) = end_point(&next, line).coords_mm();
    assert!(
        ex.abs() < 1e-9,
        "end point projected onto axis (x == anchor.x), got {ex}",
    );
}

#[test]
fn line_end_click_tangent_snap_adds_tangent_relation() {
    let (sketch, a) = add_point(fresh(), Point2::from_mm(0.0, 0.0));
    let (sketch, c) = add_point(sketch, Point2::from_mm(5.0, 0.0));
    let (sketch, circle) = add_circle(sketch, c, bone_types::Length::new::<millimeter>(3.0), false);
    let snap = SnapHit {
        kind: SnapKind::Tangent(circle),
        world: Point2::from_mm(3.2, 2.4),
    };
    let (next, _) = place(
        sketch,
        SketchTool::Line,
        Point2::from_mm(3.2, 2.4),
        Some(Pending::First(ClickAnchor::Endpoint(a))),
        Some(snap),
    );
    let Some(next) = next else {
        panic!("line commits");
    };
    let (line_id, _) = only_line(&next);
    let relations = line_relations(&next, line_id);
    assert!(
        relations.iter().any(|r| matches!(
            r,
            SketchRelation::Tangent(left, right)
                if (*left == line_id && *right == circle) || (*left == circle && *right == line_id)
        )),
        "tangent inference must add Tangent(line, circle) relation: {relations:?}",
    );
}

#[test]
fn line_end_click_midpoint_snap_adds_midpoint_relation() {
    let (sketch, p0) = add_point(fresh(), Point2::from_mm(0.0, 0.0));
    let (sketch, p1) = add_point(sketch, Point2::from_mm(10.0, 0.0));
    let (sketch, host) = add_line(sketch, p0, p1, false);
    let (sketch, anchor) = add_point(sketch, Point2::from_mm(5.0, 5.0));
    let snap = SnapHit {
        kind: SnapKind::Midpoint(host),
        world: Point2::from_mm(5.0, 0.0),
    };
    let (next, _) = place(
        sketch,
        SketchTool::Line,
        Point2::from_mm(5.0, 0.0),
        Some(Pending::First(ClickAnchor::Endpoint(anchor))),
        Some(snap),
    );
    let Some(next) = next else {
        panic!("line commits");
    };
    let mid_relations: Vec<_> = next
        .relation_order()
        .iter()
        .filter_map(|rid| next.relations().get(*rid).copied())
        .filter(|r| matches!(r, SketchRelation::Midpoint { line, .. } if *line == host))
        .collect();
    assert_eq!(
        mid_relations.len(),
        1,
        "midpoint inference adds exactly one Midpoint relation against the host line",
    );
}
