use bone_kernel::{BrepEdge, BrepFace, BrepSolid, BrepVertex};
use bone_types::{
    Aabb3, BodyOrdinal, BrepEdgeId, BrepFaceId, BrepVertexId, EdgeFingerprint, EdgeLabel, EdgeRef,
    EdgeRole, EntityRef, FaceFingerprint, FaceLabel, FaceRef, FeatureId, MatchScore, Point3,
    Resolution, SketchPlaneBasis, VertexFingerprint, VertexLabel, VertexRef,
};

use crate::recompute::{EvaluatedModel, RebuildPass};

const ACCEPT_THRESHOLD: f64 = 0.5;
const SEPARATION_MARGIN: f64 = 1.0e-6;

#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub struct ResolvedFace {
    body: FeatureId,
    ordinal: BodyOrdinal,
    face: BrepFaceId,
    built_at: RebuildPass,
}

impl ResolvedFace {
    #[must_use]
    pub const fn body(self) -> FeatureId {
        self.body
    }

    #[must_use]
    pub const fn ordinal(self) -> BodyOrdinal {
        self.ordinal
    }

    #[must_use]
    pub const fn face(self) -> BrepFaceId {
        self.face
    }
}

#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub struct ResolvedEdge {
    body: FeatureId,
    ordinal: BodyOrdinal,
    edge: BrepEdgeId,
    built_at: RebuildPass,
}

impl ResolvedEdge {
    #[must_use]
    pub const fn body(self) -> FeatureId {
        self.body
    }

    #[must_use]
    pub const fn ordinal(self) -> BodyOrdinal {
        self.ordinal
    }

    #[must_use]
    pub const fn edge(self) -> BrepEdgeId {
        self.edge
    }
}

#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub struct ResolvedVertex {
    body: FeatureId,
    ordinal: BodyOrdinal,
    vertex: BrepVertexId,
    built_at: RebuildPass,
}

impl ResolvedVertex {
    #[must_use]
    pub const fn body(self) -> FeatureId {
        self.body
    }

    #[must_use]
    pub const fn ordinal(self) -> BodyOrdinal {
        self.ordinal
    }

    #[must_use]
    pub const fn vertex(self) -> BrepVertexId {
        self.vertex
    }
}

#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub enum ResolvedEntity {
    Face(ResolvedFace),
    Edge(ResolvedEdge),
    Vertex(ResolvedVertex),
}

#[must_use]
pub fn resolve(model: &EvaluatedModel, reference: EntityRef) -> Resolution<ResolvedEntity> {
    match reference {
        EntityRef::Face(face) => resolve_face_ref(model, face).map(ResolvedEntity::Face),
        EntityRef::Edge(edge) => resolve_edge_ref(model, edge).map(ResolvedEntity::Edge),
        EntityRef::Vertex(vertex) => resolve_vertex_ref(model, vertex).map(ResolvedEntity::Vertex),
    }
}

#[must_use]
pub fn resolve_face(
    model: &EvaluatedModel,
    label: FaceLabel,
    fingerprint: FaceFingerprint,
) -> Resolution<ResolvedFace> {
    resolve_face_ref(model, FaceRef::new(label, fingerprint))
}

fn resolve_face_ref(model: &EvaluatedModel, reference: FaceRef) -> Resolution<ResolvedFace> {
    match lookup_across_bodies(model, reference.ordinal, |solid| {
        face_matches(solid, reference.label)
    }) {
        Lookup::Unique(found) => Resolution::Resolved(ResolvedFace {
            body: found.body,
            ordinal: found.ordinal,
            face: found.entity.id(),
            built_at: found.built_at,
        }),
        Lookup::Ambiguous | Lookup::Absent => Resolution::Dangling {
            last_known: EntityRef::Face(reference),
        },
    }
}

fn face_matches(solid: &BrepSolid, label: FaceLabel) -> impl Iterator<Item = &BrepFace> {
    solid.iter_faces().filter(move |face| face.label() == label)
}

fn edge_matches(solid: &BrepSolid, label: EdgeLabel) -> impl Iterator<Item = &BrepEdge> {
    solid.iter_edges().filter(move |edge| edge.label() == label)
}

fn vertex_matches(solid: &BrepSolid, label: VertexLabel) -> impl Iterator<Item = &BrepVertex> {
    solid
        .iter_vertices()
        .filter(move |vertex| vertex.label() == label)
}

#[must_use]
pub(crate) fn unique_face(solid: &BrepSolid, label: FaceLabel) -> Option<&BrepFace> {
    let mut matches = face_matches(solid, label);
    let first = matches.next()?;
    matches.next().is_none().then_some(first)
}

#[must_use]
pub fn resolve_edge(
    model: &EvaluatedModel,
    label: EdgeLabel,
    fingerprint: EdgeFingerprint,
) -> Resolution<ResolvedEdge> {
    resolve_edge_ref(model, EdgeRef::new(label, fingerprint))
}

fn resolve_edge_ref(model: &EvaluatedModel, reference: EdgeRef) -> Resolution<ResolvedEdge> {
    match lookup_across_bodies(model, reference.ordinal, |solid| {
        edge_matches(solid, reference.label)
    }) {
        Lookup::Unique(found) => {
            return Resolution::Resolved(ResolvedEdge {
                body: found.body,
                ordinal: found.ordinal,
                edge: found.entity.id(),
                built_at: found.built_at,
            });
        }
        Lookup::Ambiguous => {
            return Resolution::Dangling {
                last_known: EntityRef::Edge(reference),
            };
        }
        Lookup::Absent => {}
    }
    if let EdgeRole::Intersection { a, b } = reference.label.role
        && let Some(derived) = derive_edge_from_faces(model, a, b)
    {
        return Resolution::Resolved(derived);
    }
    if let Some((edge, score)) = nearest_edge(model, reference.label, reference.fingerprint) {
        return Resolution::Repaired { id: edge, score };
    }
    Resolution::Dangling {
        last_known: EntityRef::Edge(reference),
    }
}

#[must_use]
pub fn resolve_vertex(
    model: &EvaluatedModel,
    label: VertexLabel,
    fingerprint: VertexFingerprint,
) -> Resolution<ResolvedVertex> {
    resolve_vertex_ref(model, VertexRef::new(label, fingerprint))
}

fn resolve_vertex_ref(model: &EvaluatedModel, reference: VertexRef) -> Resolution<ResolvedVertex> {
    match lookup_across_bodies(model, reference.ordinal, |solid| {
        vertex_matches(solid, reference.label)
    }) {
        Lookup::Unique(found) => {
            return Resolution::Resolved(ResolvedVertex {
                body: found.body,
                ordinal: found.ordinal,
                vertex: found.entity.id(),
                built_at: found.built_at,
            });
        }
        Lookup::Ambiguous => {
            return Resolution::Dangling {
                last_known: EntityRef::Vertex(reference),
            };
        }
        Lookup::Absent => {}
    }
    if let Some((vertex, score)) = nearest_vertex(model, reference.label, reference.fingerprint) {
        return Resolution::Repaired { id: vertex, score };
    }
    Resolution::Dangling {
        last_known: EntityRef::Vertex(reference),
    }
}

struct BodyMatch<'model, T> {
    body: FeatureId,
    ordinal: BodyOrdinal,
    built_at: RebuildPass,
    entity: &'model T,
}

enum Lookup<T> {
    Unique(T),
    Ambiguous,
    Absent,
}

enum BodyHit<'model, T> {
    One(BodyMatch<'model, T>),
    Many,
    None,
}

fn lookup_across_bodies<'model, T, I>(
    model: &'model EvaluatedModel,
    wanted: BodyOrdinal,
    candidates: impl Fn(&'model BrepSolid) -> I,
) -> Lookup<BodyMatch<'model, T>>
where
    T: 'model,
    I: Iterator<Item = &'model T>,
{
    let hits: Vec<BodyHit<'model, T>> = model
        .bodies()
        .map(|(body, ordinal, solid)| match model.built_at(body) {
            Some(built_at) => {
                let mut matches = candidates(solid);
                match (matches.next(), matches.next()) {
                    (Some(entity), None) => BodyHit::One(BodyMatch {
                        body,
                        ordinal,
                        built_at,
                        entity,
                    }),
                    (Some(_), Some(_)) => BodyHit::Many,
                    (None, _) => BodyHit::None,
                }
            }
            None => BodyHit::None,
        })
        .collect();
    if hits.iter().any(|hit| matches!(hit, BodyHit::Many)) {
        return Lookup::Ambiguous;
    }
    let candidates: Vec<BodyMatch<'model, T>> = hits
        .into_iter()
        .filter_map(|hit| match hit {
            BodyHit::One(found) => Some(found),
            BodyHit::Many | BodyHit::None => None,
        })
        .collect();
    disambiguate_by_ordinal(candidates, wanted)
}

fn disambiguate_by_ordinal<T>(
    mut candidates: Vec<BodyMatch<'_, T>>,
    wanted: BodyOrdinal,
) -> Lookup<BodyMatch<'_, T>> {
    match candidates.len() {
        0 => Lookup::Absent,
        1 => candidates.pop().map_or(Lookup::Absent, Lookup::Unique),
        _ => {
            candidates.retain(|found| found.ordinal == wanted);
            match candidates.len() {
                1 => candidates.pop().map_or(Lookup::Absent, Lookup::Unique),
                _ => Lookup::Ambiguous,
            }
        }
    }
}

fn derive_edge_from_faces(
    model: &EvaluatedModel,
    a: FaceLabel,
    b: FaceLabel,
) -> Option<ResolvedEdge> {
    let mut derived = model.bodies().filter_map(|(body, ordinal, solid)| {
        let built_at = model.built_at(body)?;
        let face_a = unique_face(solid, a)?.id();
        let face_b = unique_face(solid, b)?.id();
        let mut between = solid.edges_between(face_a, face_b);
        let first = between.next()?;
        between.next().is_none().then_some(ResolvedEdge {
            body,
            ordinal,
            edge: first.id(),
            built_at,
        })
    });
    let first = derived.next()?;
    derived.next().is_none().then_some(first)
}

fn nearest_edge(
    model: &EvaluatedModel,
    label: EdgeLabel,
    want: EdgeFingerprint,
) -> Option<(ResolvedEdge, MatchScore)> {
    let graded = model.bodies().flat_map(|(body, ordinal, solid)| {
        let built_at = model.built_at(body);
        let candidates: Vec<(BrepEdgeId, EdgeFingerprint)> = solid
            .iter_edges()
            .filter(|edge| same_edge_kind(edge.label(), label))
            .filter_map(|edge| Some((edge.id(), solid.edge_fingerprint(edge.id())?)))
            .collect();
        let scale = local_scale(
            candidates.iter().map(|(_, candidate)| candidate.sample),
            solid,
        );
        candidates
            .into_iter()
            .filter_map(move |(edge, candidate)| {
                Some((
                    ResolvedEdge {
                        body,
                        ordinal,
                        edge,
                        built_at: built_at?,
                    },
                    edge_proximity(candidate, want, scale?),
                ))
            })
            .collect::<Vec<_>>()
    });
    accept_best(graded)
}

fn nearest_vertex(
    model: &EvaluatedModel,
    label: VertexLabel,
    want: VertexFingerprint,
) -> Option<(ResolvedVertex, MatchScore)> {
    let graded = model.bodies().flat_map(|(body, ordinal, solid)| {
        let built_at = model.built_at(body);
        let candidates: Vec<(BrepVertexId, VertexFingerprint)> = solid
            .iter_vertices()
            .filter(|vertex| same_vertex_kind(vertex.label(), label))
            .filter_map(|vertex| Some((vertex.id(), solid.vertex_fingerprint(vertex.id())?)))
            .collect();
        let scale = local_scale(
            candidates.iter().map(|(_, candidate)| candidate.point),
            solid,
        );
        candidates
            .into_iter()
            .filter_map(move |(vertex, candidate)| {
                Some((
                    ResolvedVertex {
                        body,
                        ordinal,
                        vertex,
                        built_at: built_at?,
                    },
                    vertex_proximity(candidate, want, scale?),
                ))
            })
            .collect::<Vec<_>>()
    });
    accept_best(graded)
}

fn accept_best<Id>(graded: impl Iterator<Item = (Id, f64)>) -> Option<(Id, MatchScore)> {
    let mut ranked: Vec<(Id, f64)> = graded.collect();
    ranked.sort_by(|(_, left), (_, right)| right.total_cmp(left));
    let best = ranked.first().map(|&(_, score)| score)?;
    let unambiguous = ranked
        .get(1)
        .is_none_or(|&(_, runner_up)| best - runner_up >= SEPARATION_MARGIN);
    let (id, score) = ranked.into_iter().next()?;
    (score >= ACCEPT_THRESHOLD && unambiguous)
        .then(|| MatchScore::new(score).ok())
        .flatten()
        .map(|score| (id, score))
}

fn local_scale(samples: impl Iterator<Item = Point3>, solid: &BrepSolid) -> Option<f64> {
    positive_span(Aabb3::from_points(samples)).or_else(|| positive_span(solid.bounding_box()))
}

fn positive_span(bounds: Option<Aabb3>) -> Option<f64> {
    let bounds = bounds?;
    let span = (bounds.max() - bounds.min()).norm_mm();
    (span > 0.0).then_some(span)
}

fn same_edge_kind(candidate: EdgeLabel, want: EdgeLabel) -> bool {
    candidate.feature == want.feature
        && core::mem::discriminant(&candidate.role) == core::mem::discriminant(&want.role)
}

fn same_vertex_kind(candidate: VertexLabel, want: VertexLabel) -> bool {
    candidate.feature == want.feature
        && core::mem::discriminant(&candidate.role) == core::mem::discriminant(&want.role)
}

fn edge_proximity(candidate: EdgeFingerprint, want: EdgeFingerprint, scale: f64) -> f64 {
    let distance = (candidate.sample - want.sample).norm_mm();
    let alignment = candidate.direction.dot(want.direction).abs();
    (falloff(distance, scale) * alignment).clamp(0.0, 1.0)
}

fn vertex_proximity(candidate: VertexFingerprint, want: VertexFingerprint, scale: f64) -> f64 {
    let distance = (candidate.point - want.point).norm_mm();
    falloff(distance, scale)
}

fn falloff(distance: f64, scale: f64) -> f64 {
    (1.0 - distance / scale).clamp(0.0, 1.0)
}

impl EvaluatedModel {
    #[must_use]
    pub fn face(&self, resolved: ResolvedFace) -> Option<&BrepFace> {
        (self.built_at(resolved.body) == Some(resolved.built_at))
            .then(|| self.body_at(resolved.body, resolved.ordinal))
            .flatten()
            .and_then(|solid| solid.iter_faces().find(|face| face.id() == resolved.face))
    }

    #[must_use]
    pub fn face_ref(&self, resolved: ResolvedFace) -> Option<FaceRef> {
        let label = self.face(resolved)?.label();
        let fingerprint = self
            .body_at(resolved.body, resolved.ordinal)?
            .face_fingerprint(resolved.face)?;
        Some(FaceRef::new(label, fingerprint).at(resolved.ordinal))
    }

    #[must_use]
    pub fn edge(&self, resolved: ResolvedEdge) -> Option<&BrepEdge> {
        (self.built_at(resolved.body) == Some(resolved.built_at))
            .then(|| self.body_at(resolved.body, resolved.ordinal))
            .flatten()
            .and_then(|solid| solid.iter_edges().find(|edge| edge.id() == resolved.edge))
    }

    #[must_use]
    pub fn vertex(&self, resolved: ResolvedVertex) -> Option<&BrepVertex> {
        (self.built_at(resolved.body) == Some(resolved.built_at))
            .then(|| self.body_at(resolved.body, resolved.ordinal))
            .flatten()
            .and_then(|solid| {
                solid
                    .iter_vertices()
                    .find(|vertex| vertex.id() == resolved.vertex)
            })
    }

    #[must_use]
    pub fn edge_ref_at(
        &self,
        body: FeatureId,
        ordinal: BodyOrdinal,
        edge: BrepEdgeId,
    ) -> Option<EntityRef> {
        let solid = self.body_at(body, ordinal)?;
        let label = solid
            .iter_edges()
            .find(|candidate| candidate.id() == edge)?
            .label();
        let fingerprint = solid.edge_fingerprint(edge)?;
        Some(EntityRef::Edge(
            EdgeRef::new(label, fingerprint).at(ordinal),
        ))
    }

    #[must_use]
    pub fn vertex_ref_at(
        &self,
        body: FeatureId,
        ordinal: BodyOrdinal,
        vertex: BrepVertexId,
    ) -> Option<EntityRef> {
        let solid = self.body_at(body, ordinal)?;
        let label = solid
            .iter_vertices()
            .find(|candidate| candidate.id() == vertex)?
            .label();
        let fingerprint = solid.vertex_fingerprint(vertex)?;
        Some(EntityRef::Vertex(
            VertexRef::new(label, fingerprint).at(ordinal),
        ))
    }

    #[must_use]
    pub fn face_ref_at(
        &self,
        body: FeatureId,
        ordinal: BodyOrdinal,
        face: BrepFaceId,
    ) -> Option<FaceRef> {
        let solid = self.body_at(body, ordinal)?;
        let label = solid
            .iter_faces()
            .find(|candidate| candidate.id() == face)?
            .label();
        let fingerprint = solid.face_fingerprint(face)?;
        Some(FaceRef::new(label, fingerprint).at(ordinal))
    }

    #[must_use]
    pub fn face_ref_any(&self, face: BrepFaceId) -> Option<FaceRef> {
        self.bodies()
            .find_map(|(body, ordinal, _)| self.face_ref_at(body, ordinal, face))
    }

    #[must_use]
    pub fn face_for_sketch(&self, face: BrepFaceId) -> Option<(FaceRef, SketchPlaneBasis)> {
        self.bodies().find_map(|(body, ordinal, solid)| {
            let basis = solid.face_plane_basis(face)?;
            let face_ref = self.face_ref_at(body, ordinal, face)?;
            Some((face_ref, basis))
        })
    }
}

#[cfg(test)]
mod tests {
    use std::collections::BTreeSet;

    use bone_kernel::{BrepEdge, BrepFace, BrepVertex};
    use bone_types::{
        BodyOrdinal, DocumentId, EdgeFingerprint, EdgeLabel, EdgeRef, EdgeRole, EntityRef,
        FaceFingerprint, FaceLabel, FaceRef, FaceRole, FeatureId, Length, LoopIndex, MatchScore,
        Plane3, Point3, Resolution, RollbackMarker, UnitVec3, millimeter,
    };
    use proptest::prelude::*;

    use super::{
        ACCEPT_THRESHOLD, ResolvedEntity, ResolvedFace, accept_best, edge_proximity, resolve,
        resolve_edge, resolve_face, resolve_vertex,
    };
    use crate::document::Document;
    use crate::recompute::{EvaluatedModel, RecomputeScope};
    use crate::sketch3::Sketch3Relation;
    use crate::test_support::{
        blind_extrude, chain_handles, circle_with_radius_dim, end_cap_ref, full_model, push_box,
        push_chain, push_face_bound_box_merge, side_face_label,
    };

    fn placeholder_fingerprint() -> FaceFingerprint {
        FaceFingerprint {
            plane: Plane3::new_unchecked(Point3::origin(), UnitVec3::x_axis(), UnitVec3::y_axis()),
            centroid: Point3::origin(),
        }
    }

    fn placeholder_edge_fingerprint() -> EdgeFingerprint {
        EdgeFingerprint {
            sample: Point3::origin(),
            direction: UnitVec3::x_axis(),
        }
    }

    fn face_labels(model: &EvaluatedModel, body: FeatureId) -> Vec<FaceLabel> {
        let Some(solid) = model.body(body) else {
            panic!("body is built");
        };
        solid.iter_faces().map(BrepFace::label).collect()
    }

    fn resolved_label(model: &EvaluatedModel, resolved: ResolvedFace) -> FaceLabel {
        let Some(face) = model.face(resolved) else {
            panic!("a resolved face is present in its model");
        };
        face.label()
    }

    #[test]
    fn accept_best_refuses_indistinguishable_candidates() {
        assert!(
            accept_best([(1u32, 0.9), (2u32, 0.9)].into_iter()).is_none(),
            "two candidates of equal grade are indistinguishable and must not be guessed",
        );
        let Some((id, score)) = accept_best([(1u32, 0.9), (2u32, 0.2)].into_iter()) else {
            panic!("a clear leader above the threshold repairs");
        };
        assert_eq!(id, 1, "the higher-grade candidate wins");
        assert!(score.value() >= ACCEPT_THRESHOLD);
        assert!(
            accept_best([(1u32, 0.3)].into_iter()).is_none(),
            "a lone candidate below the threshold dangles",
        );
    }

    #[test]
    fn edge_proximity_stays_inside_the_match_score_range() {
        let here = EdgeFingerprint {
            sample: Point3::origin(),
            direction: UnitVec3::x_axis(),
        };
        assert!(
            MatchScore::new(edge_proximity(here, here, 1.0)).is_ok(),
            "a colocated, aligned candidate grades at the top of the range, never above it",
        );
    }

    #[test]
    fn face_ref_at_round_trips_a_picked_face() {
        let mut document = Document::new(DocumentId::default(), "doc".to_owned());
        let chain = push_chain(&mut document, "Body", 6.0, 4.0);
        let model = full_model(&document);
        let Some(label) = face_labels(&model, chain.extrude).into_iter().next() else {
            panic!("the body has faces");
        };
        let Resolution::Resolved(resolved) = resolve_face(&model, label, placeholder_fingerprint())
        else {
            panic!("the label resolves on its own model");
        };
        let Some(face_ref) =
            model.face_ref_at(resolved.body(), resolved.ordinal(), resolved.face())
        else {
            panic!("a picked face yields a face reference");
        };
        let Resolution::Resolved(ResolvedEntity::Face(again)) =
            resolve(&model, face_ref.entity_ref())
        else {
            panic!("the reattach reference resolves back to a face");
        };
        assert_eq!(
            again.face(),
            resolved.face(),
            "reattach targets the same face"
        );
        assert_eq!(
            model
                .face_ref_any(resolved.face())
                .map(|reference| reference.label),
            Some(face_ref.label),
        );
    }

    #[test]
    fn face_for_sketch_yields_a_basis_for_a_planar_cap() {
        let mut document = Document::new(DocumentId::default(), "doc".to_owned());
        let chain = push_chain(&mut document, "Body", 6.0, 4.0);
        let model = full_model(&document);
        let Some(cap) = face_labels(&model, chain.extrude)
            .into_iter()
            .find(|label| matches!(label.role, FaceRole::EndCap))
        else {
            panic!("the cylinder has a planar cap");
        };
        let Resolution::Resolved(resolved) = resolve_face(&model, cap, placeholder_fingerprint())
        else {
            panic!("the cap resolves on its own model");
        };
        assert!(
            model.face_for_sketch(resolved.face()).is_some(),
            "a planar cap yields a face reference and a sketch basis",
        );
    }

    proptest! {
        #[test]
        fn a_surviving_ref_resolves_and_a_deleted_target_dangles(
            kept_radius in 2.0f64..18.0,
            doomed_radius in 2.0f64..18.0,
        ) {
            let mut document = Document::new(DocumentId::default(), "doc".to_owned());
            let kept = push_chain(&mut document, "Keep", kept_radius, 4.0);
            let doomed = push_chain(&mut document, "Drop", doomed_radius, 4.0);
            let mut model = full_model(&document);

            let Some(survivor) = face_labels(&model, kept.extrude).into_iter().next() else {
                return Err(TestCaseError::fail("the kept body has faces"));
            };
            let Some(lost) = face_labels(&model, doomed.extrude).into_iter().next() else {
                return Err(TestCaseError::fail("the doomed body has faces"));
            };

            document.remove_extrude(doomed.extrude_id);
            model.recompute(
                &document,
                &BTreeSet::new(),
                RollbackMarker::AtEnd,
                RecomputeScope::Full,
            );

            let Resolution::Resolved(resolved) =
                resolve_face(&model, survivor, placeholder_fingerprint())
            else {
                return Err(TestCaseError::fail(format!(
                    "{survivor} must survive a sibling deletion"
                )));
            };
            prop_assert_eq!(resolved.body(), kept.extrude);
            prop_assert_eq!(resolved_label(&model, resolved), survivor);

            prop_assert_eq!(
                resolve_face(&model, lost, placeholder_fingerprint()),
                Resolution::Dangling {
                    last_known: EntityRef::Face(FaceRef::new(lost, placeholder_fingerprint())),
                },
                "a deleted target dangles with its last-known reference",
            );
        }
    }

    #[test]
    fn a_no_match_label_never_binds_to_a_nearest_face() {
        let mut document = Document::new(DocumentId::default(), "doc".to_owned());
        let sketch_id = document.allocate_sketch();
        let (sketch, circle, _dim) = circle_with_radius_dim(5.0);
        document.insert_sketch(sketch_id, "Sketch1".to_owned(), sketch);
        let extrude_id = document.commit_extrude(blind_extrude(sketch_id, 4.0));
        let chain = chain_handles(&document, sketch_id, extrude_id);
        let model = full_model(&document);

        let absent = FaceLabel {
            feature: chain.extrude,
            role: FaceRole::Side {
                loop_index: LoopIndex::new(u16::MAX),
                from: circle,
            },
        };
        let resolution = resolve_face(&model, absent, placeholder_fingerprint());
        assert!(resolution.is_dangling());
        assert_eq!(resolution.id(), None, "no nearest-guess id is returned");
    }

    #[test]
    fn resolve_routes_face_edge_and_vertex_references() {
        let mut document = Document::new(DocumentId::default(), "doc".to_owned());
        let body = push_box(&mut document, "Body", 4.0, 3.0);
        let model = full_model(&document);
        let Some(solid) = model.body(body.extrude) else {
            panic!("the box builds");
        };

        let Some(face) = solid.iter_faces().next() else {
            panic!("the box has faces");
        };
        let Some(face_ref) = model.face_ref_at(body.extrude, BodyOrdinal::origin(), face.id())
        else {
            panic!("a picked face yields a reference");
        };
        assert!(matches!(
            resolve(&model, face_ref.entity_ref()),
            Resolution::Resolved(ResolvedEntity::Face(_)),
        ));

        let Some(edge) = solid.iter_edges().next() else {
            panic!("the box has edges");
        };
        let Some(edge_ref) = model.edge_ref_at(body.extrude, BodyOrdinal::origin(), edge.id())
        else {
            panic!("a picked edge yields a reference");
        };
        let Resolution::Resolved(ResolvedEntity::Edge(resolved_edge)) = resolve(&model, edge_ref)
        else {
            panic!("an edge reference resolves through the dispatcher");
        };
        assert_eq!(resolved_edge.body(), body.extrude);

        let Some(vertex) = solid.iter_vertices().next() else {
            panic!("the box has vertices");
        };
        let Some(vertex_ref) =
            model.vertex_ref_at(body.extrude, BodyOrdinal::origin(), vertex.id())
        else {
            panic!("a picked vertex yields a reference");
        };
        let Resolution::Resolved(ResolvedEntity::Vertex(resolved_vertex)) =
            resolve(&model, vertex_ref)
        else {
            panic!("a vertex reference resolves through the dispatcher");
        };
        assert_eq!(resolved_vertex.body(), body.extrude);
    }

    #[test]
    fn an_absent_edge_label_dangles_without_a_nearby_candidate() {
        let mut document = Document::new(DocumentId::default(), "doc".to_owned());
        let sketch_id = document.allocate_sketch();
        let (sketch, _circle, _dim) = circle_with_radius_dim(5.0);
        document.insert_sketch(sketch_id, "Sketch1".to_owned(), sketch);
        let extrude_id = document.commit_extrude(blind_extrude(sketch_id, 4.0));
        let chain = chain_handles(&document, sketch_id, extrude_id);
        let model = full_model(&document);

        let absent = EdgeLabel {
            feature: chain.extrude,
            role: EdgeRole::Intersection {
                a: FaceLabel {
                    feature: chain.extrude,
                    role: FaceRole::StartCap,
                },
                b: FaceLabel {
                    feature: chain.extrude,
                    role: FaceRole::EndCap,
                },
            },
        };
        let resolution = resolve_edge(&model, absent, placeholder_edge_fingerprint());
        assert!(
            resolution.is_dangling(),
            "two real faces that share no edge cannot fabricate an intersection edge",
        );
        assert_eq!(resolution.id(), None, "no nearest-guess id is returned");
    }

    #[test]
    fn an_edge_ref_survives_a_dimension_edit() {
        let mut document = Document::new(DocumentId::default(), "doc".to_owned());
        let sketch_id = document.allocate_sketch();
        let (sketch, circle, radius_dim) = circle_with_radius_dim(5.0);
        document.insert_sketch(sketch_id, "Sketch1".to_owned(), sketch);
        let extrude_id = document.commit_extrude(blind_extrude(sketch_id, 4.0));
        let chain = chain_handles(&document, sketch_id, extrude_id);
        let mut model = full_model(&document);

        let Some(edge_id) = model
            .body(chain.extrude)
            .and_then(|solid| solid.iter_edges().next().map(BrepEdge::id))
        else {
            panic!("the cylinder has edges");
        };
        let Some(reference) = model.edge_ref_at(chain.extrude, BodyOrdinal::origin(), edge_id)
        else {
            panic!("a picked edge yields a reference");
        };
        let EntityRef::Edge(edge) = reference else {
            panic!("the reference is an edge reference");
        };
        let label = edge.label;

        let Some(original) = document.sketch3(sketch_id).cloned() else {
            panic!("the sketch is present");
        };
        let edited = original
            .without_relation(radius_dim)
            .with_relation(Sketch3Relation::Radius {
                target: circle,
                value: Length::new::<millimeter>(9.0),
                reference: false,
            })
            .0;
        document.replace_sketch(sketch_id, edited);
        model.recompute(
            &document,
            &BTreeSet::new(),
            RollbackMarker::AtEnd,
            RecomputeScope::Edited(chain.sketch),
        );

        let Resolution::Resolved(ResolvedEntity::Edge(resolved)) = resolve(&model, reference)
        else {
            panic!("a stable edge label resolves exactly across a dimension edit");
        };
        assert_eq!(
            model.edge(resolved).map(BrepEdge::label),
            Some(label),
            "the reference resolves to the identically-labeled edge",
        );
    }

    #[test]
    fn a_boolean_intersection_edge_resolves_in_the_consumer_body() {
        let mut document = Document::new(DocumentId::default(), "doc".to_owned());
        let base = push_box(&mut document, "Base", 4.0, 2.0);
        let probe = full_model(&document);
        let cap = end_cap_ref(&probe, base.extrude);
        let boss = push_face_bound_box_merge(&mut document, "Boss", cap, 4.0, 3.0);
        let model = full_model(&document);

        let Some(solid) = model.body(boss.extrude) else {
            panic!("the boss node owns the union");
        };
        let Some(seam) = solid
            .iter_edges()
            .find(|edge| matches!(edge.label().role, EdgeRole::Intersection { .. }))
        else {
            panic!("the union carries intersection edges");
        };
        let Some(reference) = model.edge_ref_at(boss.extrude, BodyOrdinal::origin(), seam.id())
        else {
            panic!("the seam edge yields a reference");
        };
        let Resolution::Resolved(ResolvedEntity::Edge(resolved)) = resolve(&model, reference)
        else {
            panic!("the derived seam edge resolves across the merge boundary");
        };
        assert_eq!(
            resolved.body(),
            boss.extrude,
            "the seam lives in the consumer body, not in the feature that names it",
        );
        assert!(
            matches!(
                model
                    .edge(resolved)
                    .map(BrepEdge::label)
                    .map(|label| label.role),
                Some(EdgeRole::Intersection { .. }),
            ),
            "the resolved entity is the intersection edge",
        );

        let (EntityRef::Edge(edge), EdgeRole::Intersection { a, b }) =
            (reference, seam.label().role)
        else {
            panic!("the seam reference carries an intersection role");
        };
        let swapped = EdgeLabel {
            feature: edge.label.feature,
            role: EdgeRole::Intersection { a: b, b: a },
        };
        let Resolution::Resolved(ResolvedEntity::Edge(via_faces)) = resolve(
            &model,
            EntityRef::Edge(EdgeRef::new(swapped, edge.fingerprint)),
        ) else {
            panic!("a missed exact label re-derives the edge by intersecting its two faces");
        };
        assert_eq!(
            via_faces.edge(),
            resolved.edge(),
            "the adjacency derivation lands the same seam edge as the exact match",
        );
    }

    #[test]
    fn a_consumed_base_face_resolves_in_the_live_merged_body() {
        let mut document = Document::new(DocumentId::default(), "doc".to_owned());
        let base = push_box(&mut document, "Base", 4.0, 2.0);
        let probe = full_model(&document);
        let base_side = side_face_label(&probe, base.extrude);
        let cap = end_cap_ref(&probe, base.extrude);
        let boss = push_face_bound_box_merge(&mut document, "Boss", cap, 4.0, 3.0);
        let model = full_model(&document);

        let live: BTreeSet<FeatureId> = model.bodies().map(|(id, _, _)| id).collect();
        assert!(
            !live.contains(&base.extrude),
            "the merging boss consumes the base body",
        );
        assert!(
            live.contains(&boss.extrude),
            "the union lives in the boss body",
        );

        let Resolution::Resolved(resolved) =
            resolve_face(&model, base_side, placeholder_fingerprint())
        else {
            panic!("a base face survives the merge and resolves");
        };
        assert_eq!(
            resolved.body(),
            boss.extrude,
            "the base face resolves in the live merged body, never the consumed base arena",
        );
        assert_eq!(
            model.face(resolved).map(BrepFace::label),
            Some(base_side),
            "the resolved handle reads the base label back from the live body",
        );
    }

    #[test]
    fn a_drifted_label_repairs_to_the_nearest_same_kind_edge() {
        let mut document = Document::new(DocumentId::default(), "doc".to_owned());
        let body = push_box(&mut document, "Body", 4.0, 3.0);
        let model = full_model(&document);
        let Some(solid) = model.body(body.extrude) else {
            panic!("the box builds");
        };

        let Some(corner) = solid.iter_edges().find_map(|edge| match edge.label().role {
            EdgeRole::SideEdge { from, .. } => Some(from),
            _ => None,
        }) else {
            panic!("the box has corner side edges");
        };
        let Some(target) = solid
            .iter_edges()
            .find(|edge| matches!(edge.label().role, EdgeRole::StartCapEdge { .. }))
        else {
            panic!("the box has start-cap edges");
        };
        let Some(want) = solid.edge_fingerprint(target.id()) else {
            panic!("the target edge has a fingerprint");
        };

        let drifted = EdgeLabel {
            feature: body.extrude,
            role: EdgeRole::StartCapEdge { from: corner },
        };
        let Resolution::Repaired { id, score } = resolve_edge(&model, drifted, want) else {
            panic!("a same-kind edge sitting on the fingerprint repairs rather than dangles");
        };
        assert_eq!(
            id.edge(),
            target.id(),
            "the repair binds the colocated edge"
        );
        assert!(
            score.value() > ACCEPT_THRESHOLD,
            "the repair grade clears the acceptance threshold",
        );
    }

    #[test]
    fn a_vertex_ref_resolves_and_a_deleted_body_dangles_it() {
        let mut document = Document::new(DocumentId::default(), "doc".to_owned());
        let body = push_box(&mut document, "Body", 4.0, 3.0);
        let mut model = full_model(&document);
        let Some(vertex_id) = model
            .body(body.extrude)
            .and_then(|solid| solid.iter_vertices().next().map(BrepVertex::id))
        else {
            panic!("the box has vertices");
        };
        let Some(reference) = model.vertex_ref_at(body.extrude, BodyOrdinal::origin(), vertex_id)
        else {
            panic!("a picked vertex yields a reference");
        };
        let EntityRef::Vertex(vertex) = reference else {
            panic!("the reference is a vertex reference");
        };
        let (label, fingerprint) = (vertex.label, vertex.fingerprint);
        let Resolution::Resolved(ResolvedEntity::Vertex(resolved)) = resolve(&model, reference)
        else {
            panic!("the vertex resolves on its own model");
        };
        assert_eq!(resolved.body(), body.extrude);

        document.remove_extrude(body.extrude_id);
        model.recompute(
            &document,
            &BTreeSet::new(),
            RollbackMarker::AtEnd,
            RecomputeScope::Full,
        );
        assert!(
            resolve_vertex(&model, label, fingerprint).is_dangling(),
            "a deleted body dangles its vertex references",
        );
    }

    #[test]
    fn a_resolved_face_goes_stale_when_its_body_rebuilds() {
        let mut document = Document::new(DocumentId::default(), "doc".to_owned());
        let sketch_id = document.allocate_sketch();
        let (sketch, circle, radius_dim) = circle_with_radius_dim(5.0);
        document.insert_sketch(sketch_id, "Sketch1".to_owned(), sketch);
        let extrude_id = document.commit_extrude(blind_extrude(sketch_id, 4.0));
        let chain = chain_handles(&document, sketch_id, extrude_id);
        let mut model = full_model(&document);

        let Some(label) = face_labels(&model, chain.extrude).into_iter().next() else {
            panic!("the body has faces");
        };
        let Resolution::Resolved(stale) = resolve_face(&model, label, placeholder_fingerprint())
        else {
            panic!("the face resolves before the edit");
        };
        assert!(
            model.face(stale).is_some(),
            "a fresh handle reads back on the model that produced it",
        );

        let Some(original) = document.sketch3(sketch_id).cloned() else {
            panic!("the sketch is present");
        };
        let edited = original
            .without_relation(radius_dim)
            .with_relation(Sketch3Relation::Radius {
                target: circle,
                value: Length::new::<millimeter>(9.0),
                reference: false,
            })
            .0;
        document.replace_sketch(sketch_id, edited);
        model.recompute(
            &document,
            &BTreeSet::new(),
            RollbackMarker::AtEnd,
            RecomputeScope::Edited(chain.sketch),
        );

        assert!(
            model.face(stale).is_none(),
            "a handle from before the rebuild must not read a face out of the new arena",
        );
        assert!(
            resolve_face(&model, label, placeholder_fingerprint())
                .id()
                .is_some(),
            "re-resolving against the rebuilt model yields a usable handle",
        );
    }

    proptest! {
        #[test]
        fn a_dimension_edit_keeps_every_face_ref_resolved(
            radius in 2.0f64..18.0,
            edited in 2.0f64..18.0,
        ) {
            let mut document = Document::new(DocumentId::default(), "doc".to_owned());
            let sketch_id = document.allocate_sketch();
            let (sketch, circle, radius_dim) = circle_with_radius_dim(radius);
            document.insert_sketch(sketch_id, "Sketch1".to_owned(), sketch);
            let extrude_id = document.commit_extrude(blind_extrude(sketch_id, 4.0));
            let chain = chain_handles(&document, sketch_id, extrude_id);
            let mut model = full_model(&document);

            let labels = face_labels(&model, chain.extrude);

            let Some(original) = document.sketch3(sketch_id).cloned() else {
                prop_assert!(false, "the sketch is present");
                return Ok(());
            };
            let edited_sketch = original
                .without_relation(radius_dim)
                .with_relation(Sketch3Relation::Radius {
                    target: circle,
                    value: Length::new::<millimeter>(edited),
                    reference: false,
                })
                .0;
            document.replace_sketch(sketch_id, edited_sketch);
            model.recompute(
                &document,
                &BTreeSet::new(),
                RollbackMarker::AtEnd,
                RecomputeScope::Edited(chain.sketch),
            );

            let unresolved: Vec<FaceLabel> = labels
                .iter()
                .copied()
                .filter(|&label| {
                    match resolve_face(&model, label, placeholder_fingerprint()) {
                        Resolution::Resolved(resolved) => resolved_label(&model, resolved) != label,
                        Resolution::Repaired { .. } | Resolution::Dangling { .. } => true,
                    }
                })
                .collect();
            prop_assert!(
                unresolved.is_empty(),
                "every face ref must resolve to its identical label across a dimension edit, failed: {unresolved:?}",
            );
        }
    }
}
