use std::collections::{BTreeSet, HashMap};

use bone_document::{
    DimensionKind, EditOutcome, Sketch, Sketch3, Sketch3Entity, Sketch3Relation, SketchDimension,
    SketchEdit, SketchEntity, SketchRelation, SketchStatusReport,
};
use bone_types::{
    Length, Plane3, Point2, Point3, SketchDimensionId, SketchEntityId, SketchItemId,
    SketchPlaneBasis, SketchRelationId, UnitVec3,
};
use uom::si::length::millimeter;

use crate::sketch_mode::{ClickAnchor, Pending, SketchTool};
use crate::snap::SnapHit;
use crate::tools;

#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub enum PlaneLock {
    Locked,
    Free,
}

pub struct PlaneView {
    plane: SketchPlaneBasis,
    sketch: Sketch,
    to_3d: HashMap<SketchEntityId, SketchEntityId>,
    to_2d: HashMap<SketchEntityId, SketchEntityId>,
}

impl PlaneView {
    #[must_use]
    pub fn plane(&self) -> SketchPlaneBasis {
        self.plane
    }

    #[must_use]
    pub fn sketch(&self) -> &Sketch {
        &self.sketch
    }

    #[must_use]
    pub fn to_3d(&self, projected: SketchEntityId) -> Option<SketchEntityId> {
        self.to_3d.get(&projected).copied()
    }

    #[must_use]
    pub fn to_2d(&self, three: SketchEntityId) -> Option<SketchEntityId> {
        self.to_2d.get(&three).copied()
    }
}

#[must_use]
pub fn flatten(plane: SketchPlaneBasis, world: Point3) -> Point2 {
    Plane3::from(plane).project(world)
}

#[must_use]
pub fn project(sketch3: &Sketch3, plane: SketchPlaneBasis) -> PlaneView {
    let projected = sketch3.project_onto(plane);
    let to_3d: HashMap<SketchEntityId, SketchEntityId> = projected
        .sketch()
        .entity_order()
        .iter()
        .filter_map(|id2| projected.to_3d(*id2).map(|id3| (*id2, id3)))
        .collect();
    let to_2d: HashMap<SketchEntityId, SketchEntityId> =
        to_3d.iter().map(|(id2, id3)| (*id3, *id2)).collect();
    let sketch = sketch3
        .relation_order()
        .iter()
        .filter_map(|rid| sketch3.relations().get(*rid).copied())
        .filter_map(|relation| unproject_relation(relation, plane, &to_2d))
        .fold(projected.into_sketch(), |sketch, edit| {
            match sketch.clone().apply(edit) {
                Ok((next, _)) => next,
                Err(_) => sketch,
            }
        });
    PlaneView {
        plane,
        sketch,
        to_3d,
        to_2d,
    }
}

#[must_use]
pub fn project_sketch(sketch3: &Sketch3, plane: SketchPlaneBasis) -> Sketch {
    project(sketch3, plane).sketch
}

#[must_use]
pub fn relation_item_map(
    sketch3: &Sketch3,
    plane: SketchPlaneBasis,
) -> HashMap<SketchRelationId, SketchItemId> {
    let projected = sketch3.project_onto(plane);
    let to_2d: HashMap<SketchEntityId, SketchEntityId> = sketch3
        .entity_order()
        .iter()
        .filter_map(|id3| projected.to_2d(*id3).map(|id2| (*id3, id2)))
        .collect();
    sketch3
        .relation_order()
        .iter()
        .filter_map(|rid| {
            sketch3
                .relations()
                .get(*rid)
                .copied()
                .map(|rel| (*rid, rel))
        })
        .filter_map(|(rid, rel)| unproject_relation(rel, plane, &to_2d).map(|edit| (rid, edit)))
        .fold(
            (projected.into_sketch(), HashMap::new()),
            |(sketch, mut map), (rid, edit)| match sketch.clone().apply(edit) {
                Ok((next, EditOutcome::Dimension(dim))) => {
                    map.insert(rid, SketchItemId::Dimension(dim));
                    (next, map)
                }
                Ok((next, EditOutcome::Relation(rel))) => {
                    map.insert(rid, SketchItemId::Relation(rel));
                    (next, map)
                }
                Ok((next, _)) => (next, map),
                Err(_) => (sketch, map),
            },
        )
        .1
}

#[must_use]
pub fn dimension_relation_map(
    sketch3: &Sketch3,
    plane: SketchPlaneBasis,
) -> HashMap<SketchDimensionId, SketchRelationId> {
    relation_item_map(sketch3, plane)
        .into_iter()
        .filter_map(|(rid, item)| match item {
            SketchItemId::Dimension(dim) => Some((dim, rid)),
            _ => None,
        })
        .collect()
}

#[must_use]
pub fn relation_for_item(
    sketch3: &Sketch3,
    plane: SketchPlaneBasis,
    item: SketchItemId,
) -> Option<SketchRelationId> {
    relation_item_map(sketch3, plane)
        .into_iter()
        .find_map(|(rid, mapped)| (mapped == item).then_some(rid))
}

#[must_use]
pub fn project_status(
    sketch3: &Sketch3,
    plane: SketchPlaneBasis,
    report: &SketchStatusReport,
) -> SketchStatusReport {
    let projected = sketch3.project_onto(plane);
    let relations = relation_item_map(sketch3, plane);
    let remap = |item: &SketchItemId| match *item {
        SketchItemId::Entity(id3) => {
            let mapped = projected.to_2d(id3).map(SketchItemId::Entity);
            debug_assert!(
                mapped.is_some(),
                "offending sketch entity {id3:?} has no projected 2D image"
            );
            mapped
        }
        SketchItemId::Relation(rid) => relations.get(&rid).copied().or_else(|| {
            sketch3.relations().get(rid).and_then(|rel| {
                rel.references()
                    .into_iter()
                    .find_map(|eid| projected.to_2d(eid).map(SketchItemId::Entity))
            })
        }),
        SketchItemId::Dimension(dim) => Some(SketchItemId::Dimension(dim)),
    };
    let offending = report.offending().iter().filter_map(remap).collect();
    let redundant = report.redundant().iter().filter_map(remap).collect();
    report
        .clone()
        .with_offending(offending)
        .with_redundant(redundant)
}

fn aligned(a: UnitVec3, b: UnitVec3) -> bool {
    let (ax, ay, az) = a.components();
    let (bx, by, bz) = b.components();
    ax.mul_add(bx, ay.mul_add(by, az * bz)).abs() > 0.999
}

const fn dimension_kind(reference: bool) -> DimensionKind {
    if reference {
        DimensionKind::Driven
    } else {
        DimensionKind::Driving
    }
}

fn unproject_relation(
    relation: Sketch3Relation,
    plane: SketchPlaneBasis,
    to_2d: &HashMap<SketchEntityId, SketchEntityId>,
) -> Option<SketchEdit> {
    let m = |id| to_2d.get(&id).copied();
    let mapped = match relation {
        Sketch3Relation::Coincident(a, b) => SketchRelation::Coincident(m(a)?, m(b)?),
        Sketch3Relation::PointOnLine { point, line } => {
            SketchRelation::Coincident(m(point)?, m(line)?)
        }
        Sketch3Relation::Perpendicular(a, b) => SketchRelation::Perpendicular(m(a)?, m(b)?),
        Sketch3Relation::Parallel(a, b) => SketchRelation::Parallel(m(a)?, m(b)?),
        Sketch3Relation::Tangent(a, b) => SketchRelation::Tangent(m(a)?, m(b)?),
        Sketch3Relation::Equal(a, b) => SketchRelation::Equal(m(a)?, m(b)?),
        Sketch3Relation::Concentric(a, b) => SketchRelation::Concentric(m(a)?, m(b)?),
        Sketch3Relation::Midpoint { point, line } => SketchRelation::Midpoint {
            point: m(point)?,
            line: m(line)?,
        },
        Sketch3Relation::Symmetric { a, b, axis } => SketchRelation::Symmetric {
            a: m(a)?,
            b: m(b)?,
            axis: m(axis)?,
        },
        Sketch3Relation::Fix(a) => SketchRelation::Fix(m(a)?),
        Sketch3Relation::Horizontal(line) => SketchRelation::Horizontal(m(line)?),
        Sketch3Relation::Vertical(line) => SketchRelation::Vertical(m(line)?),
        Sketch3Relation::AlongAxis { line, axis } => {
            if aligned(axis, plane.x_axis()) {
                SketchRelation::Horizontal(m(line)?)
            } else if aligned(axis, plane.y_axis()) {
                SketchRelation::Vertical(m(line)?)
            } else {
                return None;
            }
        }
        Sketch3Relation::Distance {
            a,
            b,
            value,
            reference,
        } => {
            return Some(SketchEdit::AddDimension(SketchDimension::Linear {
                a: m(a)?,
                b: m(b)?,
                value,
                kind: dimension_kind(reference),
            }));
        }
        Sketch3Relation::Radius {
            target,
            value,
            reference,
        } => {
            return Some(SketchEdit::AddDimension(SketchDimension::Radius {
                target: m(target)?,
                value,
                kind: dimension_kind(reference),
            }));
        }
        Sketch3Relation::Diameter {
            target,
            value,
            reference,
        } => {
            return Some(SketchEdit::AddDimension(SketchDimension::Diameter {
                target: m(target)?,
                value,
                kind: dimension_kind(reference),
            }));
        }
        Sketch3Relation::Angle {
            a,
            b,
            value,
            reference,
        } => {
            return Some(SketchEdit::AddDimension(SketchDimension::Angular {
                a: m(a)?,
                b: m(b)?,
                value,
                kind: dimension_kind(reference),
            }));
        }
        Sketch3Relation::OnPlane { .. } | Sketch3Relation::Pierce { .. } => return None,
    };
    Some(SketchEdit::AddRelation(mapped))
}

#[must_use]
pub fn lift(plane: SketchPlaneBasis, point: Point2) -> Point3 {
    let (u, v) = point.coords_mm();
    Plane3::from(plane).point_at_local(u, v, 0.0)
}

fn plane_offset(plane: SketchPlaneBasis) -> Length {
    let (nx, ny, nz) = plane.normal().components();
    let (ox, oy, oz) = plane.origin().coords_mm();
    Length::new::<millimeter>(ox.mul_add(nx, oy.mul_add(ny, oz * nz)))
}

#[must_use]
pub fn embed_point(
    sketch3: &Sketch3,
    plane: SketchPlaneBasis,
    point: Point2,
) -> (Sketch3, SketchEntityId) {
    let (next, id) = sketch3.with_entity(Sketch3Entity::point(lift(plane, point)));
    let (next, _) = next.with_relation(Sketch3Relation::OnPlane {
        point: id,
        normal: plane.normal(),
        offset: plane_offset(plane),
    });
    (next, id)
}

#[must_use]
pub fn embed_line(
    sketch3: &Sketch3,
    from: SketchEntityId,
    to: SketchEntityId,
    for_construction: bool,
) -> (Sketch3, SketchEntityId) {
    sketch3.with_entity(Sketch3Entity::line(from, to, for_construction))
}

pub struct Placed3 {
    pub sketch: Sketch3,
    pub pending: Option<Pending>,
}

#[must_use]
pub fn embed_place(
    base: &Sketch3,
    view: &PlaneView,
    tool: SketchTool,
    effective: Point2,
    pending3: Option<Pending>,
    snap: Option<SnapHit>,
    lock: PlaneLock,
) -> Placed3 {
    let pending2 = pending3.and_then(|p| translate_pending(p, |id| view.to_2d(id)));
    let (placed, next2) = tools::place(view.sketch().clone(), tool, effective, pending2, snap);
    let Some(new2) = placed else {
        return Placed3 {
            sketch: base.clone(),
            pending: next2.and_then(|p| translate_pending(p, |id| view.to_3d(id))),
        };
    };
    let (sketch, map) = embed_delta(base, view, &new2, lock);
    Placed3 {
        sketch,
        pending: next2.and_then(|p| translate_pending(p, |id| map.get(&id).copied())),
    }
}

pub(crate) fn embed_delta(
    base: &Sketch3,
    view: &PlaneView,
    edited: &Sketch,
    lock: PlaneLock,
) -> (Sketch3, HashMap<SketchEntityId, SketchEntityId>) {
    let plane = view.plane();
    let old_entities: BTreeSet<SketchEntityId> =
        view.sketch().entity_order().iter().copied().collect();
    let old_relations: BTreeSet<_> = view.sketch().relation_order().iter().copied().collect();
    let mut map: HashMap<SketchEntityId, SketchEntityId> = view
        .sketch()
        .entity_order()
        .iter()
        .filter_map(|id2| view.to_3d(*id2).map(|id3| (*id2, id3)))
        .collect();
    let fresh: Vec<SketchEntityId> = edited
        .entity_order()
        .iter()
        .copied()
        .filter(|id| !old_entities.contains(id))
        .collect();
    let points = fresh
        .iter()
        .filter(|id| matches!(edited.entities().get(**id), Some(SketchEntity::Point(_))));
    let curves = fresh
        .iter()
        .filter(|id| !matches!(edited.entities().get(**id), Some(SketchEntity::Point(_))));
    let sketch = points.chain(curves).fold(base.clone(), |sketch, id2| {
        match embed_entity(&sketch, plane, edited.entities().get(*id2), &map, lock) {
            Some((next, id3)) => {
                map.insert(*id2, id3);
                next
            }
            None => sketch,
        }
    });
    let relations: Vec<Sketch3Relation> = edited
        .relation_order()
        .iter()
        .filter(|rid| !old_relations.contains(*rid))
        .filter_map(|rid| edited.relations().get(*rid).copied())
        .filter_map(|rel| map_relation(rel, &map, &sketch))
        .collect();
    let sketch = relations
        .into_iter()
        .fold(sketch, |sketch, rel| sketch.with_relation(rel).0);
    (sketch, map)
}

#[must_use]
pub(crate) fn to_plane_pending(view: &PlaneView, pending3: Option<Pending>) -> Option<Pending> {
    pending3.and_then(|p| translate_pending(p, |id| view.to_2d(id)))
}

fn embed_entity(
    sketch3: &Sketch3,
    plane: SketchPlaneBasis,
    entity: Option<&SketchEntity>,
    map: &HashMap<SketchEntityId, SketchEntityId>,
    lock: PlaneLock,
) -> Option<(Sketch3, SketchEntityId)> {
    let to3d = |id| map.get(&id).copied();
    match entity? {
        SketchEntity::Point(point) => Some(match lock {
            PlaneLock::Locked => embed_point(sketch3, plane, point.at()),
            PlaneLock::Free => sketch3.with_entity(Sketch3Entity::point(lift(plane, point.at()))),
        }),
        SketchEntity::Line(line) => Some(embed_line(
            sketch3,
            to3d(line.a())?,
            to3d(line.b())?,
            line.for_construction(),
        )),
        SketchEntity::Circle(circle) => Some(sketch3.with_entity(Sketch3Entity::circle(
            to3d(circle.center())?,
            circle.radius(),
            plane.normal(),
            circle.for_construction(),
        ))),
        SketchEntity::Arc(arc) => Some(sketch3.with_entity(Sketch3Entity::arc(
            to3d(arc.center())?,
            to3d(arc.start())?,
            to3d(arc.end())?,
            plane.normal(),
            arc.for_construction(),
        ))),
        SketchEntity::Ellipse(ellipse) => Some(sketch3.with_entity(Sketch3Entity::ellipse(
            to3d(ellipse.center())?,
            to3d(ellipse.major())?,
            to3d(ellipse.minor())?,
            plane.normal(),
            ellipse.for_construction(),
        ))),
        SketchEntity::Spline(spline) => {
            let control = spline
                .control()
                .iter()
                .map(|id| to3d(*id))
                .collect::<Option<Vec<_>>>()?;
            Some(sketch3.with_entity(Sketch3Entity::spline(
                control,
                spline.closed(),
                spline.for_construction(),
            )))
        }
    }
}

#[cfg(test)]
#[must_use]
pub fn embed_sketch(sketch: &Sketch) -> Sketch3 {
    let basis = sketch.plane();
    let points = sketch
        .entity_order()
        .iter()
        .filter(|id| matches!(sketch.entities().get(**id), Some(SketchEntity::Point(_))));
    let others = sketch
        .entity_order()
        .iter()
        .filter(|id| !matches!(sketch.entities().get(**id), Some(SketchEntity::Point(_))));
    let (base, map) = points.chain(others).fold(
        (Sketch3::on_plane(basis), HashMap::new()),
        |(s3, mut map), id2| match embed_entity(
            &s3,
            basis,
            sketch.entities().get(*id2),
            &map,
            PlaneLock::Locked,
        ) {
            Some((next, id3)) => {
                map.insert(*id2, id3);
                (next, map)
            }
            None => (s3, map),
        },
    );
    let relations: Vec<Sketch3Relation> = sketch
        .relation_order()
        .iter()
        .filter_map(|rid| sketch.relations().get(*rid).copied())
        .filter_map(|relation| map_relation(relation, &map, &base))
        .collect();
    let with_relations = relations
        .into_iter()
        .fold(base, |s3, relation| s3.with_relation(relation).0);
    sketch
        .dimension_order()
        .iter()
        .filter_map(|did| sketch.dimensions().get(*did).copied())
        .filter_map(|dimension| map_dimension(dimension, &map))
        .fold(with_relations, |s3, relation| s3.with_relation(relation).0)
}

pub(crate) fn map_dimension(
    dimension: SketchDimension,
    map: &HashMap<SketchEntityId, SketchEntityId>,
) -> Option<Sketch3Relation> {
    let m = |id| map.get(&id).copied();
    let reference = matches!(dimension.kind(), DimensionKind::Driven);
    match dimension {
        SketchDimension::Linear { a, b, value, .. } => Some(Sketch3Relation::Distance {
            a: m(a)?,
            b: m(b)?,
            value,
            reference,
        }),
        SketchDimension::Radius { target, value, .. } => Some(Sketch3Relation::Radius {
            target: m(target)?,
            value,
            reference,
        }),
        SketchDimension::Diameter { target, value, .. } => Some(Sketch3Relation::Diameter {
            target: m(target)?,
            value,
            reference,
        }),
        SketchDimension::Angular { a, b, value, .. } => Some(Sketch3Relation::Angle {
            a: m(a)?,
            b: m(b)?,
            value,
            reference,
        }),
    }
}

pub(crate) fn map_relation(
    relation: SketchRelation,
    map: &HashMap<SketchEntityId, SketchEntityId>,
    sketch3: &Sketch3,
) -> Option<Sketch3Relation> {
    let m = |id| map.get(&id).copied();
    let is_point = |id| matches!(sketch3.entities().get(id), Some(Sketch3Entity::Point(_)));
    let is_line = |id| matches!(sketch3.entities().get(id), Some(Sketch3Entity::Line(_)));
    let is_curve = |id| {
        matches!(
            sketch3.entities().get(id),
            Some(Sketch3Entity::Circle(_) | Sketch3Entity::Arc(_) | Sketch3Entity::Ellipse(_))
        )
    };
    match relation {
        SketchRelation::Coincident(a, b) => {
            let (a3, b3) = (m(a)?, m(b)?);
            match (
                is_point(a3),
                is_point(b3),
                is_line(a3),
                is_line(b3),
                is_curve(a3),
                is_curve(b3),
            ) {
                (true, true, ..) => Some(Sketch3Relation::Coincident(a3, b3)),
                (true, _, _, true, _, _) => Some(Sketch3Relation::PointOnLine {
                    point: a3,
                    line: b3,
                }),
                (_, true, true, _, _, _) => Some(Sketch3Relation::PointOnLine {
                    point: b3,
                    line: a3,
                }),
                (true, _, _, _, _, true) => Some(Sketch3Relation::Coincident(a3, b3)),
                (_, true, _, _, true, _) => Some(Sketch3Relation::Coincident(b3, a3)),
                _ => None,
            }
        }
        SketchRelation::Horizontal(line) => Some(Sketch3Relation::Horizontal(m(line)?)),
        SketchRelation::Vertical(line) => Some(Sketch3Relation::Vertical(m(line)?)),
        SketchRelation::Parallel(a, b) => Some(Sketch3Relation::Parallel(m(a)?, m(b)?)),
        SketchRelation::Perpendicular(a, b) => Some(Sketch3Relation::Perpendicular(m(a)?, m(b)?)),
        SketchRelation::Tangent(a, b) => Some(Sketch3Relation::Tangent(m(a)?, m(b)?)),
        SketchRelation::Equal(a, b) => Some(Sketch3Relation::Equal(m(a)?, m(b)?)),
        SketchRelation::Concentric(a, b) => Some(Sketch3Relation::Concentric(m(a)?, m(b)?)),
        SketchRelation::Midpoint { point, line } => Some(Sketch3Relation::Midpoint {
            point: m(point)?,
            line: m(line)?,
        }),
        SketchRelation::Symmetric { a, b, axis } => Some(Sketch3Relation::Symmetric {
            a: m(a)?,
            b: m(b)?,
            axis: m(axis)?,
        }),
        SketchRelation::Fix(id) => Some(Sketch3Relation::Fix(m(id)?)),
    }
}

fn translate_pending(
    pending: Pending,
    map: impl Fn(SketchEntityId) -> Option<SketchEntityId>,
) -> Option<Pending> {
    match pending {
        Pending::First(a) => translate_anchor(a, &map).map(Pending::First),
        Pending::Second(a, b) => Some(Pending::Second(
            translate_anchor(a, &map)?,
            translate_anchor(b, &map)?,
        )),
    }
}

fn translate_anchor(
    anchor: ClickAnchor,
    map: impl Fn(SketchEntityId) -> Option<SketchEntityId>,
) -> Option<ClickAnchor> {
    match anchor {
        ClickAnchor::Position(p) => Some(ClickAnchor::Position(p)),
        ClickAnchor::Endpoint(id) => map(id).map(ClickAnchor::Endpoint),
        ClickAnchor::MidpointOf { line, position } => {
            map(line).map(|line| ClickAnchor::MidpointOf { line, position })
        }
        ClickAnchor::OnEdge { entity, position } => {
            map(entity).map(|entity| ClickAnchor::OnEdge { entity, position })
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use bone_types::{Tolerance, UnitVec3};

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

    fn plane() -> SketchPlaneBasis {
        let Ok(basis) = SketchPlaneBasis::new(
            Point3::from_mm(0.0, 0.0, 5.0),
            UnitVec3::x_axis(),
            UnitVec3::y_axis(),
            TOL,
        ) else {
            panic!("xy plane offset by 5mm is orthonormal");
        };
        basis
    }

    #[test]
    fn project_round_trips_points_into_plane_coordinates() {
        let basis = plane();
        let (s3, a) = embed_point(&Sketch3::new(), basis, Point2::from_mm(3.0, -4.0));
        let (s3, b) = embed_point(&s3, basis, Point2::from_mm(7.0, 1.0));
        let (s3, _line) = embed_line(&s3, a, b, false);

        let view = project(&s3, basis);
        assert_eq!(
            view.sketch().entity_order().len(),
            3,
            "two points and one line"
        );
        let Some((sx, sy)) =
            view.sketch()
                .entities()
                .iter()
                .find_map(|(id, entity)| match entity {
                    SketchEntity::Point(p) if view.to_3d(id) == Some(a) => Some(p.at().coords_mm()),
                    _ => None,
                })
        else {
            panic!("the first projected point maps back to its 3d id");
        };
        assert!(
            (sx - 3.0).abs() < 1e-9 && (sy + 4.0).abs() < 1e-9,
            "({sx}, {sy})"
        );
    }

    #[test]
    fn project_status_remaps_offending_into_the_projection() {
        let basis = plane();
        let (s3, a) = embed_point(&Sketch3::new(), basis, Point2::from_mm(0.0, 0.0));
        let (s3, b) = embed_point(&s3, basis, Point2::from_mm(5.0, 0.0));
        let (s3, _) = s3.with_relation(Sketch3Relation::Fix(a));
        let (s3, _) = s3.with_relation(Sketch3Relation::Distance {
            a,
            b,
            value: Length::new::<millimeter>(5.0),
            reference: false,
        });
        let (s3, _) = s3.with_relation(Sketch3Relation::Distance {
            a,
            b,
            value: Length::new::<millimeter>(7.0),
            reference: false,
        });
        let report = s3.status();
        assert_eq!(report.status(), bone_types::SketchStatus::OverDefined);
        let remapped = project_status(&s3, basis, &report);
        assert_eq!(remapped.status(), report.status(), "status is preserved");
        assert!(
            !remapped.offending().is_empty(),
            "the conflict survives the id remap"
        );
        let view = project_sketch(&s3, basis);
        remapped.offending().iter().for_each(|item| {
            let present = match *item {
                SketchItemId::Entity(id) => view.entities().contains_key(id),
                SketchItemId::Relation(id) => view.relations().contains_key(id),
                SketchItemId::Dimension(id) => view.dimensions().contains_key(id),
            };
            assert!(
                present,
                "offending item resolves in the projection: {item:?}"
            );
        });
    }

    #[test]
    fn map_relation_routes_point_and_circle_to_coincident() {
        let basis = plane();
        let (s3, center) = embed_point(&Sketch3::new(), basis, Point2::from_mm(0.0, 0.0));
        let (s3, circle) = s3.with_entity(Sketch3Entity::circle(
            center,
            Length::new::<millimeter>(4.0),
            basis.normal(),
            false,
        ));
        let (s3, free) = embed_point(&s3, basis, Point2::from_mm(8.0, 0.0));
        let view = project(&s3, basis);
        let map: HashMap<SketchEntityId, SketchEntityId> = view
            .sketch()
            .entity_order()
            .iter()
            .filter_map(|id2| view.to_3d(*id2).map(|id3| (*id2, id3)))
            .collect();
        let (Some(free2), Some(circle2)) = (view.to_2d(free), view.to_2d(circle)) else {
            panic!("the free point and the circle both project");
        };
        let mapped = map_relation(SketchRelation::Coincident(free2, circle2), &map, &s3);
        assert_eq!(
            mapped,
            Some(Sketch3Relation::Coincident(free, circle)),
            "a point-and-circle coincidence lands a point-on-curve relation",
        );
    }

    #[test]
    fn project_emits_a_circle_for_a_3d_circle() {
        let basis = plane();
        let (s3, center) = embed_point(&Sketch3::new(), basis, Point2::from_mm(1.0, 2.0));
        let (s3, circle3) = s3.with_entity(Sketch3Entity::circle(
            center,
            Length::new::<millimeter>(7.0),
            basis.normal(),
            false,
        ));
        let view = project(&s3, basis);
        let Some(id2) = view.to_2d(circle3) else {
            panic!("the circle maps into the plane view");
        };
        let Some(SketchEntity::Circle(c)) = view.sketch().entities().get(id2) else {
            panic!("the projected entity is a circle");
        };
        assert!((c.radius().get::<millimeter>() - 7.0).abs() < 1e-9);
        assert_eq!(view.to_3d(id2), Some(circle3), "the id map round-trips");
    }

    #[test]
    fn embed_point_lands_on_the_plane() {
        let basis = plane();
        let (s3, id) = embed_point(&Sketch3::new(), basis, Point2::from_mm(2.0, 6.0));
        let Some(position) = s3.point_position(id) else {
            panic!("placed point resolves");
        };
        let (x, y, z) = position.coords_mm();
        assert!((x - 2.0).abs() < 1e-9 && (y - 6.0).abs() < 1e-9 && (z - 5.0).abs() < 1e-9);
    }

    #[test]
    fn reference_dimension_projects_to_a_driven_overlay_dimension() {
        let basis = plane();
        let (s3, a) = embed_point(&Sketch3::new(), basis, Point2::from_mm(0.0, 0.0));
        let (s3, b) = embed_point(&s3, basis, Point2::from_mm(10.0, 0.0));
        let (s3, _line) = embed_line(&s3, a, b, false);
        let distance = |reference| Sketch3Relation::Distance {
            a,
            b,
            value: Length::new::<millimeter>(10.0),
            reference,
        };
        let overlay_kind = |sketch: &Sketch3| {
            project(sketch, basis)
                .sketch()
                .dimensions()
                .iter()
                .map(|(_, dim)| dim.kind())
                .next()
        };

        let driving = s3.with_relation(distance(false)).0;
        let reference = s3.with_relation(distance(true)).0;
        assert_eq!(overlay_kind(&driving), Some(DimensionKind::Driving));
        assert_eq!(
            overlay_kind(&reference),
            Some(DimensionKind::Driven),
            "a reference 3d dimension renders as a driven overlay dimension",
        );
    }

    #[test]
    fn driven_overlay_dimension_maps_back_to_a_reference_relation() {
        let a = SketchEntityId::default();
        let b = SketchEntityId::default();
        let map = HashMap::from([(a, a), (b, b)]);
        let driven = SketchDimension::Linear {
            a,
            b,
            value: Length::new::<millimeter>(4.0),
            kind: DimensionKind::Driven,
        };
        let Some(relation) = map_dimension(driven, &map) else {
            panic!("a linear dimension maps to a distance relation");
        };
        assert!(
            relation.is_reference(),
            "a driven overlay dimension carries the reference flag into the 3d relation",
        );
    }
}
