use std::collections::HashSet;

use bone_types::{
    Aabb3, AngleTolerance, BrepEdgeId, BrepFaceId, BrepLoopId, BrepShellId, BrepVertexId,
    ChordHeightTolerance, CreaseAngle, EdgeLabel, EdgeRole, FaceLabel, Point3, SideKind,
    StepEntityKind, Tolerance, VertexLabel,
};
use slotmap::SlotMap;
use truck_modeling::ShellCondition;

mod build;
pub(crate) mod convert;
mod edges;
pub mod eval;
pub mod persist;
pub mod profile;
pub mod step;
pub mod tessellate;
use crate::curve3::Curve3;
use build::{Arena, BoundaryIndex, EdgeArenaHandle, edge_length, edge_points};
use convert::point_from_truck;
pub use edges::{EdgeCurve3, EdgePolyline, EdgePolylines};
pub use persist::{BrepReattach, EdgeReattach};
pub use tessellate::{FaceMesh, MeshError, SolidMesh};

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TruckGap {
    ReverseNormal,
    AxisDirection,
    ReferenceDirection,
    ThroughAll,
    UpToNext,
    UpToVertex,
    UpToSurface,
    OffsetFromSurface,
    UpToBody,
    Draft,
    ThinWall,
}

impl core::fmt::Display for TruckGap {
    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
        f.write_str(match self {
            Self::ReverseNormal => "reverse-normal extrude direction",
            Self::AxisDirection => "along-axis extrude direction",
            Self::ReferenceDirection => "between-references extrude direction",
            Self::ThroughAll => "through-all end condition",
            Self::UpToNext => "up-to-next end condition",
            Self::UpToVertex => "up-to-vertex end condition",
            Self::UpToSurface => "up-to-surface end condition",
            Self::OffsetFromSurface => "offset-from-surface end condition",
            Self::UpToBody => "up-to-body end condition",
            Self::Draft => "draft taper",
            Self::ThinWall => "thin-wall shelling",
        })
    }
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum LabelKind {
    Face,
    Edge,
    Vertex,
}

impl core::fmt::Display for LabelKind {
    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
        f.write_str(match self {
            Self::Face => "face",
            Self::Edge => "edge",
            Self::Vertex => "vertex",
        })
    }
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ProfileDefect {
    OpenLoop,
    BranchingVertex,
    SelfIntersectingLoop,
    ZeroArea,
    UncontainedLoop,
    OverlappingLoops,
}

impl core::fmt::Display for ProfileDefect {
    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
        f.write_str(match self {
            Self::OpenLoop => "an open loop",
            Self::BranchingVertex => "a vertex shared by more than two edges",
            Self::SelfIntersectingLoop => "a self-intersecting loop",
            Self::ZeroArea => "zero area",
            Self::UncontainedLoop => "a loop outside the outer boundary",
            Self::OverlappingLoops => "overlapping inner loops",
        })
    }
}

#[derive(Debug, Clone, thiserror::Error)]
pub enum BrepError {
    #[error("extrude profile has {reason}")]
    InvalidProfile { reason: ProfileDefect },
    #[error("extrude depth is zero")]
    EmptyExtrudeDepth,
    #[error("boundary shell is not a closed manifold")]
    ShellNotClosed,
    #[error("edge {edge:?} collapses below the validation tolerance")]
    DegenerateEdge { edge: BrepEdgeId },
    #[error("edge {edge:?} belongs to no loop")]
    DanglingEdge { edge: BrepEdgeId },
    #[error("vertex {vertex:?} belongs to no loop")]
    DanglingVertex { vertex: BrepVertexId },
    #[error("kernel facade does not yet wrap this path: {detail}")]
    TruckUnsupported { detail: TruckGap },
    #[error("{kind} carries no extrude label")]
    MissingLabel { kind: LabelKind },
    #[error("solid has {found} {kind} entries, reattach payload carries {expected}")]
    ReattachMismatch {
        kind: LabelKind,
        found: usize,
        expected: usize,
    },
    #[error("solid geometry blob could not be serialized")]
    BlobSerialize,
    #[error("solid geometry blob could not be parsed")]
    BlobParse,
    #[error("STEP text is not valid ISO-10303-21")]
    StepSyntax,
    #[error("STEP file has no data section")]
    StepNoData,
    #[error("STEP shell could not be reconstructed")]
    StepShellMalformed,
    #[error("STEP file carries no solid shell")]
    StepEmpty,
    #[error("STEP file carries {count} shells; only a single closed shell is supported")]
    StepMultipleShells { count: usize },
    #[error("STEP geometry uses {kind}, which the facade does not yet bridge")]
    StepUnsupported { kind: StepEntityKind },
}

#[derive(Clone, Debug)]
pub struct BrepShell {
    id: BrepShellId,
    boundary_index: BoundaryIndex,
    faces: Vec<BrepFaceId>,
}

impl BrepShell {
    #[must_use]
    pub fn id(&self) -> BrepShellId {
        self.id
    }

    #[must_use]
    pub fn faces(&self) -> &[BrepFaceId] {
        &self.faces
    }
}

#[derive(Clone, Debug)]
pub struct BrepFace {
    id: BrepFaceId,
    label: FaceLabel,
    loops: Vec<BrepLoopId>,
}

impl BrepFace {
    #[must_use]
    pub fn id(&self) -> BrepFaceId {
        self.id
    }

    #[must_use]
    pub fn label(&self) -> FaceLabel {
        self.label
    }

    #[must_use]
    pub fn loops(&self) -> &[BrepLoopId] {
        &self.loops
    }
}

#[derive(Clone, Debug)]
pub struct BrepLoop {
    id: BrepLoopId,
    edges: Vec<BrepEdgeId>,
}

impl BrepLoop {
    #[must_use]
    pub fn id(&self) -> BrepLoopId {
        self.id
    }

    #[must_use]
    pub fn edges(&self) -> &[BrepEdgeId] {
        &self.edges
    }
}

#[derive(Clone, Debug)]
pub struct BrepEdge {
    id: BrepEdgeId,
    label: EdgeLabel,
    handles: Vec<EdgeArenaHandle>,
    vertices: [BrepVertexId; 2],
    curve: EdgeCurve3,
    crease: CreaseAngle,
}

impl BrepEdge {
    #[must_use]
    pub fn id(&self) -> BrepEdgeId {
        self.id
    }

    #[must_use]
    pub fn label(&self) -> EdgeLabel {
        self.label
    }

    #[must_use]
    pub fn vertices(&self) -> [BrepVertexId; 2] {
        self.vertices
    }

    #[must_use]
    pub fn curve(&self) -> &EdgeCurve3 {
        &self.curve
    }

    #[must_use]
    pub fn crease(&self) -> CreaseAngle {
        self.crease
    }
}

#[derive(Clone, Debug)]
pub struct BrepVertex {
    id: BrepVertexId,
    label: VertexLabel,
    position: Point3,
}

impl BrepVertex {
    #[must_use]
    pub fn id(&self) -> BrepVertexId {
        self.id
    }

    #[must_use]
    pub fn label(&self) -> VertexLabel {
        self.label
    }

    #[must_use]
    pub fn position(&self) -> Point3 {
        self.position
    }
}

#[derive(Clone)]
pub struct BrepSolid {
    arena: Arena,
    shells: SlotMap<BrepShellId, BrepShell>,
    faces: SlotMap<BrepFaceId, BrepFace>,
    loops: SlotMap<BrepLoopId, BrepLoop>,
    edges: SlotMap<BrepEdgeId, BrepEdge>,
    vertices: SlotMap<BrepVertexId, BrepVertex>,
    shell_order: Vec<BrepShellId>,
    face_order: Vec<BrepFaceId>,
    loop_order: Vec<BrepLoopId>,
    edge_order: Vec<BrepEdgeId>,
    vertex_order: Vec<BrepVertexId>,
    reattach: persist::BrepReattach,
}

impl BrepSolid {
    pub fn iter_shells(&self) -> impl Iterator<Item = &BrepShell> {
        self.shell_order.iter().map(|id| &self.shells[*id])
    }

    pub fn iter_faces(&self) -> impl Iterator<Item = &BrepFace> {
        self.face_order.iter().map(|id| &self.faces[*id])
    }

    pub fn iter_loops(&self) -> impl Iterator<Item = &BrepLoop> {
        self.loop_order.iter().map(|id| &self.loops[*id])
    }

    pub fn iter_edges(&self) -> impl Iterator<Item = &BrepEdge> {
        self.edge_order.iter().map(|id| &self.edges[*id])
    }

    pub fn iter_vertices(&self) -> impl Iterator<Item = &BrepVertex> {
        self.vertex_order.iter().map(|id| &self.vertices[*id])
    }

    pub fn validate(&self, tolerance: Tolerance) -> Result<(), BrepError> {
        if self.shells.is_empty() {
            return Err(BrepError::ShellNotClosed);
        }
        self.iter_shells().try_for_each(|shell| {
            let truck_shell = self.arena.boundary(shell.boundary_index);
            let closed = truck_shell.shell_condition() == ShellCondition::Closed;
            if closed && truck_shell.singular_vertices().is_empty() {
                Ok(())
            } else {
                Err(BrepError::ShellNotClosed)
            }
        })?;
        self.iter_edges().try_for_each(|edge| {
            let length: f64 = edge
                .handles
                .iter()
                .map(|handle| edge_length(self.arena.edge(*handle)))
                .sum();
            if length <= tolerance.value() {
                Err(BrepError::DegenerateEdge { edge: edge.id })
            } else {
                Ok(())
            }
        })?;
        let loop_edges: HashSet<BrepEdgeId> = self
            .iter_loops()
            .flat_map(|brep_loop| brep_loop.edges().iter().copied())
            .collect();
        self.iter_edges().try_for_each(|edge| {
            if loop_edges.contains(&edge.id)
                || matches!(
                    edge.label.role,
                    EdgeRole::SideEdge {
                        side: SideKind::Seam,
                        ..
                    }
                )
            {
                Ok(())
            } else {
                Err(BrepError::DanglingEdge { edge: edge.id })
            }
        })?;
        let loop_vertices: HashSet<BrepVertexId> = loop_edges
            .iter()
            .flat_map(|edge| self.edges[*edge].vertices())
            .collect();
        self.iter_vertices().try_for_each(|vertex| {
            if loop_vertices.contains(&vertex.id) {
                Ok(())
            } else {
                Err(BrepError::DanglingVertex { vertex: vertex.id })
            }
        })
    }

    #[must_use]
    pub fn bounding_box(&self) -> Option<Aabb3> {
        let points = self.iter_edges().flat_map(|edge| {
            edge.handles.iter().flat_map(|handle| {
                edge_points(self.arena.edge(*handle))
                    .into_iter()
                    .map(point_from_truck)
            })
        });
        Aabb3::from_points(points)
    }

    pub fn tessellate(
        &self,
        chord: ChordHeightTolerance,
        angle: AngleTolerance,
    ) -> Result<SolidMesh, MeshError> {
        tessellate::tessellate_solid(self, chord, angle)
    }

    #[must_use]
    pub fn edges_for_render(&self, chord: ChordHeightTolerance) -> EdgePolylines {
        EdgePolylines::new(
            self.iter_edges()
                .filter(|edge| edges::is_render_visible(edge.label))
                .map(|edge| {
                    let points = edge.curve.tessellate(chord);
                    EdgePolyline::new(edge.id, edge.label, points, edge.curve.clone(), edge.crease)
                })
                .collect(),
        )
    }
}

#[cfg(test)]
mod tests {
    use super::build::{SolidLabeling, assemble, edge_length};
    use super::{BrepError, BrepLoop, BrepSolid, LabelKind, MeshError, SolidMesh};
    use bone_types::{
        AngleTolerance, BrepLoopId, ChordHeightTolerance, EdgeLabel, EdgeRole, FaceLabel, FaceRole,
        FeatureId, LoopIndex, SideKind, SketchEntityId, Tolerance, VertexLabel, VertexRole,
    };
    use slotmap::SlotMap;
    use std::collections::{HashMap, HashSet};
    use truck_modeling::{Face, Point3, Shell, Solid, Vector3, builder};

    const LOW: f64 = 0.25;
    const HIGH: f64 = 0.75;

    struct Profile {
        feature: FeatureId,
        edges: [SketchEntityId; 4],
        corners: [SketchEntityId; 4],
    }

    fn profile() -> Profile {
        let mut features: SlotMap<FeatureId, ()> = SlotMap::with_key();
        let mut entities: SlotMap<SketchEntityId, ()> = SlotMap::with_key();
        Profile {
            feature: features.insert(()),
            edges: [
                entities.insert(()),
                entities.insert(()),
                entities.insert(()),
                entities.insert(()),
            ],
            corners: [
                entities.insert(()),
                entities.insert(()),
                entities.insert(()),
                entities.insert(()),
            ],
        }
    }

    fn unit_cube() -> Solid {
        let corner = builder::vertex(Point3::new(0.0, 0.0, 0.0));
        let edge = builder::tsweep(&corner, Vector3::new(1.0, 0.0, 0.0));
        let face = builder::tsweep(&edge, Vector3::new(0.0, 1.0, 0.0));
        builder::tsweep(&face, Vector3::new(0.0, 0.0, 1.0))
    }

    fn iteration_fingerprint(solid: &Solid) -> String {
        let faces = solid
            .boundaries()
            .iter()
            .flat_map(Shell::face_iter)
            .map(|face| {
                let centroid = face
                    .boundaries()
                    .iter()
                    .flat_map(|wire| wire.vertex_iter().map(|v| v.point()))
                    .fold((0.0, 0.0, 0.0, 0.0), |(x, y, z, n), p| {
                        (x + p.x, y + p.y, z + p.z, n + 1.0)
                    });
                format!("f({:.3},{:.3},{:.3})", centroid.0, centroid.1, centroid.2)
            })
            .collect::<Vec<_>>()
            .join(" ");
        let vertices = solid
            .vertex_iter()
            .map(|v| {
                let p = v.point();
                format!("v({:.3},{:.3},{:.3})", p.x, p.y, p.z)
            })
            .collect::<Vec<_>>()
            .join(" ");
        let edges = solid
            .edge_iter()
            .map(|e| {
                let a = e.front().point();
                let b = e.back().point();
                format!("e({:.3},{:.3}->{:.3},{:.3})", a.x, a.y, b.x, b.y)
            })
            .collect::<Vec<_>>()
            .join(" ");
        format!("faces:{faces}\nverts:{vertices}\nedges:{edges}")
    }

    #[test]
    fn truck_solid_iteration_order_survives_serde() {
        let solid = unit_cube();
        let before = iteration_fingerprint(&solid);
        let Ok(text) = ron::to_string(&solid) else {
            panic!("serialize truck solid");
        };
        let Ok(restored) = ron::from_str::<Solid>(&text) else {
            panic!("deserialize truck solid");
        };
        let after = iteration_fingerprint(&restored);
        assert_eq!(before, after);
    }

    fn corner_index(x: f64, y: f64) -> usize {
        usize::from(x > 0.5) * 2 + usize::from(y > 0.5)
    }

    fn wall_index(footprint: &[(f64, f64)]) -> usize {
        if footprint.iter().all(|&(_, y)| y < LOW) {
            0
        } else if footprint.iter().all(|&(x, _)| x > HIGH) {
            1
        } else if footprint.iter().all(|&(_, y)| y > HIGH) {
            2
        } else {
            3
        }
    }

    fn label_solid(solid: &Solid, profile: &Profile) -> SolidLabeling {
        let faces: HashMap<_, _> = solid
            .boundaries()
            .iter()
            .flat_map(Shell::face_iter)
            .map(|face| {
                let points: Vec<Point3> = face
                    .boundaries()
                    .iter()
                    .flat_map(|wire| wire.vertex_iter().map(|vertex| vertex.point()))
                    .collect();
                let role = if points.iter().all(|p| p.z < LOW) {
                    FaceRole::StartCap
                } else if points.iter().all(|p| p.z > HIGH) {
                    FaceRole::EndCap
                } else {
                    let footprint: Vec<(f64, f64)> = points.iter().map(|p| (p.x, p.y)).collect();
                    FaceRole::Side {
                        loop_index: LoopIndex::OUTER,
                        from: profile.edges[wall_index(&footprint)],
                    }
                };
                (
                    face.id(),
                    FaceLabel {
                        feature: profile.feature,
                        role,
                    },
                )
            })
            .collect();

        let edges: HashMap<_, _> = solid
            .edge_iter()
            .map(|edge| {
                let a = edge.front().point();
                let b = edge.back().point();
                let footprint = [(a.x, a.y), (b.x, b.y)];
                let role = if a.z < LOW && b.z < LOW {
                    EdgeRole::StartCapEdge {
                        from: profile.edges[wall_index(&footprint)],
                    }
                } else if a.z > HIGH && b.z > HIGH {
                    EdgeRole::EndCapEdge {
                        from: profile.edges[wall_index(&footprint)],
                    }
                } else {
                    EdgeRole::SideEdge {
                        from: profile.corners[corner_index(a.x, a.y)],
                        side: SideKind::Corner,
                    }
                };
                (
                    edge.id(),
                    EdgeLabel {
                        feature: profile.feature,
                        role,
                    },
                )
            })
            .collect();

        let vertices: HashMap<_, _> = solid
            .vertex_iter()
            .map(|vertex| {
                let p = vertex.point();
                let from = profile.corners[corner_index(p.x, p.y)];
                let role = if p.z < 0.5 {
                    VertexRole::StartCapVertex {
                        from,
                        side: SideKind::Corner,
                    }
                } else {
                    VertexRole::EndCapVertex {
                        from,
                        side: SideKind::Corner,
                    }
                };
                (
                    vertex.id(),
                    VertexLabel {
                        feature: profile.feature,
                        role,
                    },
                )
            })
            .collect();

        SolidLabeling {
            faces,
            edges,
            vertices,
            closed_curves: HashSet::new(),
            edge_curves: HashMap::new(),
        }
    }

    fn open_shell(solid: &Solid) -> Solid {
        let faces: Vec<Face> = solid
            .boundaries()
            .iter()
            .flat_map(Shell::face_iter)
            .take(5)
            .cloned()
            .collect();
        Solid::new_unchecked(vec![faces.into_iter().collect::<Shell>()])
    }

    #[test]
    fn unit_cube_iteration_is_label_ordered() {
        let profile = profile();
        let solid = unit_cube();
        let labeling = label_solid(&solid, &profile);
        let Ok(brep) = assemble(solid, &labeling) else {
            panic!("unit cube labels are complete");
        };

        let faces = brep
            .iter_faces()
            .map(|face| format!("  {}", face.label()))
            .collect::<Vec<_>>()
            .join("\n");
        let edges = brep
            .iter_edges()
            .map(|edge| format!("  {}", edge.label()))
            .collect::<Vec<_>>()
            .join("\n");
        let vertices = brep
            .iter_vertices()
            .map(|vertex| format!("  {}", vertex.label()))
            .collect::<Vec<_>>()
            .join("\n");
        let shells = brep
            .iter_shells()
            .map(|shell| format!("  shell faces={}", shell.faces().len()))
            .collect::<Vec<_>>()
            .join("\n");

        insta::assert_snapshot!(format!(
            "shells:\n{shells}\nfaces:\n{faces}\nedges:\n{edges}\nvertices:\n{vertices}"
        ));
    }

    #[test]
    fn unit_cube_topology_counts() {
        let profile = profile();
        let solid = unit_cube();
        let labeling = label_solid(&solid, &profile);
        let Ok(brep) = assemble(solid, &labeling) else {
            panic!("unit cube labels are complete");
        };
        assert_eq!(brep.iter_shells().count(), 1);
        assert_eq!(brep.iter_faces().count(), 6);
        assert_eq!(brep.iter_loops().count(), 6);
        assert_eq!(brep.iter_edges().count(), 12);
        assert_eq!(brep.iter_vertices().count(), 8);
        let Some(first) = brep.iter_faces().next() else {
            panic!("cube has faces");
        };
        let Some(last) = brep.iter_faces().last() else {
            panic!("cube has faces");
        };
        assert_eq!(first.label().role, FaceRole::StartCap);
        assert_eq!(last.label().role, FaceRole::EndCap);
    }

    #[test]
    fn loop_iteration_follows_face_order() {
        let profile = profile();
        let solid = unit_cube();
        let labeling = label_solid(&solid, &profile);
        let Ok(brep) = assemble(solid, &labeling) else {
            panic!("unit cube labels are complete");
        };
        let grouped_by_face: Vec<BrepLoopId> = brep
            .iter_faces()
            .flat_map(|face| face.loops().iter().copied())
            .collect();
        let iterated: Vec<BrepLoopId> = brep.iter_loops().map(BrepLoop::id).collect();
        assert_eq!(iterated.len(), 6);
        assert_eq!(grouped_by_face, iterated);
    }

    #[test]
    fn unit_cube_validates() {
        let profile = profile();
        let solid = unit_cube();
        let labeling = label_solid(&solid, &profile);
        let Ok(brep) = assemble(solid, &labeling) else {
            panic!("unit cube labels are complete");
        };
        assert!(brep.validate(Tolerance::new(1e-9)).is_ok());
    }

    #[test]
    fn open_shell_is_not_closed() {
        let profile = profile();
        let solid = open_shell(&unit_cube());
        let labeling = label_solid(&solid, &profile);
        let Ok(brep) = assemble(solid, &labeling) else {
            panic!("open shell labels are complete");
        };
        assert!(matches!(
            brep.validate(Tolerance::new(1e-9)),
            Err(BrepError::ShellNotClosed)
        ));
    }

    #[test]
    fn missing_label_is_reported() {
        let profile = profile();
        let solid = unit_cube();
        let mut labeling = label_solid(&solid, &profile);
        labeling.faces.clear();
        assert!(matches!(
            assemble(solid, &labeling),
            Err(BrepError::MissingLabel {
                kind: LabelKind::Face
            })
        ));
    }

    #[test]
    fn shared_face_label_groups_into_one() {
        let profile = profile();
        let solid = unit_cube();
        let mut labeling = label_solid(&solid, &profile);
        let Some(shared) = labeling.faces.values().copied().next() else {
            panic!("cube has faces");
        };
        labeling
            .faces
            .values_mut()
            .for_each(|label| *label = shared);
        let Ok(brep) = assemble(solid, &labeling) else {
            panic!("faces sharing one label group rather than collide");
        };
        assert_eq!(brep.iter_faces().count(), 1);
    }

    #[test]
    fn oversized_tolerance_flags_degenerate_edge() {
        let profile = profile();
        let solid = unit_cube();
        let labeling = label_solid(&solid, &profile);
        let Ok(brep) = assemble(solid, &labeling) else {
            panic!("unit cube labels are complete");
        };
        assert!(matches!(
            brep.validate(Tolerance::new(2.0)),
            Err(BrepError::DegenerateEdge { .. })
        ));
    }

    #[test]
    fn closed_curve_edge_is_not_degenerate() {
        let start = builder::vertex(Point3::new(1.0, 0.0, 0.0));
        let end = builder::vertex(Point3::new(1.0, 0.05, 0.0));
        let arc = builder::circle_arc(&start, &end, Point3::new(-1.0, 0.0, 0.0));
        let gap = end.point() - start.point();
        let chord = gap.x.hypot(gap.y).hypot(gap.z);
        assert!(chord < 0.1);
        assert!(edge_length(&arc) > 5.0);
    }

    #[test]
    fn empty_solid_is_not_closed() {
        let solid = Solid::new_unchecked(vec![]);
        let labeling = SolidLabeling {
            faces: HashMap::new(),
            edges: HashMap::new(),
            vertices: HashMap::new(),
            closed_curves: HashSet::new(),
            edge_curves: HashMap::new(),
        };
        let Ok(brep) = assemble(solid, &labeling) else {
            panic!("empty solid has nothing to label");
        };
        assert!(matches!(
            brep.validate(Tolerance::new(1e-9)),
            Err(BrepError::ShellNotClosed)
        ));
    }

    #[test]
    fn missing_edge_label_is_reported() {
        let profile = profile();
        let solid = unit_cube();
        let mut labeling = label_solid(&solid, &profile);
        labeling.edges.clear();
        assert!(matches!(
            assemble(solid, &labeling),
            Err(BrepError::MissingLabel {
                kind: LabelKind::Edge
            })
        ));
    }

    #[test]
    fn missing_vertex_label_is_reported() {
        let profile = profile();
        let solid = unit_cube();
        let mut labeling = label_solid(&solid, &profile);
        labeling.vertices.clear();
        assert!(matches!(
            assemble(solid, &labeling),
            Err(BrepError::MissingLabel {
                kind: LabelKind::Vertex
            })
        ));
    }

    #[test]
    fn shared_edge_label_groups_into_one() {
        let profile = profile();
        let solid = unit_cube();
        let mut labeling = label_solid(&solid, &profile);
        let Some(shared) = labeling.edges.values().copied().next() else {
            panic!("cube has edges");
        };
        labeling
            .edges
            .values_mut()
            .for_each(|label| *label = shared);
        let Ok(brep) = assemble(solid, &labeling) else {
            panic!("edges sharing one label group rather than collide");
        };
        assert_eq!(brep.iter_edges().count(), 1);
    }

    #[test]
    fn shared_vertex_label_groups_into_one() {
        let profile = profile();
        let solid = unit_cube();
        let mut labeling = label_solid(&solid, &profile);
        let Some(shared) = labeling.vertices.values().copied().next() else {
            panic!("cube has vertices");
        };
        labeling
            .vertices
            .values_mut()
            .for_each(|label| *label = shared);
        let Ok(brep) = assemble(solid, &labeling) else {
            panic!("vertices sharing one label group rather than collide");
        };
        assert_eq!(brep.iter_vertices().count(), 1);
    }

    fn unit_cube_brep() -> BrepSolid {
        let profile = profile();
        let solid = unit_cube();
        let labeling = label_solid(&solid, &profile);
        let Ok(brep) = assemble(solid, &labeling) else {
            panic!("unit cube labels are complete");
        };
        brep
    }

    fn tessellate_cube(chord: ChordHeightTolerance, angle: AngleTolerance) -> SolidMesh {
        let Ok(mesh) = unit_cube_brep().tessellate(chord, angle) else {
            panic!("unit cube tessellates");
        };
        mesh
    }

    #[test]
    fn tessellate_unit_cube_emits_per_face_slabs() {
        let brep = unit_cube_brep();
        let Ok(mesh) = brep.tessellate(
            ChordHeightTolerance::from_mm(0.05),
            AngleTolerance::from_radians(0.2),
        ) else {
            panic!("unit cube tessellates");
        };
        let face_count = brep.iter_faces().count();
        assert_eq!(mesh.faces().len(), face_count);
        mesh.faces().iter().for_each(|slab| {
            assert!(!slab.triangles().is_empty(), "slab has triangles");
            assert!(!slab.positions().is_empty(), "slab has positions");
            assert_eq!(slab.positions().len(), slab.normals().len());
        });
    }

    #[test]
    fn tessellate_unit_cube_validates() {
        let mesh = tessellate_cube(
            ChordHeightTolerance::from_mm(0.05),
            AngleTolerance::from_radians(0.2),
        );
        assert!(mesh.validate(Tolerance::new(1e-9)).is_ok());
    }

    #[test]
    fn tessellate_is_deterministic() {
        let chord = ChordHeightTolerance::from_mm(0.05);
        let angle = AngleTolerance::from_radians(0.2);
        let first = tessellate_cube(chord, angle);
        let second = tessellate_cube(chord, angle);
        assert_eq!(first, second);
        assert_eq!(first.generation(), second.generation());
    }

    #[test]
    fn tessellate_generation_changes_with_chord() {
        let coarse = tessellate_cube(
            ChordHeightTolerance::from_mm(0.1),
            AngleTolerance::from_radians(0.2),
        );
        let fine = tessellate_cube(
            ChordHeightTolerance::from_mm(0.001),
            AngleTolerance::from_radians(0.2),
        );
        assert_ne!(coarse.generation(), fine.generation());
    }

    #[test]
    fn tessellate_generation_changes_with_angle() {
        let chord = ChordHeightTolerance::from_mm(0.05);
        let lo = tessellate_cube(chord, AngleTolerance::from_radians(0.01));
        let hi = tessellate_cube(chord, AngleTolerance::from_radians(1.0));
        assert_ne!(lo.generation(), hi.generation());
    }

    #[test]
    fn tessellate_oversized_tolerance_flags_degenerate() {
        let mesh = tessellate_cube(
            ChordHeightTolerance::from_mm(0.05),
            AngleTolerance::from_radians(0.2),
        );
        assert!(matches!(
            mesh.validate(Tolerance::new(10.0)),
            Err(MeshError::DegenerateTriangle { .. })
        ));
    }

    proptest::proptest! {
        #[test]
        fn tessellate_proptest_is_deterministic(
            chord_mm in 0.001f64..=0.2f64,
            angle_rad in 0.01f64..=0.5f64,
        ) {
            let chord = ChordHeightTolerance::from_mm(chord_mm);
            let angle = AngleTolerance::from_radians(angle_rad);
            let first = tessellate_cube(chord, angle);
            let second = tessellate_cube(chord, angle);
            proptest::prop_assert_eq!(first, second);
        }
    }
}
