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

use bone_kernel::{BrepEdge, BrepFace, BrepSolid, BrepVertex, ExtrudeFeature};
use bone_types::{
    BodyId, DocumentId, ExtrudeId, FaceRef, FeatureGeneration, FeatureId, RollbackMarker,
    SchemaHeader, SketchId, SuppressionState,
};
use serde::{Deserialize, Serialize};
use slotmap::KeyData;

use crate::Sketch;

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 BindSketchToFaceError {
    #[error("sketch {0:?} not found")]
    UnknownSketch(SketchId),
    #[error("binding sketch onto feature {0:?} would make the feature graph cyclic")]
    WouldCycle(FeatureId),
}

#[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, FaceRef>,
    #[serde(default)]
    pub sketch_high_water: FeatureGeneration,
    #[serde(default)]
    pub extrude_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>,
}

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,
            body_high_water: FeatureGeneration::START,
            rollback: RollbackMarker::AtEnd,
            suppressed: BTreeSet::new(),
            extrudes: BTreeMap::new(),
            extrude_labels: BTreeMap::new(),
        }
    }

    pub(crate) fn rebuild_edges(&mut self) {
        let bindings: Vec<(FeatureId, FeatureId)> = self
            .sketch_planes
            .iter()
            .filter_map(|(sketch_id, face)| {
                let sketch = self.feature_tree.feature_of_sketch(*sketch_id)?;
                Some((face.label.feature, sketch))
            })
            .collect();
        self.feature_tree.rebuild_edges(
            self.extrudes.iter().map(|(id, feature)| (*id, feature)),
            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()));
    }

    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)
    }
}

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

impl SketchFile {
    #[must_use]
    pub fn new(sketch: Sketch) -> 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)]
pub struct Document {
    header: DocumentHeader,
    sketches: BTreeMap<SketchId, Sketch>,
    bodies: BTreeMap<BodyId, ImportedSolid>,
}

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, Sketch>,
        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();
        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 sketch(&self, id: SketchId) -> Option<&Sketch> {
        self.sketches.get(&id)
    }

    #[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, &Sketch)> + '_ {
        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, Sketch> {
        &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: Sketch) {
        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);
            }
        });
    }

    pub fn replace_sketch(&mut self, id: SketchId, sketch: Sketch) -> Option<Sketch> {
        self.sketches.insert(id, sketch)
    }

    pub fn bind_sketch_to_face(
        &mut self,
        sketch: SketchId,
        face: FaceRef,
    ) -> Result<(), BindSketchToFaceError> {
        let sketch_feature = self
            .header
            .feature_tree
            .feature_of_sketch(sketch)
            .ok_or(BindSketchToFaceError::UnknownSketch(sketch))?;
        let target = face.label.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, face);
        self.header
            .feature_tree
            .bind_face_to_sketch(target, sketch_feature);
        Ok(())
    }

    #[must_use]
    pub fn sketch_plane_binding(&self, sketch: SketchId) -> Option<FaceRef> {
        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_datum(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(())
    }

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

    pub fn remove_sketch(&mut self, id: SketchId) -> Option<Sketch> {
        let dependents: Vec<ExtrudeId> = self.extrudes_of_sketch(id).collect();
        dependents.iter().for_each(|extrude| {
            self.remove_extrude(*extrude);
        });
        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);
        }
        self.sketches.remove(&id)
    }

    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<&Sketch> {
        match self.header.feature_tree.node(feature)? {
            FeatureNode::Sketch(id) => self.sketches.get(&id),
            FeatureNode::Origin
            | FeatureNode::PrincipalPlane(_)
            | FeatureNode::Extrude(_)
            | 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::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(_) => 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 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, Sketch, SketchId,
    };
    use bone_types::{
        DocumentId, ExtrudeId, FaceFingerprint, FaceLabel, FaceRef, FaceRole, FeatureId, Plane3,
        Point3, RollbackMarker, SketchPlaneBasis, SuppressionState, Tolerance, UnitVec3,
    };

    fn xy_basis() -> SketchPlaneBasis {
        let Ok(basis) = SketchPlaneBasis::new(
            Point3::origin(),
            UnitVec3::x_axis(),
            UnitVec3::y_axis(),
            Tolerance::new(1e-9),
        ) else {
            panic!("xy plane basis is orthogonal");
        };
        basis
    }

    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(), Sketch::new(xy_basis()));
        (document, id)
    }

    #[test]
    fn reorder_rejects_moving_extrude_before_its_source_sketch() {
        let (mut document, sketch) = doc_with_sketch();
        let extrude = document.commit_extrude(sample_blind_extrude(sketch));
        let tree = document.feature_tree();
        let (Some(sketch_feature), Some(extrude_feature)) = (
            tree.feature_of_sketch(sketch),
            tree.feature_of_extrude(extrude),
        ) else {
            panic!("the chain resolves to feature ids");
        };
        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));
    }

    #[test]
    fn reorder_moves_an_independent_feature_and_keeps_the_dag_valid() {
        let mut document = Document::new(DocumentId::default(), "doc".to_owned());
        let s1 = SketchId::default();
        document.insert_sketch(s1, "Sketch1".to_owned(), Sketch::new(xy_basis()));
        let e1 = document.commit_extrude(sample_blind_extrude(s1));
        let s2 = document.allocate_sketch();
        document.insert_sketch(s2, "Sketch2".to_owned(), Sketch::new(xy_basis()));
        let e2 = document.commit_extrude(sample_blind_extrude(s2));
        let tree = document.feature_tree();
        let (Some(f1), Some(f2)) = (tree.feature_of_extrude(e1), tree.feature_of_extrude(e2))
        else {
            panic!("both extrudes resolve to feature ids");
        };
        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",
        );
    }

    #[test]
    fn rename_sketch_updates_label() {
        let (mut document, id) = doc_with_sketch();
        let Ok(()) = document.rename_sketch(id, "Profile") else {
            panic!("rename should accept non-empty label");
        };
        assert_eq!(document.sketch_label(id), Some("Profile"));
    }

    #[test]
    fn rename_sketch_trims_whitespace() {
        let (mut document, id) = doc_with_sketch();
        let Ok(()) = document.rename_sketch(id, "  Base \n") else {
            panic!("rename should trim and accept");
        };
        assert_eq!(document.sketch_label(id), Some("Base"));
    }

    #[test]
    fn rename_sketch_rejects_empty_label() {
        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"));
    }

    #[test]
    fn insert_extrude_resolves_through_feature_node() {
        let (mut document, sketch) = doc_with_sketch();
        let extrude = ExtrudeId::default();
        let feature = sample_blind_extrude(sketch);
        document.insert_extrude(extrude, feature);

        let Some(extrude_feature) = document.feature_tree().feature_of_extrude(extrude) else {
            panic!("extrude node present after insert");
        };
        assert_eq!(document.extrude_of_feature(extrude_feature), Some(&feature));
        assert_eq!(document.sketch_of_feature(extrude_feature), None);
    }

    #[test]
    fn rename_sketch_rejects_unknown_id() {
        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));
    }

    #[test]
    fn remove_extrude_resolves_to_none_afterward() {
        let (mut document, sketch) = doc_with_sketch();
        let extrude = ExtrudeId::default();
        document.insert_extrude(extrude, sample_blind_extrude(sketch));
        let Some(feature) = document.feature_tree().feature_of_extrude(extrude) else {
            panic!("extrude node present after insert");
        };
        assert!(document.extrude_of_feature(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);
    }

    #[test]
    fn remove_sketch_cascades_dependent_extrude() {
        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());
    }

    #[test]
    fn insert_sketch_backlinks_pending_extrude() {
        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(), Sketch::new(xy_basis()));

        let Some(sketch_feature) = document.feature_tree().feature_of_sketch(sketch) else {
            panic!("sketch node present after insert");
        };
        let Some(extrude_feature) = document.feature_tree().feature_of_extrude(extrude) else {
            panic!("extrude node present");
        };
        assert_eq!(
            document.feature_tree().edges(),
            &[super::FeatureEdge::SketchToExtrude {
                sketch: sketch_feature,
                extrude: extrude_feature,
            }]
        );
    }

    #[test]
    fn commit_extrude_assigns_incrementing_default_labels() {
        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"));
    }

    #[test]
    fn rename_extrude_updates_and_trims_label() {
        let (mut document, sketch) = doc_with_sketch();
        let id = document.commit_extrude(sample_blind_extrude(sketch));
        let Ok(()) = document.rename_extrude(id, "  Boss \n") else {
            panic!("rename should trim and accept");
        };
        assert_eq!(document.extrude_label(id), Some("Boss"));
    }

    #[test]
    fn rename_extrude_rejects_empty_and_unknown() {
        let (mut document, sketch) = doc_with_sketch();
        let id = document.commit_extrude(sample_blind_extrude(sketch));
        assert_eq!(
            document.rename_extrude(id, "   "),
            Err(RenameExtrudeError::EmptyLabel)
        );
        assert_eq!(document.extrude_label(id), Some("Extrude1"));
        let stranger = ExtrudeId::default();
        assert_eq!(
            document.rename_extrude(stranger, "Boss"),
            Err(RenameExtrudeError::UnknownExtrude(stranger))
        );
    }

    #[test]
    fn remove_extrude_drops_label() {
        let (mut document, sketch) = doc_with_sketch();
        let id = document.commit_extrude(sample_blind_extrude(sketch));
        document.remove_extrude(id);
        assert_eq!(document.extrude_label(id), None);
    }

    #[test]
    fn commit_after_removing_earlier_extrude_keeps_labels_unique() {
        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",
        );
    }

    #[test]
    fn commit_after_removing_top_extrude_does_not_reuse_id() {
        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);
    }

    #[test]
    fn allocate_sketch_walks_above_removed_ids() {
        let mut document = Document::new(DocumentId::default(), "doc".to_owned());
        let first = document.allocate_sketch();
        document.insert_sketch(first, "Sketch1".to_owned(), Sketch::new(xy_basis()));
        let second = document.allocate_sketch();
        document.insert_sketch(second, "Sketch2".to_owned(), Sketch::new(xy_basis()));
        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_a_face_to_sketch_edge() {
        let (mut document, base_sketch) = doc_with_sketch();
        let extrude = document.commit_extrude(sample_blind_extrude(base_sketch));
        let Some(body) = document.feature_tree().feature_of_extrude(extrude) else {
            panic!("the extrude has a feature node");
        };
        let downstream = document.allocate_sketch();
        document.insert_sketch(downstream, "Sketch2".to_owned(), Sketch::new(xy_basis()));
        let face = sample_face_ref(body);
        let Ok(()) = document.bind_sketch_to_face(downstream, face) else {
            panic!("the face binding is acyclic");
        };

        assert_eq!(document.sketch_plane_binding(downstream), Some(face));
        let Some(sketch_feature) = document.feature_tree().feature_of_sketch(downstream) else {
            panic!("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",
        );
    }

    #[test]
    fn sketch_plane_binding_round_trips_through_the_header() {
        let (mut document, base_sketch) = doc_with_sketch();
        let extrude = document.commit_extrude(sample_blind_extrude(base_sketch));
        let Some(body) = document.feature_tree().feature_of_extrude(extrude) else {
            panic!("the extrude has a feature node");
        };
        let downstream = document.allocate_sketch();
        document.insert_sketch(downstream, "Sketch2".to_owned(), Sketch::new(xy_basis()));
        let face = sample_face_ref(body);
        let Ok(()) = document.bind_sketch_to_face(downstream, face) else {
            panic!("the face binding is acyclic");
        };

        let Ok(ron) = crate::io::ron_io::to_string(document.header()) else {
            panic!("the header serializes");
        };
        let Ok(back) = crate::io::ron_io::from_str::<DocumentHeader>(&ron) else {
            panic!("the header deserializes");
        };
        assert_eq!(back.sketch_planes.get(&downstream).copied(), Some(face));
    }

    #[test]
    fn removing_a_sketch_drops_its_plane_binding() {
        let (mut document, base_sketch) = doc_with_sketch();
        let extrude = document.commit_extrude(sample_blind_extrude(base_sketch));
        let Some(body) = document.feature_tree().feature_of_extrude(extrude) else {
            panic!("the extrude has a feature node");
        };
        let downstream = document.allocate_sketch();
        document.insert_sketch(downstream, "Sketch2".to_owned(), Sketch::new(xy_basis()));
        let Ok(()) = document.bind_sketch_to_face(downstream, sample_face_ref(body)) else {
            panic!("the face binding is acyclic");
        };

        document.remove_sketch(downstream);

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

    #[test]
    fn binding_a_sketch_onto_its_own_descendant_is_rejected_as_cyclic() {
        let (mut document, _base_sketch) = doc_with_sketch();
        let downstream = document.allocate_sketch();
        document.insert_sketch(downstream, "Sketch2".to_owned(), Sketch::new(xy_basis()));
        let extrude = document.commit_extrude(sample_blind_extrude(downstream));
        let Some(body) = document.feature_tree().feature_of_extrude(extrude) else {
            panic!("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",
        );
    }

    #[test]
    fn binding_a_sketch_absent_from_the_tree_is_rejected() {
        let (mut document, base_sketch) = doc_with_sketch();
        let extrude = document.commit_extrude(sample_blind_extrude(base_sketch));
        let Some(body) = document.feature_tree().feature_of_extrude(extrude) else {
            panic!("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",
        );
    }

    #[test]
    fn removing_a_feature_prunes_its_history_state() {
        let (mut document, sketch) = doc_with_sketch();
        let extrude = document.commit_extrude(sample_blind_extrude(sketch));
        let Some(feature) = document.feature_tree().feature_of_extrude(extrude) else {
            panic!("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",
        );
    }

    #[test]
    fn removing_a_sketch_prunes_history_state_of_cascaded_features() {
        let (mut document, sketch) = doc_with_sketch();
        let extrude = document.commit_extrude(sample_blind_extrude(sketch));
        let Some(sketch_feature) = document.feature_tree().feature_of_sketch(sketch) else {
            panic!("the sketch has a feature node");
        };
        let Some(extrude_feature) = document.feature_tree().feature_of_extrude(extrude) else {
            panic!("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",
        );
    }

    #[test]
    fn deleting_the_bar_feature_moves_the_bar_to_its_successor() {
        let (mut document, first) = doc_with_sketch();
        let second = document.allocate_sketch();
        document.insert_sketch(second, "Sketch2".to_owned(), Sketch::new(xy_basis()));
        let (Some(first_feature), Some(second_feature)) = (
            document.feature_tree().feature_of_sketch(first),
            document.feature_tree().feature_of_sketch(second),
        ) else {
            panic!("both sketches resolve to feature nodes");
        };
        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",
        );
    }

    #[test]
    fn suppressing_a_datum_is_ignored() {
        let (mut document, _sketch) = doc_with_sketch();
        let Some(datum) = document
            .feature_tree()
            .iter()
            .find_map(|(id, _)| document.feature_tree().is_datum(id).then_some(id))
        else {
            panic!("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);
    }

    #[test]
    fn a_dependent_added_after_suppression_reads_suppressed() {
        let (mut document, sketch) = doc_with_sketch();
        let Some(sketch_feature) = document.feature_tree().feature_of_sketch(sketch) else {
            panic!("the sketch resolves to a feature node");
        };
        document.suppress(sketch_feature);

        let extrude = document.commit_extrude(sample_blind_extrude(sketch));
        let Some(extrude_feature) = document.feature_tree().feature_of_extrude(extrude) else {
            panic!("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",
        );
    }

    #[test]
    fn reinsert_existing_extrude_preserves_renamed_label() {
        let (mut document, sketch) = doc_with_sketch();
        let id = document.commit_extrude(sample_blind_extrude(sketch));
        let Ok(()) = document.rename_extrude(id, "Boss") else {
            panic!("rename accepts");
        };
        document.insert_extrude(id, sample_blind_extrude(sketch));
        assert_eq!(document.extrude_label(id), Some("Boss"));
    }

    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)
    }

    #[test]
    fn roll_to_here_marks_the_contiguous_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(), Sketch::new(xy_basis()));
        let Some(second_feature) = document.feature_tree().feature_of_sketch(second) else {
            panic!("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",
        );
    }

    #[test]
    fn roll_to_previous_walks_up_one_feature_then_clamps() {
        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",
        );
    }

    #[test]
    fn roll_to_here_ignores_a_datum_feature() {
        let (mut document, _sketch_feature, _extrude_feature) = sketch_extrude_doc();
        let Some((origin, _)) = document.feature_tree().iter().next() else {
            panic!("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",
        );
    }

    #[test]
    fn a_feature_inserted_while_rolled_back_lands_at_the_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(), Sketch::new(xy_basis()));
        let Some(inserted_feature) = document.feature_tree().feature_of_sketch(inserted) else {
            panic!("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",
        );
    }

    #[test]
    fn inserting_an_extrude_on_a_rolled_back_sketch_keeps_dependency_order() {
        let (mut document, sketch) = doc_with_sketch();
        let Some(sketch_feature) = document.feature_tree().feature_of_sketch(sketch) else {
            panic!("the sketch resolves to a feature node");
        };
        document.roll_to_here(sketch_feature);

        let extrude = document.commit_extrude(sample_blind_extrude(sketch));
        let Some(extrude_feature) = document.feature_tree().feature_of_extrude(extrude) else {
            panic!("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",
        );
    }

    #[test]
    fn editing_an_above_bar_feature_does_not_reorder_history() {
        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 Some(bar) = document.feature_tree().feature_of_extrude(last) else {
            panic!("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",
        );
    }

    #[test]
    fn unsuppressing_a_parent_restores_its_children() {
        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",
        );
    }

    #[test]
    fn suppressing_a_feature_cascades_to_its_dependents() {
        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",
        );
    }

    #[test]
    fn unsuppressing_a_child_restores_its_parent_but_not_its_sibling() {
        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 (Some(sketch_feature), Some(left_feature), Some(right_feature)) = (
            document.feature_tree().feature_of_sketch(sketch),
            document.feature_tree().feature_of_extrude(left),
            document.feature_tree().feature_of_extrude(right),
        ) else {
            panic!("the sketch and both extrudes resolve to feature nodes");
        };

        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",
        );
    }
}
