use std::collections::{BTreeMap, BTreeSet};
use std::sync::Arc;

use bone_kernel::{
    BrepEdge, BrepFace, BrepSolid, BrepVertex, DatumFeature, ExtrudeFeature, RevolveAxis,
    RevolveFeature, SweepAlignment, SweepFeature,
};
use bone_types::{
    BodyId, DatumId, DocumentId, ExtrudeId, FaceRef, FeatureGeneration, FeatureId, RevolveId,
    RollbackMarker, SchemaHeader, SketchId, SuppressionState, SweepId,
};
use serde::{Deserialize, Serialize};
use slotmap::KeyData;

use crate::Sketch;
use crate::sketch3::Sketch3;

pub mod feature_tree;

pub use feature_tree::{FeatureEdge, FeatureNode, FeatureTree, PrincipalPlane};

#[derive(Clone)]
pub struct ImportedSolid {
    solid: Arc<BrepSolid>,
}

impl ImportedSolid {
    #[must_use]
    pub fn new(solid: BrepSolid) -> Self {
        Self {
            solid: Arc::new(solid),
        }
    }

    #[must_use]
    pub fn solid(&self) -> &BrepSolid {
        self.solid.as_ref()
    }
}

impl core::fmt::Debug for ImportedSolid {
    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
        f.debug_struct("ImportedSolid")
            .field("content_key", &self.solid.content_key())
            .finish()
    }
}

impl PartialEq for ImportedSolid {
    fn eq(&self, other: &Self) -> bool {
        self.solid.content_key() == other.solid.content_key()
            && self
                .solid
                .iter_faces()
                .map(BrepFace::label)
                .eq(other.solid.iter_faces().map(BrepFace::label))
            && self
                .solid
                .iter_edges()
                .map(BrepEdge::label)
                .eq(other.solid.iter_edges().map(BrepEdge::label))
            && self
                .solid
                .iter_vertices()
                .map(BrepVertex::label)
                .eq(other.solid.iter_vertices().map(BrepVertex::label))
    }
}

#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub enum UnitsPreference {
    Millimetre,
    Centimetre,
    Metre,
    Inch,
    Foot,
}

impl UnitsPreference {
    pub const DEFAULT: Self = Self::Millimetre;
}

#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct SketchRegistryEntry {
    pub label: String,
    pub filename: String,
}

#[derive(Clone, Debug, PartialEq, Default, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct SketchRegistry {
    order: Vec<SketchId>,
    entries: BTreeMap<SketchId, SketchRegistryEntry>,
}

impl SketchRegistry {
    #[must_use]
    pub fn new() -> Self {
        Self::default()
    }

    #[must_use]
    pub fn order(&self) -> &[SketchId] {
        &self.order
    }

    #[must_use]
    pub fn entry(&self, id: SketchId) -> Option<&SketchRegistryEntry> {
        self.entries.get(&id)
    }

    pub fn iter(&self) -> impl Iterator<Item = (SketchId, &SketchRegistryEntry)> + '_ {
        self.order
            .iter()
            .filter_map(|id| self.entries.get(id).map(|e| (*id, e)))
    }

    pub(crate) fn insert(&mut self, id: SketchId, entry: SketchRegistryEntry) {
        if !self.entries.contains_key(&id) {
            self.order.push(id);
        }
        self.entries.insert(id, entry);
    }

    pub(crate) fn remove(&mut self, id: SketchId) -> Option<SketchRegistryEntry> {
        self.order.retain(|other| *other != id);
        self.entries.remove(&id)
    }

    pub(crate) fn label_mut(&mut self, id: SketchId) -> Option<&mut String> {
        self.entries.get_mut(&id).map(|entry| &mut entry.label)
    }
}

#[derive(Copy, Clone, Debug, PartialEq, Eq, thiserror::Error)]
pub enum RenameSketchError {
    #[error("sketch {0:?} not found")]
    UnknownSketch(SketchId),
    #[error("sketch label must contain non-whitespace characters")]
    EmptyLabel,
}

#[derive(Copy, Clone, Debug, PartialEq, Eq, thiserror::Error)]
pub enum RenameExtrudeError {
    #[error("extrude {0:?} not found")]
    UnknownExtrude(ExtrudeId),
    #[error("extrude label must contain non-whitespace characters")]
    EmptyLabel,
}

#[derive(Copy, Clone, Debug, PartialEq, Eq, thiserror::Error)]
pub enum RenameRevolveError {
    #[error("revolve {0:?} not found")]
    UnknownRevolve(RevolveId),
    #[error("revolve label must contain non-whitespace characters")]
    EmptyLabel,
}

#[derive(Copy, Clone, Debug, PartialEq, Eq, thiserror::Error)]
pub enum RenameSweepError {
    #[error("sweep {0:?} not found")]
    UnknownSweep(SweepId),
    #[error("sweep label must contain non-whitespace characters")]
    EmptyLabel,
}

#[derive(Copy, Clone, Debug, PartialEq, Eq, thiserror::Error)]
pub enum RenameDatumError {
    #[error("datum {0:?} not found")]
    UnknownDatum(DatumId),
    #[error("datum label must contain non-whitespace characters")]
    EmptyLabel,
}

#[derive(Copy, Clone, Debug, PartialEq, Eq, thiserror::Error)]
pub enum DatumReferenceError {
    #[error("datum cannot reference feature {0:?} built later in the tree")]
    WouldCycle(FeatureId),
}

#[derive(Copy, Clone, Debug, PartialEq, Eq, thiserror::Error)]
pub enum BindSketchToFaceError {
    #[error("sketch {0:?} not found")]
    UnknownSketch(SketchId),
    #[error("binding sketch onto feature {0:?} would make the feature graph cyclic")]
    WouldCycle(FeatureId),
}

#[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)]
pub enum SketchPlaneRef {
    Face(FaceRef),
    Datum(FeatureId),
}

impl SketchPlaneRef {
    #[must_use]
    pub fn source_feature(self) -> FeatureId {
        match self {
            Self::Face(face) => face.label.feature,
            Self::Datum(feature) => feature,
        }
    }
}

#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct DocumentParameters {
    order: Vec<String>,
    entries: BTreeMap<String, f64>,
}

impl DocumentParameters {
    #[must_use]
    pub fn new() -> Self {
        Self::default()
    }

    #[must_use]
    pub fn order(&self) -> &[String] {
        &self.order
    }

    #[must_use]
    pub fn get(&self, name: &str) -> Option<f64> {
        self.entries.get(name).copied()
    }

    pub fn insert(&mut self, name: String, value: f64) {
        if !self.entries.contains_key(&name) {
            self.order.push(name.clone());
        }
        self.entries.insert(name, value);
    }

    pub fn remove(&mut self, name: &str) -> Option<f64> {
        self.order.retain(|n| n != name);
        self.entries.remove(name)
    }
}

#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct DocumentHeader {
    pub schema: SchemaHeader,
    pub id: DocumentId,
    pub name: String,
    pub units: UnitsPreference,
    pub parameters: DocumentParameters,
    pub feature_tree: FeatureTree,
    pub sketches: SketchRegistry,
    #[serde(default)]
    pub sketch_planes: BTreeMap<SketchId, SketchPlaneRef>,
    #[serde(default)]
    pub sketch_high_water: FeatureGeneration,
    #[serde(default)]
    pub extrude_high_water: FeatureGeneration,
    #[serde(default)]
    pub revolve_high_water: FeatureGeneration,
    #[serde(default)]
    pub sweep_high_water: FeatureGeneration,
    #[serde(default)]
    pub datum_high_water: FeatureGeneration,
    #[serde(default)]
    pub body_high_water: FeatureGeneration,
    #[serde(default)]
    pub rollback: RollbackMarker,
    #[serde(default)]
    pub suppressed: BTreeSet<FeatureId>,
    #[serde(skip)]
    pub extrudes: BTreeMap<ExtrudeId, ExtrudeFeature>,
    #[serde(skip)]
    pub extrude_labels: BTreeMap<ExtrudeId, String>,
    #[serde(skip)]
    pub revolves: BTreeMap<RevolveId, RevolveFeature>,
    #[serde(skip)]
    pub revolve_labels: BTreeMap<RevolveId, String>,
    #[serde(skip)]
    pub sweeps: BTreeMap<SweepId, SweepFeature>,
    #[serde(skip)]
    pub sweep_labels: BTreeMap<SweepId, String>,
    #[serde(skip)]
    pub datums: BTreeMap<DatumId, DatumFeature>,
    #[serde(skip)]
    pub datum_labels: BTreeMap<DatumId, String>,
}

impl DocumentHeader {
    #[must_use]
    pub fn new(id: DocumentId, name: String) -> Self {
        Self {
            schema: SchemaHeader::bone_document(),
            id,
            name,
            units: UnitsPreference::DEFAULT,
            parameters: DocumentParameters::new(),
            feature_tree: FeatureTree::seeded(),
            sketches: SketchRegistry::new(),
            sketch_planes: BTreeMap::new(),
            sketch_high_water: FeatureGeneration::START,
            extrude_high_water: FeatureGeneration::START,
            revolve_high_water: FeatureGeneration::START,
            sweep_high_water: FeatureGeneration::START,
            datum_high_water: FeatureGeneration::START,
            body_high_water: FeatureGeneration::START,
            rollback: RollbackMarker::AtEnd,
            suppressed: BTreeSet::new(),
            extrudes: BTreeMap::new(),
            extrude_labels: BTreeMap::new(),
            revolves: BTreeMap::new(),
            revolve_labels: BTreeMap::new(),
            sweeps: BTreeMap::new(),
            sweep_labels: BTreeMap::new(),
            datums: BTreeMap::new(),
            datum_labels: BTreeMap::new(),
        }
    }

    pub(crate) fn rebuild_edges(&mut self, sketches: &BTreeMap<SketchId, Sketch3>) {
        let bindings: Vec<(FeatureId, FeatureId)> = self
            .sketch_planes
            .iter()
            .filter_map(|(sketch_id, plane)| {
                let sketch = self.feature_tree.feature_of_sketch(*sketch_id)?;
                Some((plane.source_feature(), sketch))
            })
            .collect();
        self.feature_tree.rebuild_edges(
            self.extrudes.iter().map(|(id, feature)| (*id, feature)),
            self.revolves.iter().map(|(id, feature)| (*id, feature)),
            self.sweeps.iter().map(|(id, feature)| (*id, feature)),
            self.datums.iter().map(|(id, feature)| (*id, feature)),
            sketches.iter().map(|(id, sketch)| (*id, sketch)),
            bindings.into_iter(),
        );
    }

    pub(crate) fn retire_feature(&mut self, feature: FeatureId) {
        self.suppressed.remove(&feature);
        if self.rollback.feature() == Some(feature) {
            let order = self.feature_tree.rollable_order();
            let successor = order
                .iter()
                .position(|candidate| *candidate == feature)
                .and_then(|at| order.get(at + 1))
                .copied();
            self.rollback = successor.map_or(RollbackMarker::AtEnd, RollbackMarker::Above);
        }
    }

    pub(crate) fn normalize_high_water(&mut self) {
        self.feature_tree.raise_high_water();
        self.sketch_high_water = self
            .sketch_high_water
            .raised_to(max_gen(self.sketches.order().iter().copied()));
        self.extrude_high_water = self
            .extrude_high_water
            .raised_to(max_gen(self.extrudes.keys().copied()));
        self.revolve_high_water = self
            .revolve_high_water
            .raised_to(max_gen(self.revolves.keys().copied()));
        self.sweep_high_water = self
            .sweep_high_water
            .raised_to(max_gen(self.sweeps.keys().copied()));
        self.datum_high_water = self
            .datum_high_water
            .raised_to(max_gen(self.datums.keys().copied()));
    }

    fn allocate_sketch(&mut self) -> SketchId {
        let next = self
            .sketch_high_water
            .raised_to(max_gen(self.sketches.order().iter().copied()))
            .succ();
        self.sketch_high_water = next;
        gen_key(next)
    }

    fn allocate_extrude(&mut self) -> ExtrudeId {
        let next = self
            .extrude_high_water
            .raised_to(max_gen(self.extrudes.keys().copied()))
            .succ();
        self.extrude_high_water = next;
        gen_key(next)
    }

    fn allocate_revolve(&mut self) -> RevolveId {
        let next = self
            .revolve_high_water
            .raised_to(max_gen(self.revolves.keys().copied()))
            .succ();
        self.revolve_high_water = next;
        gen_key(next)
    }

    fn allocate_sweep(&mut self) -> SweepId {
        let next = self
            .sweep_high_water
            .raised_to(max_gen(self.sweeps.keys().copied()))
            .succ();
        self.sweep_high_water = next;
        gen_key(next)
    }

    fn allocate_datum(&mut self) -> DatumId {
        let next = self
            .datum_high_water
            .raised_to(max_gen(self.datums.keys().copied()))
            .succ();
        self.datum_high_water = next;
        gen_key(next)
    }
}

#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct SketchFile {
    pub schema: SchemaHeader,
    pub sketch: Sketch3,
}

impl SketchFile {
    #[must_use]
    pub fn new(sketch: Sketch3) -> Self {
        Self {
            schema: SchemaHeader::bone_document(),
            sketch,
        }
    }
}

#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct ExtrudeFile {
    pub schema: SchemaHeader,
    pub feature: ExtrudeFeature,
    pub label: String,
}

impl ExtrudeFile {
    #[must_use]
    pub fn new(feature: ExtrudeFeature, label: String) -> Self {
        Self {
            schema: SchemaHeader::bone_document(),
            feature,
            label,
        }
    }
}

#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct RevolveFile {
    pub schema: SchemaHeader,
    pub feature: RevolveFeature,
    pub label: String,
}

impl RevolveFile {
    #[must_use]
    pub fn new(feature: RevolveFeature, label: String) -> Self {
        Self {
            schema: SchemaHeader::bone_document(),
            feature,
            label,
        }
    }
}

#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct SweepFile {
    pub schema: SchemaHeader,
    pub feature: SweepFeature,
    pub label: String,
}

impl SweepFile {
    #[must_use]
    pub fn new(feature: SweepFeature, label: String) -> Self {
        Self {
            schema: SchemaHeader::bone_document(),
            feature,
            label,
        }
    }
}

#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct DatumFile {
    pub schema: SchemaHeader,
    pub feature: DatumFeature,
    pub label: String,
}

impl DatumFile {
    #[must_use]
    pub fn new(feature: DatumFeature, label: String) -> Self {
        Self {
            schema: SchemaHeader::bone_document(),
            feature,
            label,
        }
    }
}

#[derive(Clone, Debug, PartialEq)]
pub struct Document {
    header: DocumentHeader,
    sketches: BTreeMap<SketchId, Sketch3>,
    bodies: BTreeMap<BodyId, ImportedSolid>,
}

fn axis_targets_sketch(axis: RevolveAxis, sketch_feature: Option<FeatureId>) -> bool {
    matches!(axis, RevolveAxis::Sketch3Line { sketch, .. } if Some(sketch) == sketch_feature)
}

fn alignment_axis(alignment: SweepAlignment) -> Option<RevolveAxis> {
    match alignment {
        SweepAlignment::DirectionVector { axis } => Some(axis),
        SweepAlignment::MinimumTwist | SweepAlignment::NonePlanar | SweepAlignment::AllFaces => {
            None
        }
    }
}

impl Document {
    #[must_use]
    pub fn new(id: DocumentId, name: String) -> Self {
        Self {
            header: DocumentHeader::new(id, name),
            sketches: BTreeMap::new(),
            bodies: BTreeMap::new(),
        }
    }

    pub(crate) fn from_parts(
        mut header: DocumentHeader,
        sketches: BTreeMap<SketchId, Sketch3>,
        bodies: BTreeMap<BodyId, ImportedSolid>,
    ) -> Self {
        header.normalize_high_water();
        header.body_high_water = header
            .body_high_water
            .raised_to(max_gen(bodies.keys().copied()));
        header.rebuild_edges(&sketches);
        Self {
            header,
            sketches,
            bodies,
        }
    }

    fn allocate_body(&mut self) -> BodyId {
        let next = self
            .header
            .body_high_water
            .raised_to(max_gen(self.bodies.keys().copied()))
            .succ();
        self.header.body_high_water = next;
        gen_key(next)
    }

    #[must_use]
    pub fn header(&self) -> &DocumentHeader {
        &self.header
    }

    #[must_use]
    pub fn id(&self) -> DocumentId {
        self.header.id
    }

    #[must_use]
    pub fn name(&self) -> &str {
        &self.header.name
    }

    #[must_use]
    pub fn units(&self) -> UnitsPreference {
        self.header.units
    }

    #[must_use]
    pub fn feature_tree(&self) -> &FeatureTree {
        &self.header.feature_tree
    }

    #[must_use]
    pub fn parameters(&self) -> &DocumentParameters {
        &self.header.parameters
    }

    #[must_use]
    pub fn registry(&self) -> &SketchRegistry {
        &self.header.sketches
    }

    #[must_use]
    pub fn sketch3(&self, id: SketchId) -> Option<&Sketch3> {
        self.sketches.get(&id)
    }

    #[must_use]
    pub fn sketch(&self, id: SketchId) -> Option<Sketch> {
        self.sketches.get(&id).map(|s| s.project().into_sketch())
    }

    #[must_use]
    pub fn sketch_label(&self, id: SketchId) -> Option<&str> {
        self.header.sketches.entry(id).map(|e| e.label.as_str())
    }

    pub fn rename_sketch(&mut self, id: SketchId, label: &str) -> Result<(), RenameSketchError> {
        let trimmed = label.trim();
        if trimmed.is_empty() {
            return Err(RenameSketchError::EmptyLabel);
        }
        let slot = self
            .header
            .sketches
            .label_mut(id)
            .ok_or(RenameSketchError::UnknownSketch(id))?;
        trimmed.clone_into(slot);
        Ok(())
    }

    pub fn sketches(&self) -> impl Iterator<Item = (SketchId, &Sketch3)> + '_ {
        self.header
            .sketches
            .order()
            .iter()
            .filter_map(|id| self.sketches.get(id).map(|s| (*id, s)))
    }

    #[must_use]
    pub fn sketches_map(&self) -> &BTreeMap<SketchId, Sketch3> {
        &self.sketches
    }

    pub fn set_name(&mut self, name: String) {
        self.header.name = name;
    }

    pub fn set_units(&mut self, units: UnitsPreference) {
        self.header.units = units;
    }

    #[must_use]
    pub fn allocate_sketch(&mut self) -> SketchId {
        self.header.allocate_sketch()
    }

    pub fn insert_sketch(&mut self, id: SketchId, label: String, sketch: Sketch3) {
        let filename = sketch_filename(id);
        self.header
            .sketches
            .insert(id, SketchRegistryEntry { label, filename });
        self.header.sketch_high_water = self.header.sketch_high_water.raised_to(key_gen(id));
        let existed = self.header.feature_tree.feature_of_sketch(id).is_some();
        let placed = self.header.feature_tree.push_sketch(id);
        if !existed {
            self.place_at_rollback_bar(placed);
        }
        self.sketches.insert(id, sketch);
        let dependents: Vec<ExtrudeId> = self.extrudes_of_sketch(id).collect();
        dependents.iter().for_each(|extrude| {
            if let Some(feature) = self.header.extrudes.get(extrude).copied() {
                self.header.feature_tree.push_extrude(*extrude, &feature);
            }
        });
        self.header.rebuild_edges(&self.sketches);
    }

    pub fn replace_sketch(&mut self, id: SketchId, sketch: Sketch3) -> Option<Sketch3> {
        let previous = self.sketches.insert(id, sketch);
        self.header.rebuild_edges(&self.sketches);
        previous
    }

    pub fn bind_sketch_to_face(
        &mut self,
        sketch: SketchId,
        face: FaceRef,
    ) -> Result<(), BindSketchToFaceError> {
        self.bind_sketch_to_plane(sketch, SketchPlaneRef::Face(face))
    }

    pub fn bind_sketch_to_datum_plane(
        &mut self,
        sketch: SketchId,
        datum: FeatureId,
    ) -> Result<(), BindSketchToFaceError> {
        self.bind_sketch_to_plane(sketch, SketchPlaneRef::Datum(datum))
    }

    fn bind_sketch_to_plane(
        &mut self,
        sketch: SketchId,
        plane: SketchPlaneRef,
    ) -> Result<(), BindSketchToFaceError> {
        let sketch_feature = self
            .header
            .feature_tree
            .feature_of_sketch(sketch)
            .ok_or(BindSketchToFaceError::UnknownSketch(sketch))?;
        let target = plane.source_feature();
        let cyclic = target == sketch_feature
            || self
                .header
                .feature_tree
                .descendants(sketch_feature)
                .contains(&target);
        if cyclic {
            return Err(BindSketchToFaceError::WouldCycle(target));
        }
        self.header.sketch_planes.insert(sketch, plane);
        self.header
            .feature_tree
            .bind_face_to_sketch(target, sketch_feature);
        Ok(())
    }

    #[must_use]
    pub fn sketch_plane_binding(&self, sketch: SketchId) -> Option<SketchPlaneRef> {
        self.header.sketch_planes.get(&sketch).copied()
    }

    #[must_use]
    pub fn reorder_is_legal(&self, moved: FeatureId, anchor: FeatureId, before: bool) -> bool {
        self.header
            .feature_tree
            .reorder_is_legal(moved, anchor, before)
    }

    pub fn reorder_feature(&mut self, moved: FeatureId, anchor: FeatureId, before: bool) -> bool {
        self.header.feature_tree.try_reorder(moved, anchor, before)
    }

    #[must_use]
    pub fn rollback(&self) -> RollbackMarker {
        self.header.rollback
    }

    pub fn roll_to_here(&mut self, feature: FeatureId) {
        if self.header.feature_tree.rollable_order().contains(&feature) {
            self.header.rollback = RollbackMarker::Above(feature);
        }
    }

    pub fn roll_to_end(&mut self) {
        self.header.rollback = RollbackMarker::AtEnd;
    }

    pub fn roll_to_previous(&mut self) {
        let order = self.header.feature_tree.rollable_order();
        if order.is_empty() {
            return;
        }
        let position = match self.header.rollback {
            RollbackMarker::AtEnd => order.len(),
            RollbackMarker::Above(feature) => order
                .iter()
                .position(|candidate| *candidate == feature)
                .unwrap_or(order.len()),
        };
        self.header.rollback = RollbackMarker::Above(order[position.saturating_sub(1)]);
    }

    #[must_use]
    pub fn is_rolled_back(&self, feature: FeatureId) -> bool {
        self.header
            .feature_tree
            .rolled_back(self.header.rollback)
            .contains(&feature)
    }

    fn place_at_rollback_bar(&mut self, feature: FeatureId) {
        if let Some(anchor) = self.header.rollback.feature() {
            self.header
                .feature_tree
                .move_before_keeping_order(feature, anchor);
        }
    }

    #[must_use]
    pub fn suppressed(&self) -> &BTreeSet<FeatureId> {
        &self.header.suppressed
    }

    #[must_use]
    pub fn suppression_state(&self, feature: FeatureId) -> SuppressionState {
        let effective = self.header.suppressed.contains(&feature)
            || self
                .header
                .feature_tree
                .ancestors(feature)
                .iter()
                .any(|ancestor| self.header.suppressed.contains(ancestor));
        if effective {
            SuppressionState::Suppressed
        } else {
            SuppressionState::Active
        }
    }

    pub fn suppress(&mut self, feature: FeatureId) {
        if self.header.feature_tree.is_seeded_reference(feature) {
            return;
        }
        let cascade = self.header.feature_tree.descendants(feature);
        self.header.suppressed.insert(feature);
        self.header.suppressed.extend(cascade);
    }

    pub fn unsuppress(&mut self, feature: FeatureId) {
        let cleared = self
            .header
            .feature_tree
            .ancestors(feature)
            .into_iter()
            .chain(self.header.feature_tree.descendants(feature))
            .chain([feature]);
        cleared.for_each(|target| {
            self.header.suppressed.remove(&target);
        });
    }

    pub fn insert_extrude(&mut self, id: ExtrudeId, feature: ExtrudeFeature) {
        self.header.extrude_high_water = self.header.extrude_high_water.raised_to(key_gen(id));
        let existed = self.header.feature_tree.feature_of_extrude(id).is_some();
        let placed = self.header.feature_tree.push_extrude(id, &feature);
        if !existed {
            self.place_at_rollback_bar(placed);
        }
        self.header.extrudes.insert(id, feature);
        self.header
            .extrude_labels
            .entry(id)
            .or_insert_with(|| format!("Extrude{}", key_index(id)));
    }

    #[must_use]
    pub fn commit_extrude(&mut self, feature: ExtrudeFeature) -> ExtrudeId {
        let id = self.header.allocate_extrude();
        self.insert_extrude(id, feature);
        id
    }

    pub fn remove_extrude(&mut self, id: ExtrudeId) -> Option<ExtrudeFeature> {
        if let Some(feature) = self.header.feature_tree.feature_of_extrude(id) {
            self.header.retire_feature(feature);
            self.header.feature_tree.remove_extrude(id);
        }
        self.header.extrude_labels.remove(&id);
        self.header.extrudes.remove(&id)
    }

    #[must_use]
    pub fn extrude(&self, id: ExtrudeId) -> Option<&ExtrudeFeature> {
        self.header.extrudes.get(&id)
    }

    #[must_use]
    pub fn extrude_label(&self, id: ExtrudeId) -> Option<&str> {
        self.header.extrude_labels.get(&id).map(String::as_str)
    }

    pub fn rename_extrude(&mut self, id: ExtrudeId, label: &str) -> Result<(), RenameExtrudeError> {
        let trimmed = label.trim();
        if trimmed.is_empty() {
            return Err(RenameExtrudeError::EmptyLabel);
        }
        let slot = self
            .header
            .extrude_labels
            .get_mut(&id)
            .ok_or(RenameExtrudeError::UnknownExtrude(id))?;
        trimmed.clone_into(slot);
        Ok(())
    }

    pub fn insert_revolve(&mut self, id: RevolveId, feature: RevolveFeature) {
        self.header.revolve_high_water = self.header.revolve_high_water.raised_to(key_gen(id));
        let existed = self.header.feature_tree.feature_of_revolve(id).is_some();
        let placed = self.header.feature_tree.push_revolve(id, &feature);
        if !existed {
            self.place_at_rollback_bar(placed);
        }
        self.header.revolves.insert(id, feature);
        self.header
            .revolve_labels
            .entry(id)
            .or_insert_with(|| format!("Revolve{}", key_index(id)));
    }

    #[must_use]
    pub fn commit_revolve(&mut self, feature: RevolveFeature) -> RevolveId {
        let id = self.header.allocate_revolve();
        self.insert_revolve(id, feature);
        id
    }

    pub fn remove_revolve(&mut self, id: RevolveId) -> Option<RevolveFeature> {
        if let Some(feature) = self.header.feature_tree.feature_of_revolve(id) {
            self.header.retire_feature(feature);
            self.header.feature_tree.remove_revolve(id);
        }
        self.header.revolve_labels.remove(&id);
        self.header.revolves.remove(&id)
    }

    #[must_use]
    pub fn revolve(&self, id: RevolveId) -> Option<&RevolveFeature> {
        self.header.revolves.get(&id)
    }

    #[must_use]
    pub fn revolve_label(&self, id: RevolveId) -> Option<&str> {
        self.header.revolve_labels.get(&id).map(String::as_str)
    }

    pub fn rename_revolve(&mut self, id: RevolveId, label: &str) -> Result<(), RenameRevolveError> {
        let trimmed = label.trim();
        if trimmed.is_empty() {
            return Err(RenameRevolveError::EmptyLabel);
        }
        let slot = self
            .header
            .revolve_labels
            .get_mut(&id)
            .ok_or(RenameRevolveError::UnknownRevolve(id))?;
        trimmed.clone_into(slot);
        Ok(())
    }

    pub fn insert_sweep(&mut self, id: SweepId, feature: SweepFeature) {
        self.header.sweep_high_water = self.header.sweep_high_water.raised_to(key_gen(id));
        let existed = self.header.feature_tree.feature_of_sweep(id).is_some();
        let placed = self.header.feature_tree.push_sweep(id, &feature);
        if !existed {
            self.place_at_rollback_bar(placed);
        }
        self.header.sweeps.insert(id, feature);
        self.header
            .sweep_labels
            .entry(id)
            .or_insert_with(|| format!("Sweep{}", key_index(id)));
    }

    #[must_use]
    pub fn commit_sweep(&mut self, feature: SweepFeature) -> SweepId {
        let id = self.header.allocate_sweep();
        self.insert_sweep(id, feature);
        id
    }

    pub fn remove_sweep(&mut self, id: SweepId) -> Option<SweepFeature> {
        if let Some(feature) = self.header.feature_tree.feature_of_sweep(id) {
            self.header.retire_feature(feature);
            self.header.feature_tree.remove_sweep(id);
        }
        self.header.sweep_labels.remove(&id);
        self.header.sweeps.remove(&id)
    }

    #[must_use]
    pub fn sweep(&self, id: SweepId) -> Option<&SweepFeature> {
        self.header.sweeps.get(&id)
    }

    #[must_use]
    pub fn sweep_label(&self, id: SweepId) -> Option<&str> {
        self.header.sweep_labels.get(&id).map(String::as_str)
    }

    pub fn rename_sweep(&mut self, id: SweepId, label: &str) -> Result<(), RenameSweepError> {
        let trimmed = label.trim();
        if trimmed.is_empty() {
            return Err(RenameSweepError::EmptyLabel);
        }
        let slot = self
            .header
            .sweep_labels
            .get_mut(&id)
            .ok_or(RenameSweepError::UnknownSweep(id))?;
        trimmed.clone_into(slot);
        Ok(())
    }

    fn apply_datum(&mut self, id: DatumId, feature: DatumFeature) {
        self.header.datum_high_water = self.header.datum_high_water.raised_to(key_gen(id));
        let existed = self.header.feature_tree.feature_of_datum(id).is_some();
        let placed = self.header.feature_tree.push_datum(id, &feature);
        if !existed {
            self.place_at_rollback_bar(placed);
        }
        let prefix = datum_label_prefix(&feature);
        let ordinal = self
            .header
            .datums
            .values()
            .filter(|existing| datum_label_prefix(existing) == prefix)
            .count()
            + 1;
        self.header.datums.insert(id, feature);
        self.header
            .datum_labels
            .entry(id)
            .or_insert_with(|| format!("{prefix}{ordinal}"));
    }

    pub fn insert_datum(
        &mut self,
        id: DatumId,
        feature: DatumFeature,
    ) -> Result<(), DatumReferenceError> {
        if let Some(reference) = self
            .header
            .feature_tree
            .datum_reference_conflict(id, &feature)
        {
            return Err(DatumReferenceError::WouldCycle(reference));
        }
        self.apply_datum(id, feature);
        Ok(())
    }

    #[must_use]
    pub fn commit_datum(&mut self, feature: DatumFeature) -> DatumId {
        let id = self.header.allocate_datum();
        self.apply_datum(id, feature);
        id
    }

    pub fn remove_datum(&mut self, id: DatumId) -> Option<DatumFeature> {
        if let Some(feature) = self.header.feature_tree.feature_of_datum(id) {
            self.header.retire_feature(feature);
            self.header.feature_tree.remove_datum(id);
        }
        self.header.datum_labels.remove(&id);
        self.header.datums.remove(&id)
    }

    #[must_use]
    pub fn datum(&self, id: DatumId) -> Option<&DatumFeature> {
        self.header.datums.get(&id)
    }

    pub fn datums(&self) -> impl Iterator<Item = (DatumId, &DatumFeature)> + '_ {
        self.header
            .datums
            .iter()
            .map(|(id, feature)| (*id, feature))
    }

    #[must_use]
    pub fn datum_label(&self, id: DatumId) -> Option<&str> {
        self.header.datum_labels.get(&id).map(String::as_str)
    }

    pub fn rename_datum(&mut self, id: DatumId, label: &str) -> Result<(), RenameDatumError> {
        let trimmed = label.trim();
        if trimmed.is_empty() {
            return Err(RenameDatumError::EmptyLabel);
        }
        let slot = self
            .header
            .datum_labels
            .get_mut(&id)
            .ok_or(RenameDatumError::UnknownDatum(id))?;
        trimmed.clone_into(slot);
        Ok(())
    }

    #[must_use]
    pub fn datum_of_feature(&self, feature: FeatureId) -> Option<&DatumFeature> {
        match self.header.feature_tree.node(feature)? {
            FeatureNode::Datum(id) => self.header.datums.get(&id),
            FeatureNode::Origin
            | FeatureNode::PrincipalPlane(_)
            | FeatureNode::Sketch(_)
            | FeatureNode::Extrude(_)
            | FeatureNode::Revolve(_)
            | FeatureNode::Sweep(_)
            | FeatureNode::ImportedBody(_) => None,
        }
    }

    fn extrudes_of_sketch(&self, sketch: SketchId) -> impl Iterator<Item = ExtrudeId> + '_ {
        self.header
            .extrudes
            .iter()
            .filter(move |(_, feature)| feature.sketch == sketch)
            .map(|(extrude, _)| *extrude)
    }

    fn revolves_of_sketch(&self, sketch: SketchId) -> impl Iterator<Item = RevolveId> + '_ {
        let axis_sketch = self.header.feature_tree.feature_of_sketch(sketch);
        self.header
            .revolves
            .iter()
            .filter(move |(_, feature)| {
                feature.sketch == sketch || axis_targets_sketch(feature.axis, axis_sketch)
            })
            .map(|(revolve, _)| *revolve)
    }

    fn sweeps_of_sketch(&self, sketch: SketchId) -> impl Iterator<Item = SweepId> + '_ {
        let axis_sketch = self.header.feature_tree.feature_of_sketch(sketch);
        self.header
            .sweeps
            .iter()
            .filter(move |(_, feature)| {
                feature.profile == sketch
                    || matches!(feature.path, bone_kernel::SweepPath::Sketch(path) if path == sketch)
                    || feature.guides.iter().any(|guide| {
                        matches!(guide, bone_kernel::SweepPath::Sketch(path) if *path == sketch)
                    })
                    || alignment_axis(feature.alignment)
                        .is_some_and(|axis| axis_targets_sketch(axis, axis_sketch))
            })
            .map(|(sweep, _)| *sweep)
    }

    pub fn remove_sketch(&mut self, id: SketchId) -> Option<Sketch3> {
        let dependents: Vec<ExtrudeId> = self.extrudes_of_sketch(id).collect();
        dependents.iter().for_each(|extrude| {
            self.remove_extrude(*extrude);
        });
        let revolves: Vec<RevolveId> = self.revolves_of_sketch(id).collect();
        revolves.iter().for_each(|revolve| {
            self.remove_revolve(*revolve);
        });
        let sweeps: Vec<SweepId> = self.sweeps_of_sketch(id).collect();
        sweeps.iter().for_each(|sweep| {
            self.remove_sweep(*sweep);
        });
        self.header.sketches.remove(id);
        self.header.sketch_planes.remove(&id);
        if let Some(feature) = self.header.feature_tree.feature_of_sketch(id) {
            self.header.retire_feature(feature);
            self.header.feature_tree.remove_sketch(id);
        }
        let removed = self.sketches.remove(&id);
        self.header.rebuild_edges(&self.sketches);
        removed
    }

    pub fn set_parameter(&mut self, name: String, value: f64) {
        self.header.parameters.insert(name, value);
    }

    #[must_use]
    pub fn sketch_of_feature(&self, feature: FeatureId) -> Option<&Sketch3> {
        match self.header.feature_tree.node(feature)? {
            FeatureNode::Sketch(id) => self.sketches.get(&id),
            FeatureNode::Origin
            | FeatureNode::PrincipalPlane(_)
            | FeatureNode::Extrude(_)
            | FeatureNode::Revolve(_)
            | FeatureNode::Sweep(_)
            | FeatureNode::Datum(_)
            | FeatureNode::ImportedBody(_) => None,
        }
    }

    #[must_use]
    pub fn extrude_of_feature(&self, feature: FeatureId) -> Option<&ExtrudeFeature> {
        match self.header.feature_tree.node(feature)? {
            FeatureNode::Extrude(id) => self.header.extrudes.get(&id),
            FeatureNode::Origin
            | FeatureNode::PrincipalPlane(_)
            | FeatureNode::Sketch(_)
            | FeatureNode::Revolve(_)
            | FeatureNode::Sweep(_)
            | FeatureNode::Datum(_)
            | FeatureNode::ImportedBody(_) => None,
        }
    }

    #[must_use]
    pub fn revolve_of_feature(&self, feature: FeatureId) -> Option<&RevolveFeature> {
        match self.header.feature_tree.node(feature)? {
            FeatureNode::Revolve(id) => self.header.revolves.get(&id),
            FeatureNode::Origin
            | FeatureNode::PrincipalPlane(_)
            | FeatureNode::Sketch(_)
            | FeatureNode::Extrude(_)
            | FeatureNode::Sweep(_)
            | FeatureNode::Datum(_)
            | FeatureNode::ImportedBody(_) => None,
        }
    }

    #[must_use]
    pub fn sweep_of_feature(&self, feature: FeatureId) -> Option<&SweepFeature> {
        match self.header.feature_tree.node(feature)? {
            FeatureNode::Sweep(id) => self.header.sweeps.get(&id),
            FeatureNode::Origin
            | FeatureNode::PrincipalPlane(_)
            | FeatureNode::Sketch(_)
            | FeatureNode::Extrude(_)
            | FeatureNode::Revolve(_)
            | FeatureNode::Datum(_)
            | FeatureNode::ImportedBody(_) => None,
        }
    }

    pub fn import_body<E>(
        &mut self,
        build: impl FnOnce(FeatureId) -> Result<BrepSolid, E>,
    ) -> Result<(FeatureId, BodyId), E> {
        let feature = self.header.feature_tree.allocate();
        let body = self.allocate_body();
        let solid = build(feature)?;
        self.header.feature_tree.push_imported_body(feature, body);
        self.place_at_rollback_bar(feature);
        self.bodies.insert(body, ImportedSolid::new(solid));
        Ok((feature, body))
    }

    #[must_use]
    pub fn imported_body(&self, body: BodyId) -> Option<&BrepSolid> {
        self.bodies.get(&body).map(ImportedSolid::solid)
    }

    #[must_use]
    pub fn imported_body_of_feature(&self, feature: FeatureId) -> Option<&BrepSolid> {
        match self.header.feature_tree.node(feature)? {
            FeatureNode::ImportedBody(body) => self.imported_body(body),
            FeatureNode::Origin
            | FeatureNode::PrincipalPlane(_)
            | FeatureNode::Sketch(_)
            | FeatureNode::Extrude(_)
            | FeatureNode::Revolve(_)
            | FeatureNode::Sweep(_)
            | FeatureNode::Datum(_) => None,
        }
    }

    pub fn imported_bodies(&self) -> impl Iterator<Item = (BodyId, &BrepSolid)> + '_ {
        self.bodies.iter().map(|(id, body)| (*id, body.solid()))
    }
}

pub(crate) fn key_index<K: slotmap::Key>(id: K) -> u32 {
    let Ok(idx) = u32::try_from(id.data().as_ffi() & 0xFFFF_FFFF) else {
        panic!("lower 32 bits of an ffi key fit in u32");
    };
    idx
}

pub(crate) fn key_from_index<K: slotmap::Key>(idx: u32) -> K {
    K::from(KeyData::from_ffi((1u64 << 32) | u64::from(idx)))
}

pub(crate) fn key_gen<K: slotmap::Key>(id: K) -> FeatureGeneration {
    if id.is_null() {
        FeatureGeneration::START
    } else {
        FeatureGeneration::new(u64::from(key_index(id)))
    }
}

pub(crate) fn max_gen<K: slotmap::Key>(ids: impl Iterator<Item = K>) -> FeatureGeneration {
    ids.map(key_gen).max().unwrap_or(FeatureGeneration::START)
}

pub(crate) fn gen_key<K: slotmap::Key>(mark: FeatureGeneration) -> K {
    let Ok(idx) = u32::try_from(mark.value()) else {
        panic!(
            "exhausted 32-bit key space for {}",
            core::any::type_name::<K>()
        );
    };
    key_from_index(idx)
}

fn id_stem<K: slotmap::Key>(id: K) -> String {
    format!("{:016x}", id.data().as_ffi())
}

#[must_use]
pub fn sketch_filename(id: SketchId) -> String {
    format!("{}.ron", id_stem(id))
}

#[must_use]
pub fn extrude_filename(id: ExtrudeId) -> String {
    format!("{}.ron", id_stem(id))
}

#[must_use]
pub fn revolve_filename(id: RevolveId) -> String {
    format!("{}.ron", id_stem(id))
}

#[must_use]
pub fn sweep_filename(id: SweepId) -> String {
    format!("{}.ron", id_stem(id))
}

#[must_use]
pub fn datum_filename(id: DatumId) -> String {
    format!("{}.ron", id_stem(id))
}

fn datum_label_prefix(feature: &DatumFeature) -> &'static str {
    match feature {
        DatumFeature::Plane(_) => "Plane",
        DatumFeature::Axis(_) => "Axis",
        DatumFeature::Point(_) => "Point",
        DatumFeature::CoordinateSystem(_) => "Coordinate System",
    }
}

#[must_use]
pub fn body_brep_filename(id: BodyId) -> String {
    format!("{}.brep", id_stem(id))
}

#[must_use]
pub fn body_labels_filename(id: BodyId) -> String {
    format!("{}.labels", id_stem(id))
}

#[cfg(test)]
mod tests {
    use super::feature_tree::sample_blind_extrude;
    use super::{
        BindSketchToFaceError, Document, DocumentHeader, FeatureEdge, RenameExtrudeError,
        RenameSketchError, Sketch3, SketchId, SketchPlaneRef,
    };
    use bone_types::{
        DocumentId, ExtrudeId, FaceFingerprint, FaceLabel, FaceRef, FaceRole, FeatureId, Plane3,
        Point3, RollbackMarker, SuppressionState, UnitVec3,
    };

    fn doc_with_sketch() -> (Document, SketchId) {
        let mut document = Document::new(DocumentId::default(), "doc".to_owned());
        let id = SketchId::default();
        document.insert_sketch(id, "Sketch1".to_owned(), Sketch3::new());
        (document, id)
    }

    #[test]
    fn reorder_respects_dependency_order() -> Result<(), Box<dyn std::error::Error>> {
        let (mut document, sketch) = doc_with_sketch();
        let extrude = document.commit_extrude(sample_blind_extrude(sketch));
        let tree = document.feature_tree();
        let sketch_feature = tree
            .feature_of_sketch(sketch)
            .ok_or("the chain resolves to a sketch feature id")?;
        let extrude_feature = tree
            .feature_of_extrude(extrude)
            .ok_or("the chain resolves to an extrude feature id")?;
        assert!(
            !document.reorder_is_legal(extrude_feature, sketch_feature, true),
            "an extrude may not precede its source sketch",
        );
        assert!(!document.reorder_feature(extrude_feature, sketch_feature, true));

        let mut document = Document::new(DocumentId::default(), "doc".to_owned());
        let s1 = SketchId::default();
        document.insert_sketch(s1, "Sketch1".to_owned(), Sketch3::new());
        let e1 = document.commit_extrude(sample_blind_extrude(s1));
        let s2 = document.allocate_sketch();
        document.insert_sketch(s2, "Sketch2".to_owned(), Sketch3::new());
        let e2 = document.commit_extrude(sample_blind_extrude(s2));
        let tree = document.feature_tree();
        let f1 = tree
            .feature_of_extrude(e1)
            .ok_or("the first extrude resolves to a feature id")?;
        let f2 = tree
            .feature_of_extrude(e2)
            .ok_or("the second extrude resolves to a feature id")?;
        assert!(
            document.reorder_is_legal(f1, f2, false),
            "the first extrude may move after the second",
        );
        assert!(document.reorder_feature(f1, f2, false));
        assert!(
            document.feature_tree().order_violation().is_none(),
            "the reorder leaves every parent before its child",
        );
        Ok(())
    }

    #[test]
    fn rename_sketch_updates_trims_and_rejects() -> Result<(), Box<dyn std::error::Error>> {
        let (mut document, id) = doc_with_sketch();
        let Err(err) = document.rename_sketch(id, "   ") else {
            panic!("whitespace-only label must be rejected");
        };
        assert_eq!(err, RenameSketchError::EmptyLabel);
        assert_eq!(document.sketch_label(id), Some("Sketch1"));
        document.rename_sketch(id, "Profile")?;
        assert_eq!(document.sketch_label(id), Some("Profile"));
        document.rename_sketch(id, "  Base \n")?;
        assert_eq!(document.sketch_label(id), Some("Base"));

        let mut document = Document::new(DocumentId::default(), "doc".to_owned());
        let stranger = SketchId::default();
        let Err(err) = document.rename_sketch(stranger, "Profile") else {
            panic!("unknown sketch must be rejected");
        };
        assert_eq!(err, RenameSketchError::UnknownSketch(stranger));
        Ok(())
    }

    #[test]
    fn insert_and_remove_wire_extrude_edges() -> Result<(), Box<dyn std::error::Error>> {
        let (mut document, sketch) = doc_with_sketch();
        let extrude = ExtrudeId::default();
        let feature = sample_blind_extrude(sketch);
        document.insert_extrude(extrude, feature);
        let extrude_feature = document
            .feature_tree()
            .feature_of_extrude(extrude)
            .ok_or("extrude node present after insert")?;
        assert_eq!(document.extrude_of_feature(extrude_feature), Some(&feature));
        assert_eq!(document.sketch_of_feature(extrude_feature), None);
        assert!(document.extrude_of_feature(extrude_feature).is_some());
        let removed = document.remove_extrude(extrude);
        assert_eq!(removed, Some(sample_blind_extrude(sketch)));
        assert_eq!(document.feature_tree().feature_of_extrude(extrude), None);

        let (mut document, sketch) = doc_with_sketch();
        let extrude = ExtrudeId::default();
        document.insert_extrude(extrude, sample_blind_extrude(sketch));
        assert_eq!(document.feature_tree().edges().len(), 1);
        document.remove_sketch(sketch);
        assert_eq!(document.feature_tree().feature_of_extrude(extrude), None);
        assert!(document.feature_tree().edges().is_empty());

        let mut document = Document::new(DocumentId::default(), "doc".to_owned());
        let sketch = SketchId::default();
        let extrude = ExtrudeId::default();
        document.insert_extrude(extrude, sample_blind_extrude(sketch));
        assert!(document.feature_tree().edges().is_empty());
        document.insert_sketch(sketch, "Sketch1".to_owned(), Sketch3::new());
        let sketch_feature = document
            .feature_tree()
            .feature_of_sketch(sketch)
            .ok_or("sketch node present after insert")?;
        let extrude_feature = document
            .feature_tree()
            .feature_of_extrude(extrude)
            .ok_or("extrude node present")?;
        assert_eq!(
            document.feature_tree().edges(),
            &[super::FeatureEdge::SketchToExtrude {
                sketch: sketch_feature,
                extrude: extrude_feature,
            }]
        );
        Ok(())
    }

    #[test]
    fn remove_sketch_cascades_a_sweep_whose_direction_axis_it_hosts()
    -> Result<(), Box<dyn std::error::Error>> {
        use crate::sketch3::{Edit3Outcome, Sketch3Edit, Sketch3Entity};
        use bone_kernel::{RevolveAxis, SweepAlignment, SweepFeature, SweepPath};

        let mut document = Document::new(DocumentId::default(), "doc".to_owned());
        let profile = document.allocate_sketch();
        document.insert_sketch(profile, "Profile".to_owned(), Sketch3::new());
        let path = document.allocate_sketch();
        document.insert_sketch(path, "Path".to_owned(), Sketch3::new());

        let entity = |outcome| match outcome {
            Edit3Outcome::Entity(id) => id,
            Edit3Outcome::Relation(_) | Edit3Outcome::None => panic!("expected an entity"),
        };
        let (axis, start) = Sketch3::new().apply(Sketch3Edit::AddEntity(Sketch3Entity::point(
            Point3::origin(),
        )));
        let start = entity(start);
        let (axis, end) = axis.apply(Sketch3Edit::AddEntity(Sketch3Entity::point(
            Point3::from_mm(0.0, 5.0, 0.0),
        )));
        let end = entity(end);
        let (axis, line) = axis.apply(Sketch3Edit::AddEntity(Sketch3Entity::line(
            start, end, false,
        )));
        let line = entity(line);

        let axis_id = document.allocate_sketch();
        document.insert_sketch(axis_id, "Axis".to_owned(), axis);
        let axis_feature = document
            .feature_tree()
            .feature_of_sketch(axis_id)
            .ok_or("the axis sketch has a feature node")?;

        let sweep = document.commit_sweep(SweepFeature {
            alignment: SweepAlignment::DirectionVector {
                axis: RevolveAxis::Sketch3Line {
                    sketch: axis_feature,
                    entity: line,
                },
            },
            ..SweepFeature::boss(profile, SweepPath::Sketch(path))
        });
        assert!(document.feature_tree().feature_of_sweep(sweep).is_some());

        document.remove_sketch(axis_id);

        assert_eq!(
            document.feature_tree().feature_of_sweep(sweep),
            None,
            "deleting the sketch that hosts the sweep direction axis removes the orphaned sweep"
        );
        Ok(())
    }

    #[test]
    fn extrude_labels_and_lifecycle() -> Result<(), Box<dyn std::error::Error>> {
        let (mut document, sketch) = doc_with_sketch();
        let first = document.commit_extrude(sample_blind_extrude(sketch));
        let second = document.commit_extrude(sample_blind_extrude(sketch));
        assert_ne!(first, second);
        assert_eq!(document.extrude_label(first), Some("Extrude1"));
        assert_eq!(document.extrude_label(second), Some("Extrude2"));

        document.rename_extrude(first, "  Boss \n")?;
        assert_eq!(document.extrude_label(first), Some("Boss"));
        assert_eq!(
            document.rename_extrude(second, "   "),
            Err(RenameExtrudeError::EmptyLabel)
        );
        assert_eq!(document.extrude_label(second), Some("Extrude2"));
        let stranger = ExtrudeId::default();
        assert_eq!(
            document.rename_extrude(stranger, "Boss"),
            Err(RenameExtrudeError::UnknownExtrude(stranger))
        );

        document.remove_extrude(second);
        assert_eq!(document.extrude_label(second), None);
        document.insert_extrude(first, sample_blind_extrude(sketch));
        assert_eq!(
            document.extrude_label(first),
            Some("Boss"),
            "re-inserting an existing extrude preserves its renamed label",
        );
        Ok(())
    }

    #[test]
    fn extrude_ids_stay_unique_across_removal() {
        let (mut document, sketch) = doc_with_sketch();
        let first = document.commit_extrude(sample_blind_extrude(sketch));
        let second = document.commit_extrude(sample_blind_extrude(sketch));
        document.remove_extrude(first);
        let third = document.commit_extrude(sample_blind_extrude(sketch));
        assert_eq!(document.extrude_label(second), Some("Extrude2"));
        assert_eq!(document.extrude_label(third), Some("Extrude3"));
        assert_ne!(
            document.extrude_label(second),
            document.extrude_label(third),
            "default labels track the id, so remove + re-commit cannot collide",
        );

        let (mut document, sketch) = doc_with_sketch();
        let first = document.commit_extrude(sample_blind_extrude(sketch));
        let second = document.commit_extrude(sample_blind_extrude(sketch));
        document.remove_extrude(second);
        let third = document.commit_extrude(sample_blind_extrude(sketch));
        assert_ne!(
            third, second,
            "removing the top extrude must not free its id for reuse"
        );
        assert_ne!(third, first);

        let mut document = Document::new(DocumentId::default(), "doc".to_owned());
        let first = document.allocate_sketch();
        document.insert_sketch(first, "Sketch1".to_owned(), Sketch3::new());
        let second = document.allocate_sketch();
        document.insert_sketch(second, "Sketch2".to_owned(), Sketch3::new());
        document.remove_sketch(second);
        let third = document.allocate_sketch();
        assert_ne!(first, second);
        assert_ne!(
            third, second,
            "a removed sketch id must not be handed out again"
        );
        assert_ne!(third, first);
    }

    fn sample_face_ref(body: super::FeatureId) -> FaceRef {
        FaceRef::new(
            FaceLabel {
                feature: body,
                role: FaceRole::EndCap,
            },
            FaceFingerprint {
                plane: Plane3::new_unchecked(
                    Point3::from_mm(0.0, 0.0, 4.0),
                    UnitVec3::x_axis(),
                    UnitVec3::y_axis(),
                ),
                centroid: Point3::from_mm(0.0, 0.0, 4.0),
            },
        )
    }

    #[test]
    fn sketch_plane_binding_records_round_trips_and_rejects()
    -> Result<(), Box<dyn std::error::Error>> {
        let (mut document, base_sketch) = doc_with_sketch();
        let extrude = document.commit_extrude(sample_blind_extrude(base_sketch));
        let body = document
            .feature_tree()
            .feature_of_extrude(extrude)
            .ok_or("the extrude has a feature node")?;
        let downstream = document.allocate_sketch();
        document.insert_sketch(downstream, "Sketch2".to_owned(), Sketch3::new());
        let face = sample_face_ref(body);
        document.bind_sketch_to_face(downstream, face)?;
        assert_eq!(
            document.sketch_plane_binding(downstream),
            Some(SketchPlaneRef::Face(face))
        );
        let sketch_feature = document
            .feature_tree()
            .feature_of_sketch(downstream)
            .ok_or("the downstream sketch has a feature node")?;
        assert!(
            document
                .feature_tree()
                .edges()
                .contains(&FeatureEdge::FaceToSketch {
                    face_feature: body,
                    sketch: sketch_feature,
                }),
            "binding a sketch to a face records the dependency edge",
        );

        let ron = crate::io::ron_io::to_string(document.header())?;
        let back = crate::io::ron_io::from_str::<DocumentHeader>(&ron)?;
        assert_eq!(
            back.sketch_planes.get(&downstream).copied(),
            Some(SketchPlaneRef::Face(face))
        );

        document.remove_sketch(downstream);
        assert_eq!(document.sketch_plane_binding(downstream), None);

        let (mut document, _base_sketch) = doc_with_sketch();
        let downstream = document.allocate_sketch();
        document.insert_sketch(downstream, "Sketch2".to_owned(), Sketch3::new());
        let extrude = document.commit_extrude(sample_blind_extrude(downstream));
        let body = document
            .feature_tree()
            .feature_of_extrude(extrude)
            .ok_or("the downstream extrude has a feature node")?;
        assert_eq!(
            document.bind_sketch_to_face(downstream, sample_face_ref(body)),
            Err(BindSketchToFaceError::WouldCycle(body)),
            "binding a sketch onto a face of its own descendant must not form a cycle",
        );
        assert_eq!(
            document.sketch_plane_binding(downstream),
            None,
            "a rejected bind records no plane",
        );
        assert!(
            !document
                .feature_tree()
                .edges()
                .iter()
                .any(|edge| matches!(edge, FeatureEdge::FaceToSketch { .. })),
            "a rejected bind records no edge",
        );

        let (mut document, base_sketch) = doc_with_sketch();
        let extrude = document.commit_extrude(sample_blind_extrude(base_sketch));
        let body = document
            .feature_tree()
            .feature_of_extrude(extrude)
            .ok_or("the extrude has a feature node")?;
        let stranger = document.allocate_sketch();
        assert_eq!(
            document.bind_sketch_to_face(stranger, sample_face_ref(body)),
            Err(BindSketchToFaceError::UnknownSketch(stranger)),
            "a sketch with no feature node cannot carry a plane binding",
        );
        Ok(())
    }

    #[test]
    fn removing_a_feature_prunes_history_state() -> Result<(), Box<dyn std::error::Error>> {
        let (mut document, sketch) = doc_with_sketch();
        let extrude = document.commit_extrude(sample_blind_extrude(sketch));
        let feature = document
            .feature_tree()
            .feature_of_extrude(extrude)
            .ok_or("the extrude has a feature node")?;
        document.suppress(feature);
        document.roll_to_here(feature);
        assert!(document.suppressed().contains(&feature));
        document.remove_extrude(extrude);
        assert!(
            !document.suppressed().contains(&feature),
            "deleting a feature drops it from the suppressed set",
        );
        assert_eq!(
            document.rollback(),
            RollbackMarker::AtEnd,
            "deleting the feature the bar sits on resets the rollback marker",
        );

        let (mut document, sketch) = doc_with_sketch();
        let extrude = document.commit_extrude(sample_blind_extrude(sketch));
        let sketch_feature = document
            .feature_tree()
            .feature_of_sketch(sketch)
            .ok_or("the sketch has a feature node")?;
        let extrude_feature = document
            .feature_tree()
            .feature_of_extrude(extrude)
            .ok_or("the extrude has a feature node")?;
        document.suppress(extrude_feature);
        document.roll_to_here(sketch_feature);
        document.remove_sketch(sketch);
        assert!(
            document.suppressed().is_empty(),
            "removing a sketch prunes the suppressed set of its cascaded extrude",
        );
        assert_eq!(
            document.rollback(),
            RollbackMarker::AtEnd,
            "removing the sketch the bar sits on resets the rollback marker",
        );
        Ok(())
    }

    #[test]
    fn rollback_bar_behaviors() -> Result<(), Box<dyn std::error::Error>> {
        let (mut document, first) = doc_with_sketch();
        let second = document.allocate_sketch();
        document.insert_sketch(second, "Sketch2".to_owned(), Sketch3::new());
        let first_feature = document
            .feature_tree()
            .feature_of_sketch(first)
            .ok_or("the first sketch resolves to a feature node")?;
        let second_feature = document
            .feature_tree()
            .feature_of_sketch(second)
            .ok_or("the second sketch resolves to a feature node")?;
        document.roll_to_here(first_feature);
        document.remove_sketch(first);
        assert_eq!(
            document.rollback(),
            RollbackMarker::Above(second_feature),
            "deleting the bar feature keeps the remaining tail rolled back",
        );

        let (mut document, sketch_feature, extrude_feature) = sketch_extrude_doc();
        let second = document.allocate_sketch();
        document.insert_sketch(second, "Sketch2".to_owned(), Sketch3::new());
        let second_feature = document
            .feature_tree()
            .feature_of_sketch(second)
            .ok_or("the second sketch resolves to a feature node")?;
        document.roll_to_here(extrude_feature);
        assert_eq!(document.rollback(), RollbackMarker::Above(extrude_feature));
        assert!(
            !document.is_rolled_back(sketch_feature),
            "a feature above the bar stays live and selectable",
        );
        assert!(
            document.is_rolled_back(extrude_feature),
            "the feature the bar sits on is rolled back",
        );
        assert!(
            document.is_rolled_back(second_feature),
            "every feature after the bar is rolled back, the contiguous-tail rule",
        );

        let (mut document, sketch_feature, extrude_feature) = sketch_extrude_doc();
        assert_eq!(document.rollback(), RollbackMarker::AtEnd);
        document.roll_to_previous();
        assert_eq!(
            document.rollback(),
            RollbackMarker::Above(extrude_feature),
            "from the end the bar steps above the last feature",
        );
        document.roll_to_previous();
        assert_eq!(
            document.rollback(),
            RollbackMarker::Above(sketch_feature),
            "another step rolls the prior feature back too",
        );
        document.roll_to_previous();
        assert_eq!(
            document.rollback(),
            RollbackMarker::Above(sketch_feature),
            "the bar never climbs above the first feature",
        );
        document.roll_to_end();
        assert_eq!(
            document.rollback(),
            RollbackMarker::AtEnd,
            "roll to end clears the marker",
        );

        let (mut document, _sketch_feature, _extrude_feature) = sketch_extrude_doc();
        let (origin, _) = document
            .feature_tree()
            .iter()
            .next()
            .ok_or("the seeded tree opens with the origin datum")?;
        document.roll_to_here(origin);
        assert_eq!(
            document.rollback(),
            RollbackMarker::AtEnd,
            "a datum can never carry the rollback bar",
        );

        let (mut document, _sketch_feature, extrude_feature) = sketch_extrude_doc();
        document.roll_to_here(extrude_feature);
        let inserted = document.allocate_sketch();
        document.insert_sketch(inserted, "Inserted".to_owned(), Sketch3::new());
        let inserted_feature = document
            .feature_tree()
            .feature_of_sketch(inserted)
            .ok_or("the inserted sketch resolves to a feature node")?;
        assert!(
            !document.is_rolled_back(inserted_feature),
            "a feature inserted while rolled back lands above the bar, active",
        );
        assert_eq!(
            document.rollback(),
            RollbackMarker::Above(extrude_feature),
            "inserting at the bar leaves the marker where it was",
        );
        let order: Vec<FeatureId> = document.feature_tree().iter().map(|(id, _)| id).collect();
        let position = |target: FeatureId| order.iter().position(|other| *other == target);
        assert!(
            position(inserted_feature) < position(extrude_feature),
            "the inserted feature sits immediately above the bar feature, not at the tree end",
        );

        let (mut document, sketch) = doc_with_sketch();
        let sketch_feature = document
            .feature_tree()
            .feature_of_sketch(sketch)
            .ok_or("the sketch resolves to a feature node")?;
        document.roll_to_here(sketch_feature);
        let extrude = document.commit_extrude(sample_blind_extrude(sketch));
        let extrude_feature = document
            .feature_tree()
            .feature_of_extrude(extrude)
            .ok_or("the committed extrude resolves to a feature node")?;
        assert!(
            document.feature_tree().order_violation().is_none(),
            "an extrude committed onto a rolled-back sketch must not precede its parent",
        );
        let order: Vec<FeatureId> = document.feature_tree().iter().map(|(id, _)| id).collect();
        let position = |target: FeatureId| order.iter().position(|other| *other == target);
        assert!(
            position(sketch_feature) < position(extrude_feature),
            "placing at the bar still lands the child after the parent it depends on",
        );

        let (mut document, sketch) = doc_with_sketch();
        let first = document.commit_extrude(sample_blind_extrude(sketch));
        let _middle = document.commit_extrude(sample_blind_extrude(sketch));
        let last = document.commit_extrude(sample_blind_extrude(sketch));
        let bar = document
            .feature_tree()
            .feature_of_extrude(last)
            .ok_or("the last extrude resolves to a feature node")?;
        document.roll_to_here(bar);
        let before: Vec<FeatureId> = document.feature_tree().iter().map(|(id, _)| id).collect();
        document.insert_extrude(first, sample_blind_extrude(sketch));
        let after: Vec<FeatureId> = document.feature_tree().iter().map(|(id, _)| id).collect();
        assert_eq!(
            before, after,
            "re-inserting an existing feature while rolled back must not relocate it to the bar",
        );
        Ok(())
    }

    #[test]
    fn suppression_cascades_and_restores() -> Result<(), Box<dyn std::error::Error>> {
        let (mut document, _sketch) = doc_with_sketch();
        let datum = document
            .feature_tree()
            .iter()
            .find_map(|(id, _)| {
                document
                    .feature_tree()
                    .is_seeded_reference(id)
                    .then_some(id)
            })
            .ok_or("the seeded tree carries datum features")?;
        document.suppress(datum);
        assert!(
            document.suppressed().is_empty(),
            "a datum never enters the suppressed set",
        );
        assert_eq!(document.suppression_state(datum), SuppressionState::Active);

        let (mut document, sketch) = doc_with_sketch();
        let sketch_feature = document
            .feature_tree()
            .feature_of_sketch(sketch)
            .ok_or("the sketch resolves to a feature node")?;
        document.suppress(sketch_feature);
        let extrude = document.commit_extrude(sample_blind_extrude(sketch));
        let extrude_feature = document
            .feature_tree()
            .feature_of_extrude(extrude)
            .ok_or("the extrude resolves to a feature node")?;
        assert_eq!(
            document.suppression_state(extrude_feature),
            SuppressionState::Suppressed,
            "a dependent of a suppressed feature reads suppressed even when added later",
        );
        assert!(
            !document.suppressed().contains(&extrude_feature),
            "the late dependent need not enter the persisted set to read suppressed",
        );

        let (mut document, sketch_feature, extrude_feature) = sketch_extrude_doc();
        document.suppress(sketch_feature);
        assert_eq!(
            document.suppression_state(extrude_feature),
            SuppressionState::Suppressed,
        );
        document.unsuppress(sketch_feature);
        assert_eq!(
            document.suppression_state(sketch_feature),
            SuppressionState::Active,
        );
        assert_eq!(
            document.suppression_state(extrude_feature),
            SuppressionState::Active,
            "unsuppressing a feature restores the feature and its children",
        );

        let (mut document, sketch_feature, extrude_feature) = sketch_extrude_doc();
        document.suppress(sketch_feature);
        assert_eq!(
            document.suppression_state(sketch_feature),
            SuppressionState::Suppressed,
        );
        assert_eq!(
            document.suppression_state(extrude_feature),
            SuppressionState::Suppressed,
            "suppressing a feature suppresses its children",
        );

        let (mut document, sketch) = doc_with_sketch();
        let left = document.commit_extrude(sample_blind_extrude(sketch));
        let right = document.commit_extrude(sample_blind_extrude(sketch));
        let sketch_feature = document
            .feature_tree()
            .feature_of_sketch(sketch)
            .ok_or("the sketch resolves to a feature node")?;
        let left_feature = document
            .feature_tree()
            .feature_of_extrude(left)
            .ok_or("the left extrude resolves to a feature node")?;
        let right_feature = document
            .feature_tree()
            .feature_of_extrude(right)
            .ok_or("the right extrude resolves to a feature node")?;
        document.suppress(sketch_feature);
        assert_eq!(
            document.suppression_state(left_feature),
            SuppressionState::Suppressed,
        );
        assert_eq!(
            document.suppression_state(right_feature),
            SuppressionState::Suppressed,
        );
        document.unsuppress(left_feature);
        assert_eq!(
            document.suppression_state(left_feature),
            SuppressionState::Active,
            "the unsuppressed child comes back",
        );
        assert_eq!(
            document.suppression_state(sketch_feature),
            SuppressionState::Active,
            "unsuppressing a child restores its parent",
        );
        assert_eq!(
            document.suppression_state(right_feature),
            SuppressionState::Suppressed,
            "the parent's other child stays suppressed",
        );
        Ok(())
    }

    fn sketch_extrude_doc() -> (Document, FeatureId, FeatureId) {
        let (mut document, sketch) = doc_with_sketch();
        let extrude = document.commit_extrude(sample_blind_extrude(sketch));
        let (Some(sketch_feature), Some(extrude_feature)) = (
            document.feature_tree().feature_of_sketch(sketch),
            document.feature_tree().feature_of_extrude(extrude),
        ) else {
            panic!("the sketch and extrude resolve to feature nodes");
        };
        (document, sketch_feature, extrude_feature)
    }
}
