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

use bone_types::{
    Length, Point2, SketchDimensionId, SketchEntityId, SketchParameterId, SketchPlaneBasis,
    SketchRelationId,
};
use serde::{Deserialize, Serialize};
use slotmap::SlotMap;

pub mod dimension;
pub mod edit;
pub mod entity;
pub mod parameter;
pub mod relation;
pub mod solve;

pub use bone_solver::SolverError;
pub use dimension::{
    DimensionKind, DimensionRefs, DimensionValue, DimensionValueMismatch, SketchDimension,
};
pub use edit::{EditOutcome, SketchEdit};
pub use entity::{
    ArcData, CircleData, EntityRefs, LineData, PointData, SketchEntity, SketchEntityKind,
};
pub use parameter::SketchParameter;
pub use relation::{RelationRefs, SketchRelation};
pub use solve::{DragError, Mapping as SketchSolveMapping, SketchDofReport, SketchStatusReport};

#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, Hash)]
pub struct SketchVersion(u64);

impl SketchVersion {
    #[must_use]
    pub const fn value(self) -> u64 {
        self.0
    }

    #[must_use]
    fn bump(self) -> Self {
        Self(self.0.wrapping_add(1))
    }
}

type EntityMap = SlotMap<SketchEntityId, SketchEntity>;
type RelationMap = SlotMap<SketchRelationId, SketchRelation>;
type DimensionMap = SlotMap<SketchDimensionId, SketchDimension>;
type ParameterMap = SlotMap<SketchParameterId, SketchParameter>;

#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Sketch {
    plane: SketchPlaneBasis,
    entities: Arc<EntityMap>,
    entity_order: Arc<Vec<SketchEntityId>>,
    relations: Arc<RelationMap>,
    relation_order: Arc<Vec<SketchRelationId>>,
    dimensions: Arc<DimensionMap>,
    dimension_order: Arc<Vec<SketchDimensionId>>,
    parameters: Arc<ParameterMap>,
    parameter_order: Arc<Vec<SketchParameterId>>,
    #[serde(skip)]
    version: SketchVersion,
}

impl Sketch {
    #[must_use]
    pub fn new(plane: SketchPlaneBasis) -> Self {
        Self {
            plane,
            entities: Arc::new(SlotMap::with_key()),
            entity_order: Arc::new(Vec::new()),
            relations: Arc::new(SlotMap::with_key()),
            relation_order: Arc::new(Vec::new()),
            dimensions: Arc::new(SlotMap::with_key()),
            dimension_order: Arc::new(Vec::new()),
            parameters: Arc::new(SlotMap::with_key()),
            parameter_order: Arc::new(Vec::new()),
            version: SketchVersion::default(),
        }
    }

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

    #[must_use]
    pub fn entities(&self) -> &EntityMap {
        &self.entities
    }

    #[must_use]
    pub fn relations(&self) -> &RelationMap {
        &self.relations
    }

    #[must_use]
    pub fn dimensions(&self) -> &DimensionMap {
        &self.dimensions
    }

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

    #[must_use]
    pub fn entity_order(&self) -> &[SketchEntityId] {
        &self.entity_order
    }

    #[must_use]
    pub fn relation_order(&self) -> &[SketchRelationId] {
        &self.relation_order
    }

    #[must_use]
    pub fn dimension_order(&self) -> &[SketchDimensionId] {
        &self.dimension_order
    }

    #[must_use]
    pub fn parameter_order(&self) -> &[SketchParameterId] {
        &self.parameter_order
    }

    #[must_use]
    pub fn version(&self) -> SketchVersion {
        self.version
    }

    #[must_use]
    pub(crate) fn with_bumped_version(mut self) -> Self {
        self.version = self.version.bump();
        self
    }

    fn entries_equal<K, V>(order: &[K], lhs: &SlotMap<K, V>, rhs: &SlotMap<K, V>) -> bool
    where
        K: slotmap::Key,
        V: PartialEq,
    {
        order.iter().all(|id| lhs.get(*id) == rhs.get(*id))
    }

    pub fn validate(&self) -> Result<(), SketchEditError> {
        ensure_order_matches("entities", &self.entity_order, &self.entities)?;
        ensure_order_matches("relations", &self.relation_order, &self.relations)?;
        ensure_order_matches("dimensions", &self.dimension_order, &self.dimensions)?;
        ensure_order_matches("parameters", &self.parameter_order, &self.parameters)?;

        self.entity_order.iter().copied().try_for_each(|id| {
            let entity = *self.require_entity(id)?;
            validate_entity_shape(entity)?;
            entity
                .references()
                .into_iter()
                .try_for_each(|reference| self.require_point(reference))
        })?;

        self.relation_order.iter().copied().try_for_each(|id| {
            let rel = *self
                .relations
                .get(id)
                .ok_or(SketchEditError::RelationNotFound(id))?;
            self.validate_relation(rel)
        })?;

        self.dimension_order.iter().copied().try_for_each(|id| {
            let dim = *self
                .dimensions
                .get(id)
                .ok_or(SketchEditError::DimensionNotFound(id))?;
            self.validate_dimension(dim)
        })
    }

    pub fn apply(self, edit: SketchEdit) -> Result<(Self, EditOutcome), SketchEditError> {
        let result = match edit {
            SketchEdit::AddEntity(entity) => {
                entity
                    .references()
                    .into_iter()
                    .try_for_each(|ref_id| self.require_point(ref_id))?;
                validate_entity_shape(entity)?;
                Ok(self.add_entity(entity))
            }
            SketchEdit::AddRelation(rel) => self.add_relation(rel),
            SketchEdit::AddDimension(dim) => self.add_dimension(dim),
            SketchEdit::AddParameter(p) => Ok(self.add_parameter(p)),
            SketchEdit::DeleteEntity(id) => self.delete_entity(id),
            SketchEdit::DeleteRelation(id) => self.delete_relation(id),
            SketchEdit::DeleteDimension(id) => self.delete_dimension(id),
            SketchEdit::DeleteParameter(id) => self.delete_parameter(id),
            SketchEdit::MovePoint { id, position } => self.move_point(id, position),
            SketchEdit::SetConstruction {
                id,
                for_construction,
            } => self.set_construction(id, for_construction),
            SketchEdit::UpdateDimensionValue { id, value } => self.update_dimension(id, value),
        };
        if let Ok((ref sketch, _)) = result {
            sketch.assert_invariants();
        }
        result.map(|(sketch, outcome)| (sketch.with_bumped_version(), outcome))
    }

    fn assert_invariants(&self) {
        debug_assert_eq!(self.entity_order.len(), self.entities.len());
        debug_assert_eq!(self.relation_order.len(), self.relations.len());
        debug_assert_eq!(self.dimension_order.len(), self.dimensions.len());
        debug_assert_eq!(self.parameter_order.len(), self.parameters.len());
        debug_assert!(
            self.entity_order
                .iter()
                .all(|id| self.entities.contains_key(*id))
        );
        debug_assert!(
            self.relation_order
                .iter()
                .all(|id| self.relations.contains_key(*id))
        );
        debug_assert!(
            self.dimension_order
                .iter()
                .all(|id| self.dimensions.contains_key(*id))
        );
        debug_assert!(
            self.parameter_order
                .iter()
                .all(|id| self.parameters.contains_key(*id))
        );
    }

    pub fn apply_all<I>(&self, edits: I) -> Result<(Self, Vec<EditOutcome>), SketchEditError>
    where
        I: IntoIterator<Item = SketchEdit>,
    {
        edits.into_iter().try_fold(
            (self.clone(), Vec::new()),
            |(sketch, mut outcomes), edit| {
                let (next, outcome) = sketch.apply(edit)?;
                outcomes.push(outcome);
                Ok((next, outcomes))
            },
        )
    }

    fn require_entity(&self, id: SketchEntityId) -> Result<&SketchEntity, SketchEditError> {
        self.entities
            .get(id)
            .ok_or(SketchEditError::EntityNotFound(id))
    }

    fn require_point(&self, id: SketchEntityId) -> Result<(), SketchEditError> {
        if self.require_entity(id)?.is_point() {
            Ok(())
        } else {
            Err(SketchEditError::ExpectedPoint(id))
        }
    }

    fn kind_of(&self, id: SketchEntityId) -> Result<SketchEntityKind, SketchEditError> {
        self.require_entity(id).map(SketchEntity::kind)
    }

    fn validate_relation(&self, rel: SketchRelation) -> Result<(), SketchEditError> {
        use SketchEntityKind as K;
        if let Some((a, b)) = rel.pair()
            && a == b
        {
            return Err(SketchEditError::SelfReferencingRelation(rel));
        }
        let ok = match rel {
            SketchRelation::Coincident(a, b) => {
                matches!(
                    (self.kind_of(a)?, self.kind_of(b)?),
                    (K::Point, _) | (_, K::Point)
                )
            }
            SketchRelation::Horizontal(a) | SketchRelation::Vertical(a) => {
                self.kind_of(a)? == K::Line
            }
            SketchRelation::Parallel(a, b) | SketchRelation::Perpendicular(a, b) => {
                self.kind_of(a)? == K::Line && self.kind_of(b)? == K::Line
            }
            SketchRelation::Tangent(a, b) => {
                let (ka, kb) = (self.kind_of(a)?, self.kind_of(b)?);
                let curve = |k| matches!(k, K::Line | K::Arc | K::Circle);
                let round = |k| matches!(k, K::Arc | K::Circle);
                curve(ka) && curve(kb) && (round(ka) || round(kb))
            }
            SketchRelation::Equal(a, b) => matches!(
                (self.kind_of(a)?, self.kind_of(b)?),
                (K::Line, K::Line) | (K::Arc | K::Circle, K::Arc | K::Circle)
            ),
            SketchRelation::Concentric(a, b) => {
                matches!(self.kind_of(a)?, K::Arc | K::Circle)
                    && matches!(self.kind_of(b)?, K::Arc | K::Circle)
            }
            SketchRelation::Midpoint { point, line } => match self.require_entity(line)? {
                SketchEntity::Line(l) => {
                    self.kind_of(point)? == K::Point && point != l.a() && point != l.b()
                }
                _ => false,
            },
            SketchRelation::Symmetric { a, b, axis } => {
                self.kind_of(a)? == K::Point
                    && self.kind_of(b)? == K::Point
                    && self.kind_of(axis)? == K::Line
                    && a != b
            }
            SketchRelation::Fix(a) => {
                self.kind_of(a)?;
                true
            }
        };
        if ok {
            Ok(())
        } else {
            Err(SketchEditError::InvalidRelationOperands(rel))
        }
    }

    fn validate_dimension(&self, dim: SketchDimension) -> Result<(), SketchEditError> {
        use SketchEntityKind as K;
        let ok = match dim {
            SketchDimension::Linear { a, b, .. } => {
                self.kind_of(a)? == K::Point && self.kind_of(b)? == K::Point
            }
            SketchDimension::Radius { target, .. } | SketchDimension::Diameter { target, .. } => {
                matches!(self.kind_of(target)?, K::Arc | K::Circle)
            }
            SketchDimension::Angular { a, b, .. } => {
                self.kind_of(a)? == K::Line && self.kind_of(b)? == K::Line
            }
        };
        if ok {
            Ok(())
        } else {
            Err(SketchEditError::InvalidDimensionOperands(dim))
        }
    }

    fn add_entity(self, entity: SketchEntity) -> (Self, EditOutcome) {
        let (id, entities) = insert(self.entities, entity);
        let entity_order = push(self.entity_order, id);
        let next = Self {
            entities,
            entity_order,
            ..self
        };
        (next, EditOutcome::Entity(id))
    }

    fn add_relation(self, rel: SketchRelation) -> Result<(Self, EditOutcome), SketchEditError> {
        self.validate_relation(rel)?;
        let (id, relations) = insert(self.relations, rel);
        let relation_order = push(self.relation_order, id);
        let next = Self {
            relations,
            relation_order,
            ..self
        };
        Ok((next, EditOutcome::Relation(id)))
    }

    fn add_dimension(self, dim: SketchDimension) -> Result<(Self, EditOutcome), SketchEditError> {
        self.validate_dimension(dim)?;
        let (id, dimensions) = insert(self.dimensions, dim);
        let dimension_order = push(self.dimension_order, id);
        let next = Self {
            dimensions,
            dimension_order,
            ..self
        };
        Ok((next, EditOutcome::Dimension(id)))
    }

    fn add_parameter(self, parameter: SketchParameter) -> (Self, EditOutcome) {
        let (id, parameters) = insert(self.parameters, parameter);
        let parameter_order = push(self.parameter_order, id);
        let next = Self {
            parameters,
            parameter_order,
            ..self
        };
        (next, EditOutcome::Parameter(id))
    }

    fn delete_entity(self, id: SketchEntityId) -> Result<(Self, EditOutcome), SketchEditError> {
        if !self.entities.contains_key(id) {
            return Err(SketchEditError::EntityNotFound(id));
        }
        let closure = entity_closure(&self.entities, id);
        let entities = remove_many(self.entities, closure.iter().copied());
        let entity_order = retain(&self.entity_order, |eid| !closure.contains(eid));
        let touches_closure_rel =
            |rel: &SketchRelation| rel.references().into_iter().any(|e| closure.contains(&e));
        let touches_closure_dim =
            |dim: &SketchDimension| dim.references().into_iter().any(|e| closure.contains(&e));
        let (relations, dropped_relations) = remove_where(self.relations, touches_closure_rel);
        let relation_order = retain(&self.relation_order, |rid| !dropped_relations.contains(rid));
        let (dimensions, dropped_dimensions) = remove_where(self.dimensions, touches_closure_dim);
        let dimension_order = retain(&self.dimension_order, |did| {
            !dropped_dimensions.contains(did)
        });
        let next = Self {
            entities,
            entity_order,
            relations,
            relation_order,
            dimensions,
            dimension_order,
            ..self
        };
        Ok((next, EditOutcome::None))
    }

    fn delete_relation(self, id: SketchRelationId) -> Result<(Self, EditOutcome), SketchEditError> {
        if !self.relations.contains_key(id) {
            return Err(SketchEditError::RelationNotFound(id));
        }
        let relations = remove_one(self.relations, id);
        let relation_order = retain(&self.relation_order, |rid| *rid != id);
        let next = Self {
            relations,
            relation_order,
            ..self
        };
        Ok((next, EditOutcome::None))
    }

    fn delete_dimension(
        self,
        id: SketchDimensionId,
    ) -> Result<(Self, EditOutcome), SketchEditError> {
        if !self.dimensions.contains_key(id) {
            return Err(SketchEditError::DimensionNotFound(id));
        }
        let dimensions = remove_one(self.dimensions, id);
        let dimension_order = retain(&self.dimension_order, |did| *did != id);
        let next = Self {
            dimensions,
            dimension_order,
            ..self
        };
        Ok((next, EditOutcome::None))
    }

    fn delete_parameter(
        self,
        id: SketchParameterId,
    ) -> Result<(Self, EditOutcome), SketchEditError> {
        if !self.parameters.contains_key(id) {
            return Err(SketchEditError::ParameterNotFound(id));
        }
        let parameters = remove_one(self.parameters, id);
        let parameter_order = retain(&self.parameter_order, |pid| *pid != id);
        let next = Self {
            parameters,
            parameter_order,
            ..self
        };
        Ok((next, EditOutcome::None))
    }

    fn set_construction(
        self,
        id: SketchEntityId,
        for_construction: bool,
    ) -> Result<(Self, EditOutcome), SketchEditError> {
        let entity = *self.require_entity(id)?;
        let updated = match entity {
            SketchEntity::Point(_) => return Err(SketchEditError::PointAlwaysConstruction),
            SketchEntity::Line(l) => SketchEntity::Line(l.with_construction(for_construction)),
            SketchEntity::Arc(a) => SketchEntity::Arc(a.with_construction(for_construction)),
            SketchEntity::Circle(c) => SketchEntity::Circle(c.with_construction(for_construction)),
        };
        if updated == entity {
            return Ok((self, EditOutcome::None));
        }
        let entities = replace(self.entities, id, updated);
        let next = Self { entities, ..self };
        Ok((next, EditOutcome::None))
    }

    fn move_point(
        self,
        id: SketchEntityId,
        position: Point2,
    ) -> Result<(Self, EditOutcome), SketchEditError> {
        let entity = *self.require_entity(id)?;
        let SketchEntity::Point(_) = entity else {
            return Err(SketchEditError::ExpectedPoint(id));
        };
        let updated = SketchEntity::point(position);
        if updated == entity {
            return Ok((self, EditOutcome::None));
        }
        let entities = replace(self.entities, id, updated);
        let next = Self { entities, ..self };
        Ok((next, EditOutcome::None))
    }

    fn update_dimension(
        self,
        id: SketchDimensionId,
        value: DimensionValue,
    ) -> Result<(Self, EditOutcome), SketchEditError> {
        let existing = *self
            .dimensions
            .get(id)
            .ok_or(SketchEditError::DimensionNotFound(id))?;
        if existing.kind() == DimensionKind::Driven {
            return Err(SketchEditError::DimensionIsDriven(id));
        }
        let updated = existing
            .with_value(value)
            .map_err(|_| SketchEditError::DimensionValueMismatch { id })?;
        if updated == existing {
            return Ok((self, EditOutcome::None));
        }
        let dimensions = replace(self.dimensions, id, updated);
        let next = Self { dimensions, ..self };
        Ok((next, EditOutcome::None))
    }

    #[must_use]
    pub(crate) fn with_point_positions(self, updates: &HashMap<SketchEntityId, Point2>) -> Self {
        if updates.is_empty() {
            return self;
        }
        let entities = updates
            .iter()
            .fold(unwrap_arc(self.entities), |mut acc, (id, pt)| {
                if let Some(slot @ SketchEntity::Point(_)) = acc.get_mut(*id) {
                    *slot = SketchEntity::Point(PointData::new(*pt));
                }
                acc
            });
        Self {
            entities: Arc::new(entities),
            ..self
        }
    }

    #[must_use]
    pub(crate) fn with_circle_radii(self, updates: &HashMap<SketchEntityId, Length>) -> Self {
        if updates.is_empty() {
            return self;
        }
        let entities = updates
            .iter()
            .fold(unwrap_arc(self.entities), |mut acc, (id, r)| {
                if let Some(SketchEntity::Circle(existing)) = acc.get(*id).copied()
                    && let Some(slot) = acc.get_mut(*id)
                {
                    *slot = SketchEntity::Circle(CircleData::new(
                        existing.center(),
                        *r,
                        existing.for_construction(),
                    ));
                }
                acc
            });
        Self {
            entities: Arc::new(entities),
            ..self
        }
    }

    #[must_use]
    pub(crate) fn with_driven_dimension_values(
        self,
        updates: &[(SketchDimensionId, DimensionValue)],
    ) -> Self {
        if updates.is_empty() {
            return self;
        }
        let dimensions =
            updates
                .iter()
                .copied()
                .fold(unwrap_arc(self.dimensions), |mut acc, (id, v)| {
                    let existing = acc.get(id).copied();
                    if let Some(dim) = existing
                        && dim.kind() == DimensionKind::Driven
                        && let Ok(next) = dim.with_value(v)
                        && let Some(slot) = acc.get_mut(id)
                    {
                        *slot = next;
                    }
                    acc
                });
        Self {
            dimensions: Arc::new(dimensions),
            ..self
        }
    }
}

impl PartialEq for Sketch {
    fn eq(&self, other: &Self) -> bool {
        self.plane == other.plane
            && *self.entity_order == *other.entity_order
            && Self::entries_equal(&self.entity_order, &self.entities, &other.entities)
            && *self.relation_order == *other.relation_order
            && Self::entries_equal(&self.relation_order, &self.relations, &other.relations)
            && *self.dimension_order == *other.dimension_order
            && Self::entries_equal(&self.dimension_order, &self.dimensions, &other.dimensions)
            && *self.parameter_order == *other.parameter_order
            && Self::entries_equal(&self.parameter_order, &self.parameters, &other.parameters)
    }
}

fn ensure_order_matches<K, V>(
    container: &'static str,
    order: &[K],
    map: &SlotMap<K, V>,
) -> Result<(), SketchEditError>
where
    K: slotmap::Key + Ord,
{
    let order_set: BTreeSet<K> = order.iter().copied().collect();
    let has_duplicates = order_set.len() != order.len();
    let same_size = order.len() == map.len();
    let covers_map = order_set.iter().all(|id| map.contains_key(*id));
    if has_duplicates || !same_size || !covers_map {
        Err(SketchEditError::OrderMismatch { container })
    } else {
        Ok(())
    }
}

fn validate_entity_shape(entity: SketchEntity) -> Result<(), SketchEditError> {
    let ok = match entity {
        SketchEntity::Point(_) => true,
        SketchEntity::Line(l) => l.a() != l.b(),
        SketchEntity::Arc(a) => {
            a.center() != a.start() && a.start() != a.end() && a.center() != a.end()
        }
        SketchEntity::Circle(c) => c.radius() > Length::default(),
    };
    if ok {
        Ok(())
    } else {
        Err(SketchEditError::DegenerateEntity(entity))
    }
}

fn insert<K, V>(map: Arc<SlotMap<K, V>>, value: V) -> (K, Arc<SlotMap<K, V>>)
where
    K: slotmap::Key,
    V: Clone,
{
    let mut owned = unwrap_arc(map);
    let id = owned.insert(value);
    (id, Arc::new(owned))
}

fn push<T: Clone>(list: Arc<Vec<T>>, value: T) -> Arc<Vec<T>> {
    let mut owned = unwrap_arc(list);
    owned.push(value);
    Arc::new(owned)
}

fn retain<T, F>(list: &Arc<Vec<T>>, keep: F) -> Arc<Vec<T>>
where
    T: Clone,
    F: Fn(&T) -> bool,
{
    Arc::new(list.iter().filter(|t| keep(t)).cloned().collect())
}

fn remove_one<K, V>(map: Arc<SlotMap<K, V>>, id: K) -> Arc<SlotMap<K, V>>
where
    K: slotmap::Key,
    V: Clone,
{
    let mut owned = unwrap_arc(map);
    owned.remove(id);
    Arc::new(owned)
}

fn remove_many<K, V, I>(map: Arc<SlotMap<K, V>>, ids: I) -> Arc<SlotMap<K, V>>
where
    K: slotmap::Key,
    V: Clone,
    I: IntoIterator<Item = K>,
{
    let owned = ids.into_iter().fold(unwrap_arc(map), |mut acc, id| {
        acc.remove(id);
        acc
    });
    Arc::new(owned)
}

fn remove_where<K, V, F>(map: Arc<SlotMap<K, V>>, predicate: F) -> (Arc<SlotMap<K, V>>, BTreeSet<K>)
where
    K: slotmap::Key + Ord,
    V: Clone,
    F: Fn(&V) -> bool,
{
    let dropped: BTreeSet<K> = map
        .iter()
        .filter_map(|(k, v)| predicate(v).then_some(k))
        .collect();
    let next = remove_many(map, dropped.iter().copied());
    (next, dropped)
}

fn replace<K, V>(map: Arc<SlotMap<K, V>>, id: K, value: V) -> Arc<SlotMap<K, V>>
where
    K: slotmap::Key,
    V: Clone,
{
    let mut owned = unwrap_arc(map);
    if let Some(slot) = owned.get_mut(id) {
        *slot = value;
    }
    Arc::new(owned)
}

fn unwrap_arc<T: Clone>(arc: Arc<T>) -> T {
    Arc::try_unwrap(arc).unwrap_or_else(|shared| (*shared).clone())
}

fn entity_closure(entities: &EntityMap, seed: SketchEntityId) -> BTreeSet<SketchEntityId> {
    grow_closure(entities, BTreeSet::from([seed]))
}

fn grow_closure(
    entities: &EntityMap,
    closure: BTreeSet<SketchEntityId>,
) -> BTreeSet<SketchEntityId> {
    let grown: BTreeSet<SketchEntityId> = entities
        .iter()
        .filter_map(|(id, entity)| {
            entity
                .references()
                .into_iter()
                .any(|r| closure.contains(&r))
                .then_some(id)
        })
        .chain(closure.iter().copied())
        .collect();
    if grown == closure {
        closure
    } else {
        grow_closure(entities, grown)
    }
}

#[derive(Debug, Clone, PartialEq, thiserror::Error)]
pub enum SketchEditError {
    #[error("entity not found: {0:?}")]
    EntityNotFound(SketchEntityId),
    #[error("relation not found: {0:?}")]
    RelationNotFound(SketchRelationId),
    #[error("dimension not found: {0:?}")]
    DimensionNotFound(SketchDimensionId),
    #[error("parameter not found: {0:?}")]
    ParameterNotFound(SketchParameterId),
    #[error("expected point entity: {0:?}")]
    ExpectedPoint(SketchEntityId),
    #[error("cannot toggle construction on a point")]
    PointAlwaysConstruction,
    #[error("dimension {id:?} value kind does not match variant")]
    DimensionValueMismatch { id: SketchDimensionId },
    #[error("cannot update driven dimension: {0:?}")]
    DimensionIsDriven(SketchDimensionId),
    #[error("invalid relation operands: {0:?}")]
    InvalidRelationOperands(SketchRelation),
    #[error("invalid dimension operands: {0:?}")]
    InvalidDimensionOperands(SketchDimension),
    #[error("entity is degenerate: {0:?}")]
    DegenerateEntity(SketchEntity),
    #[error("self-referencing relation: {0:?}")]
    SelfReferencingRelation(SketchRelation),
    #[error("{container} order list is out of step with its slotmap")]
    OrderMismatch { container: &'static str },
}

#[cfg(test)]
mod tests {
    use super::*;
    use bone_types::{
        Angle, Length, Parameter, Point2, Point3, Tolerance, UnitVec3, degree, millimeter,
    };

    fn plane() -> 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 len_mm(v: f64) -> Length {
        Length::new::<millimeter>(v)
    }

    fn angle_deg(v: f64) -> Angle {
        Angle::new::<degree>(v)
    }

    fn rectangle_script() -> Vec<SketchEdit> {
        vec![
            SketchEdit::AddEntity(SketchEntity::point(Point2::from_mm(0.0, 0.0))),
            SketchEdit::AddEntity(SketchEntity::point(Point2::from_mm(10.0, 0.0))),
            SketchEdit::AddEntity(SketchEntity::point(Point2::from_mm(10.0, 5.0))),
            SketchEdit::AddEntity(SketchEntity::point(Point2::from_mm(0.0, 5.0))),
        ]
    }

    fn apply_script(sketch: &Sketch, edits: Vec<SketchEdit>) -> (Sketch, Vec<EditOutcome>) {
        let Ok(pair) = sketch.apply_all(edits) else {
            panic!("fixture edits apply cleanly");
        };
        pair
    }

    #[test]
    fn point_is_always_construction() {
        let p = SketchEntity::point(Point2::from_mm(0.0, 0.0));
        assert!(p.for_construction());
    }

    #[test]
    fn new_sketch_is_empty() {
        let s = Sketch::new(plane());
        assert!(s.entities().is_empty());
        assert!(s.relations().is_empty());
        assert!(s.dimensions().is_empty());
        assert!(s.parameters().is_empty());
        assert!(s.entity_order().is_empty());
    }

    #[test]
    fn add_point_records_insertion_order() {
        let (sketch, outcomes) = apply_script(&Sketch::new(plane()), rectangle_script());
        let ids: Vec<_> = outcomes
            .iter()
            .map(|o| match *o {
                EditOutcome::Entity(id) => id,
                _ => panic!("expected entity outcomes"),
            })
            .collect();
        assert_eq!(sketch.entity_order(), ids.as_slice());
        assert_eq!(sketch.entities().len(), 4);
    }

    #[test]
    fn add_line_requires_point_endpoints() {
        let mut sketch = Sketch::new(plane());
        let Ok((s1, EditOutcome::Entity(a))) = sketch.apply(SketchEdit::AddEntity(
            SketchEntity::point(Point2::from_mm(0.0, 0.0)),
        )) else {
            panic!("add point");
        };
        let Ok((s2, EditOutcome::Entity(b))) = s1.apply(SketchEdit::AddEntity(
            SketchEntity::point(Point2::from_mm(1.0, 0.0)),
        )) else {
            panic!("add point");
        };
        let Ok((s3, EditOutcome::Entity(_))) =
            s2.apply(SketchEdit::AddEntity(SketchEntity::line(a, b, false)))
        else {
            panic!("add line");
        };
        sketch = s3;
        let bad = sketch.apply(SketchEdit::AddEntity(SketchEntity::line(
            a,
            SketchEntityId::default(),
            false,
        )));
        assert!(matches!(bad, Err(SketchEditError::EntityNotFound(_))));
    }

    #[test]
    fn add_line_rejects_non_point_endpoint() {
        let Ok((s1, EditOutcome::Entity(a))) = Sketch::new(plane()).apply(SketchEdit::AddEntity(
            SketchEntity::point(Point2::from_mm(0.0, 0.0)),
        )) else {
            panic!("add point");
        };
        let Ok((s2, EditOutcome::Entity(center))) = s1.apply(SketchEdit::AddEntity(
            SketchEntity::point(Point2::from_mm(2.0, 0.0)),
        )) else {
            panic!("add center");
        };
        let Ok((s3, EditOutcome::Entity(circle))) = s2.apply(SketchEdit::AddEntity(
            SketchEntity::circle(center, len_mm(1.0), false),
        )) else {
            panic!("add circle");
        };
        let bad = s3.apply(SketchEdit::AddEntity(SketchEntity::line(a, circle, false)));
        assert!(matches!(bad, Err(SketchEditError::ExpectedPoint(_))));
    }

    #[test]
    fn delete_entity_cascades_dependents() {
        let s = Sketch::new(plane());
        let Ok((s, EditOutcome::Entity(a))) = s.apply(SketchEdit::AddEntity(SketchEntity::point(
            Point2::from_mm(0.0, 0.0),
        ))) else {
            panic!("a");
        };
        let Ok((s, EditOutcome::Entity(b))) = s.apply(SketchEdit::AddEntity(SketchEntity::point(
            Point2::from_mm(1.0, 0.0),
        ))) else {
            panic!("b");
        };
        let Ok((s, EditOutcome::Entity(line))) =
            s.apply(SketchEdit::AddEntity(SketchEntity::line(a, b, false)))
        else {
            panic!("line");
        };
        let Ok((s, EditOutcome::Relation(_))) =
            s.apply(SketchEdit::AddRelation(SketchRelation::Horizontal(line)))
        else {
            panic!("rel");
        };
        let Ok((s, EditOutcome::Dimension(_))) =
            s.apply(SketchEdit::AddDimension(SketchDimension::Linear {
                a,
                b,
                value: len_mm(10.0),
                kind: DimensionKind::Driving,
            }))
        else {
            panic!("dim");
        };
        let Ok((after, EditOutcome::None)) = s.apply(SketchEdit::DeleteEntity(a)) else {
            panic!("delete");
        };
        assert_eq!(after.entities().len(), 1);
        assert!(after.entities().contains_key(b));
        assert!(!after.entities().contains_key(line));
        assert!(after.relations().is_empty());
        assert!(after.dimensions().is_empty());
        assert_eq!(after.entity_order(), [b]);
        assert!(after.relation_order().is_empty());
        assert!(after.dimension_order().is_empty());
    }

    #[test]
    fn delete_entity_errors_on_unknown() {
        let s = Sketch::new(plane());
        let bad = s.apply(SketchEdit::DeleteEntity(SketchEntityId::default()));
        assert!(matches!(bad, Err(SketchEditError::EntityNotFound(_))));
    }

    #[test]
    fn move_point_updates_position() {
        let Ok((s, EditOutcome::Entity(p))) = Sketch::new(plane()).apply(SketchEdit::AddEntity(
            SketchEntity::point(Point2::from_mm(1.0, 2.0)),
        )) else {
            panic!("seed point");
        };
        let target = Point2::from_mm(7.0, -3.0);
        let Ok((after, EditOutcome::None)) = s.apply(SketchEdit::MovePoint {
            id: p,
            position: target,
        }) else {
            panic!("move accepts");
        };
        let SketchEntity::Point(pt) = after.entities()[p] else {
            panic!("still a point");
        };
        assert_eq!(pt.at(), target);
    }

    #[test]
    fn move_point_no_op_when_position_unchanged() {
        let at = Point2::from_mm(4.0, 4.0);
        let Ok((s, EditOutcome::Entity(p))) =
            Sketch::new(plane()).apply(SketchEdit::AddEntity(SketchEntity::point(at)))
        else {
            panic!("seed point");
        };
        let Ok((after, EditOutcome::None)) = s.clone().apply(SketchEdit::MovePoint {
            id: p,
            position: at,
        }) else {
            panic!("idempotent move accepts");
        };
        assert_eq!(after, s);
    }

    #[test]
    fn move_point_rejects_non_point_entity() {
        let s = Sketch::new(plane());
        let Ok((s, EditOutcome::Entity(a))) = s.apply(SketchEdit::AddEntity(SketchEntity::point(
            Point2::from_mm(0.0, 0.0),
        ))) else {
            panic!("a");
        };
        let Ok((s, EditOutcome::Entity(b))) = s.apply(SketchEdit::AddEntity(SketchEntity::point(
            Point2::from_mm(1.0, 0.0),
        ))) else {
            panic!("b");
        };
        let Ok((s, EditOutcome::Entity(line))) =
            s.apply(SketchEdit::AddEntity(SketchEntity::line(a, b, false)))
        else {
            panic!("line");
        };
        let bad = s.apply(SketchEdit::MovePoint {
            id: line,
            position: Point2::from_mm(2.0, 2.0),
        });
        assert!(matches!(bad, Err(SketchEditError::ExpectedPoint(_))));
    }

    #[test]
    fn move_point_errors_on_unknown_id() {
        let s = Sketch::new(plane());
        let bad = s.apply(SketchEdit::MovePoint {
            id: SketchEntityId::default(),
            position: Point2::from_mm(0.0, 0.0),
        });
        assert!(matches!(bad, Err(SketchEditError::EntityNotFound(_))));
    }

    #[test]
    fn set_construction_refuses_point() {
        let Ok((s, EditOutcome::Entity(p))) = Sketch::new(plane()).apply(SketchEdit::AddEntity(
            SketchEntity::point(Point2::from_mm(0.0, 0.0)),
        )) else {
            panic!("p");
        };
        let bad = s.apply(SketchEdit::SetConstruction {
            id: p,
            for_construction: false,
        });
        assert!(matches!(bad, Err(SketchEditError::PointAlwaysConstruction)));
    }

    #[test]
    fn set_construction_toggles_line() {
        let s = Sketch::new(plane());
        let Ok((s, EditOutcome::Entity(a))) = s.apply(SketchEdit::AddEntity(SketchEntity::point(
            Point2::from_mm(0.0, 0.0),
        ))) else {
            panic!("a");
        };
        let Ok((s, EditOutcome::Entity(b))) = s.apply(SketchEdit::AddEntity(SketchEntity::point(
            Point2::from_mm(1.0, 0.0),
        ))) else {
            panic!("b");
        };
        let Ok((s, EditOutcome::Entity(line))) =
            s.apply(SketchEdit::AddEntity(SketchEntity::line(a, b, false)))
        else {
            panic!("line");
        };
        assert!(!s.entities()[line].for_construction());
        let Ok((s, _)) = s.apply(SketchEdit::SetConstruction {
            id: line,
            for_construction: true,
        }) else {
            panic!("toggle");
        };
        assert!(s.entities()[line].for_construction());
    }

    #[test]
    fn update_dimension_value_rejects_unit_mismatch() {
        let Ok((s, EditOutcome::Entity(a))) = Sketch::new(plane()).apply(SketchEdit::AddEntity(
            SketchEntity::point(Point2::from_mm(0.0, 0.0)),
        )) else {
            panic!("a");
        };
        let Ok((s, EditOutcome::Entity(b))) = s.apply(SketchEdit::AddEntity(SketchEntity::point(
            Point2::from_mm(1.0, 0.0),
        ))) else {
            panic!("b");
        };
        let Ok((s, EditOutcome::Dimension(id))) =
            s.apply(SketchEdit::AddDimension(SketchDimension::Linear {
                a,
                b,
                value: len_mm(1.0),
                kind: DimensionKind::Driving,
            }))
        else {
            panic!("dim");
        };
        let bad = s.apply(SketchEdit::UpdateDimensionValue {
            id,
            value: DimensionValue::Angle(angle_deg(90.0)),
        });
        assert!(matches!(
            bad,
            Err(SketchEditError::DimensionValueMismatch { .. })
        ));
    }

    #[test]
    fn update_dimension_value_replaces_length() {
        let Ok((s, EditOutcome::Entity(a))) = Sketch::new(plane()).apply(SketchEdit::AddEntity(
            SketchEntity::point(Point2::from_mm(0.0, 0.0)),
        )) else {
            panic!("a");
        };
        let Ok((s, EditOutcome::Entity(b))) = s.apply(SketchEdit::AddEntity(SketchEntity::point(
            Point2::from_mm(1.0, 0.0),
        ))) else {
            panic!("b");
        };
        let Ok((s, EditOutcome::Dimension(id))) =
            s.apply(SketchEdit::AddDimension(SketchDimension::Linear {
                a,
                b,
                value: len_mm(1.0),
                kind: DimensionKind::Driving,
            }))
        else {
            panic!("dim");
        };
        let Ok((s, _)) = s.apply(SketchEdit::UpdateDimensionValue {
            id,
            value: DimensionValue::Length(len_mm(12.5)),
        }) else {
            panic!("update");
        };
        let SketchDimension::Linear { value, .. } = s.dimensions()[id] else {
            panic!("variant unchanged");
        };
        assert!((value.get::<millimeter>() - 12.5).abs() < 1e-12);
    }

    #[test]
    fn identical_scripts_produce_equal_sketches() {
        let script = rich_script();
        let a = Sketch::new(plane())
            .apply_all(script.clone())
            .map(|(s, _)| s);
        let b = Sketch::new(plane()).apply_all(script).map(|(s, _)| s);
        let (Ok(a), Ok(b)) = (a, b) else {
            panic!("scripts apply cleanly");
        };
        assert_eq!(a, b);
    }

    #[test]
    fn relation_edit_shares_entity_storage() {
        let (base, _) = apply_script(&Sketch::new(plane()), rectangle_script());
        let before = base.clone();
        let Ok((base_with_rel, _)) = base.apply(SketchEdit::AddRelation(SketchRelation::Fix(
            before.entity_order()[0],
        ))) else {
            panic!("relation");
        };
        assert!(Arc::ptr_eq(&before.entities, &base_with_rel.entities));
        assert!(Arc::ptr_eq(
            &before.entity_order,
            &base_with_rel.entity_order
        ));
        assert!(Arc::ptr_eq(&before.dimensions, &base_with_rel.dimensions));
        assert!(!Arc::ptr_eq(&before.relations, &base_with_rel.relations));
    }

    #[test]
    fn add_parameter_roundtrips() {
        let s = Sketch::new(plane());
        let Ok((s, EditOutcome::Parameter(id))) = s.apply(SketchEdit::AddParameter(
            SketchParameter::new(Parameter::new(0.5)),
        )) else {
            panic!("param");
        };
        assert!((s.parameters()[id].value().value() - 0.5).abs() < f64::EPSILON);
        let Ok((s, _)) = s.apply(SketchEdit::DeleteParameter(id)) else {
            panic!("delete param");
        };
        assert!(s.parameters().is_empty());
        assert!(s.parameter_order().is_empty());
    }

    fn rich_script() -> Vec<SketchEdit> {
        let mut edits = rectangle_script();
        edits.extend([
            SketchEdit::AddParameter(SketchParameter::new(Parameter::new(0.0))),
            SketchEdit::AddParameter(SketchParameter::new(Parameter::new(1.0))),
        ]);
        edits
    }

    fn two_points() -> (Sketch, SketchEntityId, SketchEntityId) {
        let Ok((s, EditOutcome::Entity(a))) = Sketch::new(plane()).apply(SketchEdit::AddEntity(
            SketchEntity::point(Point2::from_mm(0.0, 0.0)),
        )) else {
            panic!("a");
        };
        let Ok((s, EditOutcome::Entity(b))) = s.apply(SketchEdit::AddEntity(SketchEntity::point(
            Point2::from_mm(1.0, 0.0),
        ))) else {
            panic!("b");
        };
        (s, a, b)
    }

    #[test]
    fn horizontal_on_point_is_rejected() {
        let (s, a, _) = two_points();
        let bad = s.apply(SketchEdit::AddRelation(SketchRelation::Horizontal(a)));
        assert!(matches!(
            bad,
            Err(SketchEditError::InvalidRelationOperands(_))
        ));
    }

    #[test]
    fn tangent_between_two_lines_is_rejected() {
        let (s, a, b) = two_points();
        let Ok((s, EditOutcome::Entity(c))) = s.apply(SketchEdit::AddEntity(SketchEntity::point(
            Point2::from_mm(0.0, 1.0),
        ))) else {
            panic!("c");
        };
        let Ok((s, EditOutcome::Entity(l1))) =
            s.apply(SketchEdit::AddEntity(SketchEntity::line(a, b, false)))
        else {
            panic!("l1");
        };
        let Ok((s, EditOutcome::Entity(l2))) =
            s.apply(SketchEdit::AddEntity(SketchEntity::line(a, c, false)))
        else {
            panic!("l2");
        };
        let bad = s.apply(SketchEdit::AddRelation(SketchRelation::Tangent(l1, l2)));
        assert!(matches!(
            bad,
            Err(SketchEditError::InvalidRelationOperands(_))
        ));
    }

    #[test]
    fn equal_cross_kind_is_rejected() {
        let (s, a, b) = two_points();
        let Ok((s, EditOutcome::Entity(line))) =
            s.apply(SketchEdit::AddEntity(SketchEntity::line(a, b, false)))
        else {
            panic!("line");
        };
        let Ok((s, EditOutcome::Entity(circle))) = s.apply(SketchEdit::AddEntity(
            SketchEntity::circle(a, len_mm(1.0), false),
        )) else {
            panic!("circle");
        };
        let bad = s.apply(SketchEdit::AddRelation(SketchRelation::Equal(line, circle)));
        assert!(matches!(
            bad,
            Err(SketchEditError::InvalidRelationOperands(_))
        ));
    }

    #[test]
    fn coincident_point_on_line_is_accepted() {
        let (s, a, b) = two_points();
        let Ok((s, EditOutcome::Entity(line))) =
            s.apply(SketchEdit::AddEntity(SketchEntity::line(a, b, false)))
        else {
            panic!("line");
        };
        let Ok((s2, _)) = s.apply(SketchEdit::AddRelation(SketchRelation::Coincident(a, line)))
        else {
            panic!("coincident");
        };
        assert_eq!(s2.relations().len(), 1);
    }

    #[test]
    fn linear_dimension_on_non_point_is_rejected() {
        let (s, a, b) = two_points();
        let Ok((s, EditOutcome::Entity(line))) =
            s.apply(SketchEdit::AddEntity(SketchEntity::line(a, b, false)))
        else {
            panic!("line");
        };
        let bad = s.apply(SketchEdit::AddDimension(SketchDimension::Linear {
            a: line,
            b: a,
            value: len_mm(1.0),
            kind: DimensionKind::Driving,
        }));
        assert!(matches!(
            bad,
            Err(SketchEditError::InvalidDimensionOperands(_))
        ));
    }

    #[test]
    fn driven_dimension_cannot_be_updated() {
        let (s, a, b) = two_points();
        let Ok((s, EditOutcome::Dimension(id))) =
            s.apply(SketchEdit::AddDimension(SketchDimension::Linear {
                a,
                b,
                value: len_mm(1.0),
                kind: DimensionKind::Driven,
            }))
        else {
            panic!("dim");
        };
        let bad = s.apply(SketchEdit::UpdateDimensionValue {
            id,
            value: DimensionValue::Length(len_mm(2.0)),
        });
        assert!(matches!(bad, Err(SketchEditError::DimensionIsDriven(_))));
    }

    #[test]
    fn apply_all_leaves_caller_unchanged_on_error() {
        let (base, _) = apply_script(&Sketch::new(plane()), rectangle_script());
        let snapshot = base.clone();
        let bad_edit = SketchEdit::AddRelation(SketchRelation::Horizontal(base.entity_order()[0]));
        let result = base.apply_all([bad_edit]);
        assert!(matches!(
            result,
            Err(SketchEditError::InvalidRelationOperands(_))
        ));
        assert_eq!(base, snapshot);
    }

    #[test]
    fn line_with_same_endpoints_is_rejected() {
        let Ok((s, EditOutcome::Entity(a))) = Sketch::new(plane()).apply(SketchEdit::AddEntity(
            SketchEntity::point(Point2::from_mm(0.0, 0.0)),
        )) else {
            panic!("a");
        };
        let bad = s.apply(SketchEdit::AddEntity(SketchEntity::line(a, a, false)));
        assert!(matches!(bad, Err(SketchEditError::DegenerateEntity(_))));
    }

    #[test]
    fn arc_with_repeated_endpoints_is_rejected() {
        let Ok((s, EditOutcome::Entity(a))) = Sketch::new(plane()).apply(SketchEdit::AddEntity(
            SketchEntity::point(Point2::from_mm(0.0, 0.0)),
        )) else {
            panic!("a");
        };
        let Ok((s, EditOutcome::Entity(b))) = s.apply(SketchEdit::AddEntity(SketchEntity::point(
            Point2::from_mm(1.0, 0.0),
        ))) else {
            panic!("b");
        };
        let bad = s.apply(SketchEdit::AddEntity(SketchEntity::arc(a, b, a, false)));
        assert!(matches!(bad, Err(SketchEditError::DegenerateEntity(_))));
    }

    #[test]
    fn self_referencing_relation_is_rejected() {
        let (s, a, b) = two_points();
        let Ok((s, EditOutcome::Entity(line))) =
            s.apply(SketchEdit::AddEntity(SketchEntity::line(a, b, false)))
        else {
            panic!("line");
        };
        let bad = s.apply(SketchEdit::AddRelation(SketchRelation::Parallel(
            line, line,
        )));
        assert!(matches!(
            bad,
            Err(SketchEditError::SelfReferencingRelation(_))
        ));
    }

    #[test]
    fn coincident_self_reference_is_rejected() {
        let (s, a, _) = two_points();
        let bad = s.apply(SketchEdit::AddRelation(SketchRelation::Coincident(a, a)));
        assert!(matches!(
            bad,
            Err(SketchEditError::SelfReferencingRelation(_))
        ));
    }

    fn line_with_third_point(
        s: Sketch,
        a: SketchEntityId,
        b: SketchEntityId,
    ) -> (Sketch, SketchEntityId, SketchEntityId) {
        let Ok((s, EditOutcome::Entity(line))) =
            s.apply(SketchEdit::AddEntity(SketchEntity::line(a, b, false)))
        else {
            panic!("line");
        };
        let Ok((s, EditOutcome::Entity(p))) = s.apply(SketchEdit::AddEntity(SketchEntity::point(
            Point2::from_mm(0.0, 1.0),
        ))) else {
            panic!("midpoint");
        };
        (s, line, p)
    }

    #[test]
    fn midpoint_relation_accepts_distinct_point() {
        let (s, a, b) = two_points();
        let (s, line, mid) = line_with_third_point(s, a, b);
        let Ok((s2, _)) = s.apply(SketchEdit::AddRelation(SketchRelation::Midpoint {
            point: mid,
            line,
        })) else {
            panic!("midpoint relation");
        };
        assert_eq!(s2.relations().len(), 1);
    }

    #[test]
    fn midpoint_relation_rejects_endpoint_a_as_point() {
        let (s, a, b) = two_points();
        let (s, line, _) = line_with_third_point(s, a, b);
        let bad = s.apply(SketchEdit::AddRelation(SketchRelation::Midpoint {
            point: a,
            line,
        }));
        assert!(matches!(
            bad,
            Err(SketchEditError::InvalidRelationOperands(_))
        ));
    }

    #[test]
    fn midpoint_relation_rejects_endpoint_b_as_point() {
        let (s, a, b) = two_points();
        let (s, line, _) = line_with_third_point(s, a, b);
        let bad = s.apply(SketchEdit::AddRelation(SketchRelation::Midpoint {
            point: b,
            line,
        }));
        assert!(matches!(
            bad,
            Err(SketchEditError::InvalidRelationOperands(_))
        ));
    }

    #[test]
    fn midpoint_relation_rejects_non_line_target() {
        let (s, a, _) = two_points();
        let Ok((s, EditOutcome::Entity(circle))) = s.apply(SketchEdit::AddEntity(
            SketchEntity::circle(a, len_mm(1.0), false),
        )) else {
            panic!("circle");
        };
        let Ok((s, EditOutcome::Entity(p))) = s.apply(SketchEdit::AddEntity(SketchEntity::point(
            Point2::from_mm(2.0, 0.0),
        ))) else {
            panic!("p");
        };
        let bad = s.apply(SketchEdit::AddRelation(SketchRelation::Midpoint {
            point: p,
            line: circle,
        }));
        assert!(matches!(
            bad,
            Err(SketchEditError::InvalidRelationOperands(_))
        ));
    }

    #[test]
    fn set_construction_noop_shares_storage() {
        let (s, a, b) = two_points();
        let Ok((s, EditOutcome::Entity(line))) =
            s.apply(SketchEdit::AddEntity(SketchEntity::line(a, b, true)))
        else {
            panic!("line");
        };
        let before = s.clone();
        let Ok((after, _)) = s.apply(SketchEdit::SetConstruction {
            id: line,
            for_construction: true,
        }) else {
            panic!("set");
        };
        assert!(Arc::ptr_eq(&before.entities, &after.entities));
    }

    #[test]
    fn circle_with_non_positive_radius_is_rejected() {
        let Ok((s, EditOutcome::Entity(center))) = Sketch::new(plane()).apply(
            SketchEdit::AddEntity(SketchEntity::point(Point2::from_mm(0.0, 0.0))),
        ) else {
            panic!("center");
        };
        let zero = s.clone().apply(SketchEdit::AddEntity(SketchEntity::circle(
            center,
            len_mm(0.0),
            false,
        )));
        assert!(matches!(zero, Err(SketchEditError::DegenerateEntity(_))));
        let negative = s.apply(SketchEdit::AddEntity(SketchEntity::circle(
            center,
            len_mm(-1.0),
            false,
        )));
        assert!(matches!(
            negative,
            Err(SketchEditError::DegenerateEntity(_))
        ));
    }

    #[test]
    fn delete_relation_errors_on_unknown() {
        let s = Sketch::new(plane());
        let bad = s.apply(SketchEdit::DeleteRelation(SketchRelationId::default()));
        assert!(matches!(bad, Err(SketchEditError::RelationNotFound(_))));
    }

    #[test]
    fn delete_dimension_errors_on_unknown() {
        let s = Sketch::new(plane());
        let bad = s.apply(SketchEdit::DeleteDimension(SketchDimensionId::default()));
        assert!(matches!(bad, Err(SketchEditError::DimensionNotFound(_))));
    }

    #[test]
    fn delete_parameter_errors_on_unknown() {
        let s = Sketch::new(plane());
        let bad = s.apply(SketchEdit::DeleteParameter(SketchParameterId::default()));
        assert!(matches!(bad, Err(SketchEditError::ParameterNotFound(_))));
    }

    #[test]
    fn update_dimension_errors_on_unknown() {
        let s = Sketch::new(plane());
        let bad = s.apply(SketchEdit::UpdateDimensionValue {
            id: SketchDimensionId::default(),
            value: DimensionValue::Length(len_mm(1.0)),
        });
        assert!(matches!(bad, Err(SketchEditError::DimensionNotFound(_))));
    }

    #[test]
    fn driven_radius_cannot_be_updated() {
        let Ok((s, EditOutcome::Entity(center))) = Sketch::new(plane()).apply(
            SketchEdit::AddEntity(SketchEntity::point(Point2::from_mm(0.0, 0.0))),
        ) else {
            panic!("center");
        };
        let Ok((s, EditOutcome::Entity(circle))) = s.apply(SketchEdit::AddEntity(
            SketchEntity::circle(center, len_mm(1.0), false),
        )) else {
            panic!("circle");
        };
        let Ok((s, EditOutcome::Dimension(id))) =
            s.apply(SketchEdit::AddDimension(SketchDimension::Radius {
                target: circle,
                value: len_mm(1.0),
                kind: DimensionKind::Driven,
            }))
        else {
            panic!("dim");
        };
        let bad = s.apply(SketchEdit::UpdateDimensionValue {
            id,
            value: DimensionValue::Length(len_mm(2.0)),
        });
        assert!(matches!(bad, Err(SketchEditError::DimensionIsDriven(_))));
    }

    #[test]
    fn driven_diameter_cannot_be_updated() {
        let Ok((s, EditOutcome::Entity(center))) = Sketch::new(plane()).apply(
            SketchEdit::AddEntity(SketchEntity::point(Point2::from_mm(0.0, 0.0))),
        ) else {
            panic!("center");
        };
        let Ok((s, EditOutcome::Entity(circle))) = s.apply(SketchEdit::AddEntity(
            SketchEntity::circle(center, len_mm(1.0), false),
        )) else {
            panic!("circle");
        };
        let Ok((s, EditOutcome::Dimension(id))) =
            s.apply(SketchEdit::AddDimension(SketchDimension::Diameter {
                target: circle,
                value: len_mm(2.0),
                kind: DimensionKind::Driven,
            }))
        else {
            panic!("dim");
        };
        let bad = s.apply(SketchEdit::UpdateDimensionValue {
            id,
            value: DimensionValue::Length(len_mm(4.0)),
        });
        assert!(matches!(bad, Err(SketchEditError::DimensionIsDriven(_))));
    }

    #[test]
    fn driven_angular_cannot_be_updated() {
        let (s, a, b) = two_points();
        let Ok((s, EditOutcome::Entity(c))) = s.apply(SketchEdit::AddEntity(SketchEntity::point(
            Point2::from_mm(0.0, 1.0),
        ))) else {
            panic!("c");
        };
        let Ok((s, EditOutcome::Entity(l1))) =
            s.apply(SketchEdit::AddEntity(SketchEntity::line(a, b, false)))
        else {
            panic!("l1");
        };
        let Ok((s, EditOutcome::Entity(l2))) =
            s.apply(SketchEdit::AddEntity(SketchEntity::line(a, c, false)))
        else {
            panic!("l2");
        };
        let Ok((s, EditOutcome::Dimension(id))) =
            s.apply(SketchEdit::AddDimension(SketchDimension::Angular {
                a: l1,
                b: l2,
                value: angle_deg(90.0),
                kind: DimensionKind::Driven,
            }))
        else {
            panic!("dim");
        };
        let bad = s.apply(SketchEdit::UpdateDimensionValue {
            id,
            value: DimensionValue::Angle(angle_deg(45.0)),
        });
        assert!(matches!(bad, Err(SketchEditError::DimensionIsDriven(_))));
    }

    #[test]
    fn status_reports_dangling_when_relation_references_missing_entity() {
        use bone_types::SketchStatus;

        let (mut sketch, outcomes) = apply_script(&Sketch::new(plane()), rectangle_script());
        let entity_ids: Vec<SketchEntityId> = outcomes
            .iter()
            .map(|o| match *o {
                EditOutcome::Entity(id) => id,
                _ => panic!("expected entity outcomes"),
            })
            .collect();
        let Ok((with_rel, EditOutcome::Relation(rel_id))) = sketch.apply(SketchEdit::AddRelation(
            SketchRelation::Coincident(entity_ids[0], entity_ids[1]),
        )) else {
            panic!("relation should add cleanly");
        };
        sketch = with_rel;
        let entities = Arc::make_mut(&mut sketch.entities);
        assert!(entities.remove(entity_ids[1]).is_some());

        let report = sketch.status();
        assert_eq!(report.status(), SketchStatus::Dangling);
        assert!(
            report
                .offending()
                .contains(&bone_types::SketchItemId::Relation(rel_id))
        );
    }

    #[test]
    fn status_reports_dangling_when_dimension_references_missing_entity() {
        use bone_types::SketchStatus;

        let (mut sketch, outcomes) = apply_script(&Sketch::new(plane()), rectangle_script());
        let entity_ids: Vec<SketchEntityId> = outcomes
            .iter()
            .map(|o| match *o {
                EditOutcome::Entity(id) => id,
                _ => panic!("expected entity outcomes"),
            })
            .collect();
        let Ok((with_dim, EditOutcome::Dimension(dim_id))) =
            sketch.apply(SketchEdit::AddDimension(SketchDimension::Linear {
                a: entity_ids[0],
                b: entity_ids[1],
                value: len_mm(10.0),
                kind: DimensionKind::Driving,
            }))
        else {
            panic!("dimension should add cleanly");
        };
        sketch = with_dim;
        let entities = Arc::make_mut(&mut sketch.entities);
        assert!(entities.remove(entity_ids[0]).is_some());

        let report = sketch.status();
        assert_eq!(report.status(), SketchStatus::Dangling);
        assert!(
            report
                .offending()
                .contains(&bone_types::SketchItemId::Dimension(dim_id))
        );
    }

    #[test]
    fn status_reports_dangling_when_entity_references_missing_point() {
        use bone_types::SketchStatus;

        let (sketch, outcomes) = apply_script(&Sketch::new(plane()), rectangle_script());
        let entity_ids: Vec<SketchEntityId> = outcomes
            .iter()
            .map(|o| match *o {
                EditOutcome::Entity(id) => id,
                _ => panic!("expected entity outcomes"),
            })
            .collect();
        let Ok((mut sketch, EditOutcome::Entity(line_id))) =
            sketch.apply(SketchEdit::AddEntity(SketchEntity::line(
                entity_ids[0],
                entity_ids[1],
                false,
            )))
        else {
            panic!("line should add cleanly");
        };
        let entities = Arc::make_mut(&mut sketch.entities);
        assert!(entities.remove(entity_ids[0]).is_some());

        let report = sketch.status();
        assert_eq!(report.status(), SketchStatus::Dangling);
        assert!(
            report
                .offending()
                .contains(&bone_types::SketchItemId::Entity(line_id))
        );
    }

    #[test]
    fn version_is_stable_across_clone_and_bumps_on_edit() {
        let sketch = Sketch::new(plane());
        let v_a = sketch.version();
        let v_clone = sketch.clone().version();
        assert_eq!(v_a, v_clone);

        let (after, _) = apply_script(&sketch, rectangle_script());
        assert_ne!(after.version(), v_a);
    }

    mod properties {
        use super::*;
        use proptest::prelude::*;

        #[derive(Copy, Clone, Debug)]
        enum Step {
            Point(i16, i16),
            Parameter(i16),
            Line {
                ai: u8,
                bi: u8,
            },
            Circle {
                ci: u8,
                r: u16,
            },
            Horizontal {
                li: u8,
            },
            LinearDim {
                ai: u8,
                bi: u8,
                v: u16,
                driven: bool,
            },
            UpdateDim {
                di: u8,
                v: u16,
            },
            Toggle {
                ei: u8,
            },
            DelEntity {
                ei: u8,
            },
            DelParam {
                pi: u8,
            },
        }

        fn arb_step() -> impl Strategy<Value = Step> {
            prop_oneof![
                (any::<i16>(), any::<i16>()).prop_map(|(x, y)| Step::Point(x, y)),
                any::<i16>().prop_map(Step::Parameter),
                (any::<u8>(), any::<u8>()).prop_map(|(ai, bi)| Step::Line { ai, bi }),
                (any::<u8>(), any::<u16>()).prop_map(|(ci, r)| Step::Circle { ci, r: r % 100 + 1 }),
                any::<u8>().prop_map(|li| Step::Horizontal { li }),
                (any::<u8>(), any::<u8>(), any::<u16>(), any::<bool>())
                    .prop_map(|(ai, bi, v, driven)| Step::LinearDim { ai, bi, v, driven }),
                (any::<u8>(), any::<u16>()).prop_map(|(di, v)| Step::UpdateDim { di, v }),
                any::<u8>().prop_map(|ei| Step::Toggle { ei }),
                any::<u8>().prop_map(|ei| Step::DelEntity { ei }),
                any::<u8>().prop_map(|pi| Step::DelParam { pi }),
            ]
        }

        fn entities_of_kind(s: &Sketch, kind: SketchEntityKind) -> Vec<SketchEntityId> {
            s.entity_order()
                .iter()
                .copied()
                .filter(|id| s.entities()[*id].kind() == kind)
                .collect()
        }

        fn pick<T: Copy>(xs: &[T], i: u8) -> Option<T> {
            if xs.is_empty() {
                None
            } else {
                Some(xs[usize::from(i) % xs.len()])
            }
        }

        fn pick_two_distinct<T: Copy + Eq>(xs: &[T], ai: u8, bi: u8) -> Option<(T, T)> {
            if xs.len() < 2 {
                return None;
            }
            let ai = usize::from(ai) % xs.len();
            let offset = usize::from(bi) % (xs.len() - 1) + 1;
            let bi = (ai + offset) % xs.len();
            Some((xs[ai], xs[bi]))
        }

        fn resolve_step(s: &Sketch, step: Step) -> Option<SketchEdit> {
            match step {
                Step::Point(x, y) => Some(SketchEdit::AddEntity(SketchEntity::point(
                    Point2::from_mm(f64::from(x), f64::from(y)),
                ))),
                Step::Parameter(v) => Some(SketchEdit::AddParameter(SketchParameter::new(
                    Parameter::new(f64::from(v)),
                ))),
                Step::Line { ai, bi } => {
                    let (a, b) =
                        pick_two_distinct(&entities_of_kind(s, SketchEntityKind::Point), ai, bi)?;
                    Some(SketchEdit::AddEntity(SketchEntity::line(a, b, false)))
                }
                Step::Circle { ci, r } => {
                    let c = pick(&entities_of_kind(s, SketchEntityKind::Point), ci)?;
                    Some(SketchEdit::AddEntity(SketchEntity::circle(
                        c,
                        Length::new::<millimeter>(f64::from(r)),
                        false,
                    )))
                }
                Step::Horizontal { li } => {
                    let l = pick(&entities_of_kind(s, SketchEntityKind::Line), li)?;
                    Some(SketchEdit::AddRelation(SketchRelation::Horizontal(l)))
                }
                Step::LinearDim { ai, bi, v, driven } => {
                    let (a, b) =
                        pick_two_distinct(&entities_of_kind(s, SketchEntityKind::Point), ai, bi)?;
                    let kind = if driven {
                        DimensionKind::Driven
                    } else {
                        DimensionKind::Driving
                    };
                    Some(SketchEdit::AddDimension(SketchDimension::Linear {
                        a,
                        b,
                        value: Length::new::<millimeter>(f64::from(v)),
                        kind,
                    }))
                }
                Step::UpdateDim { di, v } => {
                    let id = pick(s.dimension_order(), di)?;
                    Some(SketchEdit::UpdateDimensionValue {
                        id,
                        value: DimensionValue::Length(Length::new::<millimeter>(f64::from(v))),
                    })
                }
                Step::Toggle { ei } => {
                    let id = pick(s.entity_order(), ei)?;
                    Some(SketchEdit::SetConstruction {
                        id,
                        for_construction: true,
                    })
                }
                Step::DelEntity { ei } => {
                    let id = pick(s.entity_order(), ei)?;
                    Some(SketchEdit::DeleteEntity(id))
                }
                Step::DelParam { pi } => {
                    let id = pick(s.parameter_order(), pi)?;
                    Some(SketchEdit::DeleteParameter(id))
                }
            }
        }

        fn build_script(steps: Vec<Step>) -> Vec<SketchEdit> {
            steps
                .into_iter()
                .fold(
                    (Sketch::new(plane()), Vec::new()),
                    |(sk, mut acc), step| match resolve_step(&sk, step) {
                        Some(edit) => match sk.clone().apply(edit) {
                            Ok((next, _)) => {
                                acc.push(edit);
                                (next, acc)
                            }
                            Err(_) => (sk, acc),
                        },
                        None => (sk, acc),
                    },
                )
                .1
        }

        proptest! {
            #[test]
            fn apply_all_is_deterministic(steps in prop::collection::vec(arb_step(), 0..40)) {
                let script = build_script(steps);
                let Ok((a, _)) = Sketch::new(plane()).apply_all(script.clone()) else {
                    unreachable!("build_script only emits successfully-applied edits");
                };
                let Ok((b, _)) = Sketch::new(plane()).apply_all(script) else {
                    unreachable!();
                };
                prop_assert_eq!(a, b);
            }

            #[test]
            fn apply_all_matches_apply_fold(steps in prop::collection::vec(arb_step(), 0..40)) {
                let script = build_script(steps);
                let Ok((via_all, _)) = Sketch::new(plane()).apply_all(script.clone()) else {
                    unreachable!();
                };
                let via_fold = script.into_iter().try_fold(Sketch::new(plane()), |acc, e| {
                    acc.apply(e).map(|(s, _)| s)
                });
                let Ok(via_fold) = via_fold else { unreachable!(); };
                prop_assert_eq!(via_all, via_fold);
            }
        }
    }
}
