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

use bone_kernel::{
    Arc2, BrepError, Circle2, ContourSelection, Curve2, Curve2Kind, ExtrudeProfile, Line2,
    ProfileDefect, ProfileEdge, ProfileLoop,
};
use bone_types::{Parameter, Plane3, Point2, SketchEntityId, Tolerance};

use crate::sketch::{ArcData, CircleData, EllipseData, LineData, Sketch, SketchEntity, SplineData};
use crate::sketch3::{ProjectedSketch, Sketch3};

const PROFILE_TOLERANCE: Tolerance = Tolerance::new(1.0e-9);

fn non_planar() -> BrepError {
    BrepError::InvalidProfile {
        reason: ProfileDefect::NonPlanar,
    }
}

pub(crate) fn profile_from_sketch3(
    sketch: &Sketch3,
    selection: ContourSelection,
    axis: Option<SketchEntityId>,
) -> Result<ExtrudeProfile, BrepError> {
    let projection = sketch.project_onto(sketch.profile_plane().ok_or_else(non_planar)?);
    let profile = build_profile_for(
        projection.sketch(),
        translate_selection(selection, &projection),
        axis.and_then(|entity| projection.to_2d(entity)),
    )?;
    Ok(remap_profile(&profile, &projection))
}

pub(crate) fn profiles_from_sketch3_regions(
    sketch: &Sketch3,
    axis: Option<SketchEntityId>,
) -> Result<Vec<ExtrudeProfile>, BrepError> {
    let projection = sketch.project_onto(sketch.profile_plane().ok_or_else(non_planar)?);
    let regions = build_profiles_for_regions(
        projection.sketch(),
        axis.and_then(|entity| projection.to_2d(entity)),
    )?;
    Ok(regions
        .into_iter()
        .map(|region| remap_profile(&region, &projection))
        .collect())
}

fn translate_selection(
    selection: ContourSelection,
    projection: &ProjectedSketch,
) -> ContourSelection {
    match selection {
        ContourSelection::All => ContourSelection::All,
        ContourSelection::Contour(seed) => {
            ContourSelection::Contour(projection.to_2d(seed).unwrap_or(seed))
        }
    }
}

fn remap_profile(profile: &ExtrudeProfile, projection: &ProjectedSketch) -> ExtrudeProfile {
    let to3 = |id: SketchEntityId| projection.to_3d(id).unwrap_or(id);
    let edge =
        |e: &ProfileEdge| ProfileEdge::new(e.curve(), to3(e.curve_entity()), to3(e.corner()));
    let loops = profile
        .loops()
        .iter()
        .map(|profile_loop| match profile_loop {
            ProfileLoop::Closed {
                curve,
                curve_entity,
            } => ProfileLoop::Closed {
                curve: *curve,
                curve_entity: to3(*curve_entity),
            },
            ProfileLoop::Open(edges) => ProfileLoop::Open(edges.iter().map(edge).collect()),
            ProfileLoop::Chain(edges) => ProfileLoop::Chain(edges.iter().map(edge).collect()),
        })
        .collect();
    ExtrudeProfile::new(profile.plane(), loops)
}

#[cfg(test)]
pub(crate) fn build_profile(sketch: &Sketch) -> Result<ExtrudeProfile, BrepError> {
    build_profile_for(sketch, ContourSelection::All, None)
}

pub(crate) fn build_profile_for(
    sketch: &Sketch,
    selection: ContourSelection,
    axis: Option<SketchEntityId>,
) -> Result<ExtrudeProfile, BrepError> {
    let plane = Plane3::from(sketch.plane());
    let loops = all_loops(sketch, axis)?;
    let selected = match selection {
        ContourSelection::All => loops,
        ContourSelection::Contour(seed) => select_contour(&loops, seed)?,
    };
    Ok(ExtrudeProfile::new(plane, selected))
}

fn all_loops(sketch: &Sketch, axis: Option<SketchEntityId>) -> Result<Vec<ProfileLoop>, BrepError> {
    let circles = circle_loops(sketch, axis)?;
    let ellipses = ellipse_loops(sketch, axis)?;
    let splines = spline_loops(sketch, axis)?;
    let chains = chain_loops(sketch, axis)?;
    Ok(circles
        .into_iter()
        .chain(ellipses)
        .chain(splines)
        .chain(chains)
        .collect())
}

pub(crate) fn build_profiles_for_regions(
    sketch: &Sketch,
    axis: Option<SketchEntityId>,
) -> Result<Vec<ExtrudeProfile>, BrepError> {
    let plane = Plane3::from(sketch.plane());
    let loops = all_loops(sketch, axis)?;
    Ok(group_regions(&loops)
        .into_iter()
        .map(|region| ExtrudeProfile::new(plane, region))
        .collect())
}

fn containment_depth(shapes: &[ClosedShape], shape: &ClosedShape) -> usize {
    core::iter::successors(immediate_container(shapes, shape), |ancestor| {
        immediate_container(shapes, ancestor)
    })
    .count()
}

fn group_regions(loops: &[ProfileLoop]) -> Vec<Vec<ProfileLoop>> {
    let shapes = closed_shapes(loops);
    let mut regions: Vec<Vec<ProfileLoop>> = shapes
        .iter()
        .filter(|shape| containment_depth(&shapes, shape).is_multiple_of(2))
        .map(|root| {
            core::iter::once(loops[root.index].clone())
                .chain(
                    shapes
                        .iter()
                        .filter(|child| {
                            immediate_container(&shapes, child)
                                .is_some_and(|parent| parent.index == root.index)
                        })
                        .map(|child| loops[child.index].clone()),
                )
                .collect()
        })
        .collect();
    regions.extend(
        loops
            .iter()
            .filter(|profile_loop| matches!(profile_loop, ProfileLoop::Chain(_)))
            .map(|chain| vec![chain.clone()]),
    );
    regions
}

const REGION_SAMPLES: u32 = 24;

fn loop_has_entity(profile_loop: &ProfileLoop, seed: SketchEntityId) -> bool {
    match profile_loop {
        ProfileLoop::Closed { curve_entity, .. } => *curve_entity == seed,
        ProfileLoop::Open(edges) | ProfileLoop::Chain(edges) => edges
            .iter()
            .any(|edge| edge.curve_entity() == seed || edge.corner() == seed),
    }
}

fn sample_curve(curve: Curve2Kind) -> Vec<Point2> {
    let count = match curve {
        Curve2Kind::Line(_) => 1,
        Curve2Kind::Arc(_)
        | Curve2Kind::Circle(_)
        | Curve2Kind::Ellipse(_)
        | Curve2Kind::Bezier(_) => REGION_SAMPLES,
    };
    (0..count)
        .map(|index| curve.evaluate(Parameter::new(f64::from(index) / f64::from(count))))
        .collect()
}

fn loop_polygon(profile_loop: &ProfileLoop) -> Vec<Point2> {
    match profile_loop {
        ProfileLoop::Closed { curve, .. } => sample_curve(*curve),
        ProfileLoop::Open(edges) | ProfileLoop::Chain(edges) => edges
            .iter()
            .flat_map(|edge| sample_curve(edge.curve()))
            .collect(),
    }
}

fn polygon_area(polygon: &[Point2]) -> f64 {
    let count = polygon.len();
    if count < 3 {
        return 0.0;
    }
    polygon
        .iter()
        .enumerate()
        .map(|(index, point)| {
            let (x0, y0) = point.coords_mm();
            let (x1, y1) = polygon[(index + 1) % count].coords_mm();
            x0 * y1 - x1 * y0
        })
        .sum::<f64>()
        .abs()
        / 2.0
}

fn point_in_polygon(point: Point2, polygon: &[Point2]) -> bool {
    let (px, py) = point.coords_mm();
    let count = polygon.len();
    (0..count).fold(false, |inside, index| {
        let (xi, yi) = polygon[index].coords_mm();
        let (xj, yj) = polygon[(index + count - 1) % count].coords_mm();
        let crosses = (yi > py) != (yj > py) && px < (xj - xi) * (py - yi) / (yj - yi) + xi;
        inside ^ crosses
    })
}

struct ClosedShape {
    index: usize,
    polygon: Vec<Point2>,
    area: f64,
}

fn closed_shapes(loops: &[ProfileLoop]) -> Vec<ClosedShape> {
    loops
        .iter()
        .enumerate()
        .filter(|(_, profile_loop)| !matches!(profile_loop, ProfileLoop::Chain(_)))
        .map(|(index, profile_loop)| {
            let polygon = loop_polygon(profile_loop);
            let area = polygon_area(&polygon);
            ClosedShape {
                index,
                polygon,
                area,
            }
        })
        .collect()
}

fn immediate_container<'a>(
    shapes: &'a [ClosedShape],
    child: &ClosedShape,
) -> Option<&'a ClosedShape> {
    shapes
        .iter()
        .filter(|candidate| {
            candidate.index != child.index
                && candidate.area > child.area
                && child
                    .polygon
                    .iter()
                    .any(|point| point_in_polygon(*point, &candidate.polygon))
        })
        .min_by(|a, b| a.area.total_cmp(&b.area))
}

fn select_contour(
    loops: &[ProfileLoop],
    seed: SketchEntityId,
) -> Result<Vec<ProfileLoop>, BrepError> {
    let seed_index = loops
        .iter()
        .position(|profile_loop| loop_has_entity(profile_loop, seed))
        .ok_or_else(|| defect(ProfileDefect::UnknownContour))?;
    if matches!(loops[seed_index], ProfileLoop::Chain(_)) {
        return Ok(vec![loops[seed_index].clone()]);
    }
    let shapes = closed_shapes(loops);
    let Some(seed_shape) = shapes.iter().find(|shape| shape.index == seed_index) else {
        return Ok(vec![loops[seed_index].clone()]);
    };
    let outer = immediate_container(&shapes, seed_shape).map_or(seed_index, |shape| shape.index);
    let region: Vec<ProfileLoop> = shapes
        .iter()
        .filter(|shape| {
            shape.index == outer
                || immediate_container(&shapes, shape).is_some_and(|parent| parent.index == outer)
        })
        .map(|shape| loops[shape.index].clone())
        .collect();
    Ok(region)
}

fn defect(reason: ProfileDefect) -> BrepError {
    BrepError::InvalidProfile { reason }
}

fn point_at(sketch: &Sketch, id: SketchEntityId) -> Point2 {
    let Some(SketchEntity::Point(point)) = sketch.entities().get(id) else {
        unreachable!("Sketch validation guarantees entity references resolve to points");
    };
    point.at()
}

fn circle_loops(
    sketch: &Sketch,
    axis: Option<SketchEntityId>,
) -> Result<Vec<ProfileLoop>, BrepError> {
    sketch
        .entity_order()
        .iter()
        .filter(|id| Some(**id) != axis)
        .filter_map(|id| match sketch.entities().get(*id) {
            Some(SketchEntity::Circle(circle)) if !circle.for_construction() => {
                Some((*id, *circle))
            }
            _ => None,
        })
        .map(|(id, circle)| circle_loop(sketch, id, circle))
        .collect()
}

fn circle_loop(
    sketch: &Sketch,
    id: SketchEntityId,
    circle: CircleData,
) -> Result<ProfileLoop, BrepError> {
    let center = point_at(sketch, circle.center());
    let disk = Circle2::new(center, circle.radius(), PROFILE_TOLERANCE)
        .map_err(|_| defect(ProfileDefect::ZeroArea))?;
    Ok(ProfileLoop::Closed {
        curve: Curve2Kind::Circle(disk),
        curve_entity: id,
    })
}

fn ellipse_loops(
    sketch: &Sketch,
    axis: Option<SketchEntityId>,
) -> Result<Vec<ProfileLoop>, BrepError> {
    sketch
        .entity_order()
        .iter()
        .filter(|id| Some(**id) != axis)
        .filter_map(|id| match sketch.entities().get(*id) {
            Some(SketchEntity::Ellipse(ellipse)) if !ellipse.for_construction() => {
                Some((*id, *ellipse))
            }
            _ => None,
        })
        .map(|(id, ellipse)| ellipse_loop(sketch, id, ellipse))
        .collect()
}

fn ellipse_loop(
    sketch: &Sketch,
    id: SketchEntityId,
    ellipse: EllipseData,
) -> Result<ProfileLoop, BrepError> {
    let curve = sketch
        .ellipse_curve(ellipse)
        .ok_or_else(|| defect(ProfileDefect::ZeroArea))?;
    Ok(ProfileLoop::Closed {
        curve: Curve2Kind::Ellipse(curve),
        curve_entity: id,
    })
}

fn spline_loops(
    sketch: &Sketch,
    axis: Option<SketchEntityId>,
) -> Result<Vec<ProfileLoop>, BrepError> {
    sketch
        .entity_order()
        .iter()
        .filter(|id| Some(**id) != axis)
        .filter_map(|id| match sketch.entities().get(*id) {
            Some(SketchEntity::Spline(spline)) if !spline.for_construction() => {
                Some((*id, spline.clone()))
            }
            _ => None,
        })
        .map(|(id, spline)| spline_loop(sketch, id, &spline))
        .collect()
}

fn spline_loop(
    sketch: &Sketch,
    id: SketchEntityId,
    spline: &SplineData,
) -> Result<ProfileLoop, BrepError> {
    let spans = sketch.spline_spans(spline);
    if spans.is_empty() {
        return Err(defect(ProfileDefect::ZeroArea));
    }
    let edges: Vec<ProfileEdge> = spans
        .into_iter()
        .map(|bezier| ProfileEdge::new(Curve2Kind::Bezier(bezier), id, id))
        .collect();
    if signed_area(&edges) < 0.0 {
        return Err(defect(ProfileDefect::ZeroArea));
    }
    Ok(ProfileLoop::Open(edges))
}

fn signed_area(edges: &[ProfileEdge]) -> f64 {
    let points: Vec<(f64, f64)> = edges
        .iter()
        .flat_map(|edge| sample_curve(edge.curve()))
        .map(Point2::coords_mm)
        .collect();
    let n = points.len();
    points
        .iter()
        .enumerate()
        .map(|(i, &(x0, y0))| {
            let (x1, y1) = points[(i + 1) % n];
            x0 * y1 - x1 * y0
        })
        .sum::<f64>()
        / 2.0
}

#[derive(Copy, Clone)]
enum EdgeCurve {
    Line(Line2),
    Arc(Arc2),
}

impl EdgeCurve {
    fn into_kind(self) -> Curve2Kind {
        match self {
            Self::Line(line) => Curve2Kind::Line(line),
            Self::Arc(arc) => Curve2Kind::Arc(arc),
        }
    }

    fn reversed(self) -> Result<Self, BrepError> {
        match self {
            Self::Line(line) => Line2::new(line.end(), line.start(), PROFILE_TOLERANCE)
                .map(Self::Line)
                .map_err(|_| defect(ProfileDefect::ZeroArea)),
            Self::Arc(arc) => Arc2::new(
                arc.center(),
                arc.radius(),
                arc.start_angle() + arc.sweep_angle(),
                -arc.sweep_angle(),
                PROFILE_TOLERANCE,
            )
            .map(Self::Arc)
            .map_err(|_| defect(ProfileDefect::ZeroArea)),
        }
    }
}

#[derive(Copy, Clone)]
struct RawEdge {
    entity: SketchEntityId,
    from: SketchEntityId,
    to: SketchEntityId,
    curve: EdgeCurve,
}

impl RawEdge {
    fn other(self, point: SketchEntityId) -> SketchEntityId {
        debug_assert!(
            point == self.from || point == self.to,
            "other() requires a vertex of this edge"
        );
        if point == self.from {
            self.to
        } else {
            self.from
        }
    }

    fn oriented(self, start: SketchEntityId) -> Result<Curve2Kind, BrepError> {
        if start == self.from {
            Ok(self.curve.into_kind())
        } else {
            self.curve.reversed().map(EdgeCurve::into_kind)
        }
    }
}

fn raw_edges(sketch: &Sketch, axis: Option<SketchEntityId>) -> Result<Vec<RawEdge>, BrepError> {
    sketch
        .entity_order()
        .iter()
        .filter(|id| Some(**id) != axis)
        .filter_map(|id| match sketch.entities().get(*id) {
            Some(SketchEntity::Line(line)) if !line.for_construction() => {
                Some(line_edge(sketch, *id, *line))
            }
            Some(SketchEntity::Arc(arc)) if !arc.for_construction() => {
                Some(arc_edge(sketch, *id, *arc))
            }
            _ => None,
        })
        .collect()
}

fn line_edge(sketch: &Sketch, id: SketchEntityId, line: LineData) -> Result<RawEdge, BrepError> {
    let start = point_at(sketch, line.a());
    let end = point_at(sketch, line.b());
    let segment =
        Line2::new(start, end, PROFILE_TOLERANCE).map_err(|_| defect(ProfileDefect::ZeroArea))?;
    Ok(RawEdge {
        entity: id,
        from: line.a(),
        to: line.b(),
        curve: EdgeCurve::Line(segment),
    })
}

fn arc_edge(sketch: &Sketch, id: SketchEntityId, arc: ArcData) -> Result<RawEdge, BrepError> {
    let center = point_at(sketch, arc.center());
    let start = point_at(sketch, arc.start());
    let end = point_at(sketch, arc.end());
    let curve = Arc2::from_center_start_end(center, start, end, PROFILE_TOLERANCE)
        .map_err(|_| defect(ProfileDefect::ZeroArea))?;
    Ok(RawEdge {
        entity: id,
        from: arc.start(),
        to: arc.end(),
        curve: EdgeCurve::Arc(curve),
    })
}

type Adjacency = BTreeMap<SketchEntityId, Vec<usize>>;

fn chain_loops(
    sketch: &Sketch,
    axis: Option<SketchEntityId>,
) -> Result<Vec<ProfileLoop>, BrepError> {
    let edges = raw_edges(sketch, axis)?;
    if edges.is_empty() {
        return Ok(Vec::new());
    }
    let adjacency = adjacency(&edges)?;
    walk_all(&edges, &adjacency)
}

fn adjacency(edges: &[RawEdge]) -> Result<Adjacency, BrepError> {
    let grouped: Adjacency =
        edges
            .iter()
            .enumerate()
            .fold(BTreeMap::new(), |mut acc, (index, edge)| {
                acc.entry(edge.from).or_default().push(index);
                acc.entry(edge.to).or_default().push(index);
                acc
            });
    grouped
        .values()
        .try_for_each(|indices| match indices.len() {
            1 | 2 => Ok(()),
            _ => Err(defect(ProfileDefect::BranchingVertex)),
        })?;
    Ok(grouped)
}

fn other_edge(adjacency: &Adjacency, point: SketchEntityId, current: usize) -> Option<usize> {
    adjacency
        .get(&point)?
        .iter()
        .copied()
        .find(|&edge| edge != current)
}

#[derive(Copy, Clone)]
struct Step {
    edge: usize,
    from: SketchEntityId,
}

#[derive(Copy, Clone, PartialEq, Eq)]
enum WalkEnd {
    Closed,
    Open,
}

fn profile_edges(edges: &[RawEdge], steps: &[Step]) -> Result<Vec<ProfileEdge>, BrepError> {
    steps
        .iter()
        .map(|step| {
            let edge = edges[step.edge];
            Ok(ProfileEdge::new(
                edge.oriented(step.from)?,
                edge.entity,
                step.from,
            ))
        })
        .collect()
}

fn mark_visited(visited: BTreeSet<usize>, steps: &[Step]) -> BTreeSet<usize> {
    steps.iter().fold(visited, |mut acc, step| {
        acc.insert(step.edge);
        acc
    })
}

fn walk_all(edges: &[RawEdge], adjacency: &Adjacency) -> Result<Vec<ProfileLoop>, BrepError> {
    let endpoints: Vec<(SketchEntityId, usize)> = adjacency
        .iter()
        .filter_map(|(vertex, indices)| match indices.as_slice() {
            [only] => Some((*vertex, *only)),
            _ => None,
        })
        .collect();
    let (visited, chains) = endpoints.iter().try_fold(
        (BTreeSet::<usize>::new(), Vec::<ProfileLoop>::new()),
        |(visited, mut loops), &(origin, start)| {
            if visited.contains(&start) {
                return Ok((visited, loops));
            }
            let (steps, end) = walk_from(edges, adjacency, start, origin);
            if end != WalkEnd::Open {
                return Err(defect(ProfileDefect::OpenLoop));
            }
            let next_visited = mark_visited(visited, &steps);
            loops.push(ProfileLoop::Chain(profile_edges(edges, &steps)?));
            Ok((next_visited, loops))
        },
    )?;
    let (_, cycles) = (0..edges.len()).try_fold(
        (visited, Vec::<ProfileLoop>::new()),
        |(visited, mut loops), start| {
            if visited.contains(&start) {
                return Ok((visited, loops));
            }
            let (steps, end) = walk_from(edges, adjacency, start, edges[start].from);
            if end != WalkEnd::Closed {
                return Err(defect(ProfileDefect::OpenLoop));
            }
            let next_visited = mark_visited(visited, &steps);
            loops.push(ProfileLoop::Open(profile_edges(edges, &steps)?));
            Ok((next_visited, loops))
        },
    )?;
    Ok(chains.into_iter().chain(cycles).collect())
}

enum Walk {
    Going {
        edge: usize,
        from: SketchEntityId,
        steps: Vec<Step>,
    },
    Done(Vec<Step>, WalkEnd),
}

fn walk_from(
    edges: &[RawEdge],
    adjacency: &Adjacency,
    start: usize,
    origin: SketchEntityId,
) -> (Vec<Step>, WalkEnd) {
    let walk = (0..edges.len()).fold(
        Walk::Going {
            edge: start,
            from: origin,
            steps: Vec::new(),
        },
        |state, _| match state {
            Walk::Done(steps, end) => Walk::Done(steps, end),
            Walk::Going {
                edge,
                from,
                mut steps,
            } => {
                let next_point = edges[edge].other(from);
                steps.push(Step { edge, from });
                if next_point == origin {
                    Walk::Done(steps, WalkEnd::Closed)
                } else {
                    match other_edge(adjacency, next_point, edge) {
                        Some(next_edge) => Walk::Going {
                            edge: next_edge,
                            from: next_point,
                            steps,
                        },
                        None => Walk::Done(steps, WalkEnd::Open),
                    }
                }
            }
        },
    );
    match walk {
        Walk::Done(steps, end) => (steps, end),
        Walk::Going { steps, .. } => (steps, WalkEnd::Open),
    }
}

#[cfg(test)]
mod tests {
    use super::{build_profile, build_profile_for};
    use crate::sketch::{EditOutcome, Sketch, SketchEdit, SketchEntity};
    use bone_kernel::{
        BrepError, BrepSolid, ContourSelection, Curve2Kind, ExtrudeFeature, ProfileDefect,
        ProfileLoop, evaluate_extrude,
    };
    use bone_types::{
        FeatureId, Length, Point2, Point3, PositiveLength, SketchEntityId, SketchId,
        SketchPlaneBasis, Tolerance, UnitVec3, millimeter,
    };
    use slotmap::{Key, KeyData};

    const TOL: Tolerance = Tolerance::new(1e-9);

    fn plane() -> SketchPlaneBasis {
        let Ok(basis) = SketchPlaneBasis::new(
            Point3::origin(),
            UnitVec3::x_axis(),
            UnitVec3::y_axis(),
            TOL,
        ) else {
            panic!("xy plane is orthonormal");
        };
        basis
    }

    fn feature_id(idx: u32) -> FeatureId {
        FeatureId::from(KeyData::from_ffi((1u64 << 32) | u64::from(idx)))
    }

    fn point(sketch: Sketch, x: f64, y: f64) -> (Sketch, SketchEntityId) {
        let Ok((next, EditOutcome::Entity(id))) = sketch.apply(SketchEdit::AddEntity(
            SketchEntity::point(Point2::from_mm(x, y)),
        )) else {
            panic!("add point");
        };
        (next, id)
    }

    fn line(
        sketch: Sketch,
        a: SketchEntityId,
        b: SketchEntityId,
        construction: bool,
    ) -> (Sketch, SketchEntityId) {
        let Ok((next, EditOutcome::Entity(id))) = sketch.apply(SketchEdit::AddEntity(
            SketchEntity::line(a, b, construction),
        )) else {
            panic!("add line");
        };
        (next, id)
    }

    fn arc(
        sketch: Sketch,
        center: SketchEntityId,
        start: SketchEntityId,
        end: SketchEntityId,
    ) -> (Sketch, SketchEntityId) {
        let Ok((next, EditOutcome::Entity(id))) = sketch.apply(SketchEdit::AddEntity(
            SketchEntity::arc(center, start, end, false),
        )) else {
            panic!("add arc");
        };
        (next, id)
    }

    fn circle(sketch: Sketch, center: SketchEntityId, radius_mm: f64) -> (Sketch, SketchEntityId) {
        let Ok((next, EditOutcome::Entity(id))) = sketch.apply(SketchEdit::AddEntity(
            SketchEntity::circle(center, Length::new::<millimeter>(radius_mm), false),
        )) else {
            panic!("add circle");
        };
        (next, id)
    }

    fn ellipse(
        sketch: Sketch,
        center: SketchEntityId,
        major: SketchEntityId,
        minor: SketchEntityId,
    ) -> (Sketch, SketchEntityId) {
        let Ok((next, EditOutcome::Entity(id))) = sketch.apply(SketchEdit::AddEntity(
            SketchEntity::ellipse(center, major, minor, false),
        )) else {
            panic!("add ellipse");
        };
        (next, id)
    }

    fn spline(
        sketch: Sketch,
        a: SketchEntityId,
        b: SketchEntityId,
        c: SketchEntityId,
    ) -> (Sketch, SketchEntityId) {
        let Ok((next, EditOutcome::Entity(id))) = sketch.apply(SketchEdit::AddEntity(
            SketchEntity::spline(vec![a, b, c], true, false),
        )) else {
            panic!("add spline");
        };
        (next, id)
    }

    #[test]
    fn ellipse_builds_one_closed_curve_loop() {
        let (sketch, center) = point(Sketch::new(plane()), 0.0, 0.0);
        let (sketch, major) = point(sketch, 4.0, 0.0);
        let (sketch, minor) = point(sketch, 0.0, 2.0);
        let (sketch, _id) = ellipse(sketch, center, major, minor);
        let Ok(profile) = build_profile(&sketch) else {
            panic!("an ellipse is a buildable profile");
        };
        let [single] = profile.loops() else {
            panic!("an ellipse is one loop");
        };
        let ProfileLoop::Closed { curve, .. } = single else {
            panic!("an ellipse lowers to a closed single-curve loop");
        };
        assert!(matches!(curve, Curve2Kind::Ellipse(_)));
    }

    #[test]
    fn spline_builds_a_closed_bezier_loop() {
        let (sketch, a) = point(Sketch::new(plane()), 5.0, 0.0);
        let (sketch, b) = point(sketch, 8.0, 4.0);
        let (sketch, c) = point(sketch, 5.0, 8.0);
        let (sketch, _id) = spline(sketch, a, b, c);
        let Ok(profile) = build_profile(&sketch) else {
            panic!("a spline is a buildable profile");
        };
        let [single] = profile.loops() else {
            panic!("a spline is one loop");
        };
        let ProfileLoop::Open(edges) = single else {
            panic!("a spline lowers to a closed bezier loop");
        };
        assert_eq!(
            edges.len(),
            3,
            "three control points give three bezier spans"
        );
        assert!(
            edges
                .iter()
                .all(|edge| matches!(edge.curve(), Curve2Kind::Bezier(_))),
            "every spline span is a bezier curve"
        );
    }

    fn blind(depth_mm: f64) -> ExtrudeFeature {
        let Ok(depth) = PositiveLength::new(Length::new::<millimeter>(depth_mm)) else {
            panic!("{depth_mm} mm is positive");
        };
        ExtrudeFeature::blind(SketchId::null(), depth)
    }

    fn extrude(sketch: &Sketch) -> Result<BrepSolid, BrepError> {
        let profile = build_profile(sketch)?;
        evaluate_extrude(feature_id(1), &profile, &blind(4.0))
    }

    fn corners(sketch: Sketch) -> (Sketch, [SketchEntityId; 4]) {
        let (sketch, p0) = point(sketch, 0.0, 0.0);
        let (sketch, p1) = point(sketch, 10.0, 0.0);
        let (sketch, p2) = point(sketch, 10.0, 5.0);
        let (sketch, p3) = point(sketch, 0.0, 5.0);
        (sketch, [p0, p1, p2, p3])
    }

    #[test]
    fn rectangle_builds_one_quad_loop() {
        let (sketch, [p0, p1, p2, p3]) = corners(Sketch::new(plane()));
        let (sketch, l0) = line(sketch, p0, p1, false);
        let (sketch, l1) = line(sketch, p1, p2, false);
        let (sketch, l2) = line(sketch, p2, p3, false);
        let (sketch, l3) = line(sketch, p3, p0, false);

        let Ok(profile) = build_profile(&sketch) else {
            panic!("rectangle is a buildable profile");
        };
        let [single] = profile.loops() else {
            panic!("rectangle is one loop");
        };
        let ProfileLoop::Open(edges) = single else {
            panic!("a line loop is open");
        };
        assert_eq!(edges.len(), 4);
        let entities: Vec<SketchEntityId> = edges.iter().map(|edge| edge.curve_entity()).collect();
        assert_eq!(entities, vec![l0, l1, l2, l3]);
        let walk_corners: Vec<SketchEntityId> = edges.iter().map(|edge| edge.corner()).collect();
        assert_eq!(walk_corners, vec![p0, p1, p2, p3]);
        assert!(
            edges
                .iter()
                .all(|edge| matches!(edge.curve(), Curve2Kind::Line(_)))
        );
    }

    #[test]
    fn rectangle_extrudes_to_closed_solid() {
        let (sketch, [p0, p1, p2, p3]) = corners(Sketch::new(plane()));
        let (sketch, _) = line(sketch, p0, p1, false);
        let (sketch, _) = line(sketch, p1, p2, false);
        let (sketch, _) = line(sketch, p2, p3, false);
        let (sketch, _) = line(sketch, p3, p0, false);
        let Ok(solid) = extrude(&sketch) else {
            panic!("rectangle extrudes");
        };
        assert_eq!(solid.iter_faces().count(), 6);
        assert!(solid.validate(TOL).is_ok());
    }

    #[test]
    fn mixed_edge_orientation_still_assembles() {
        let (sketch, [p0, p1, p2, p3]) = corners(Sketch::new(plane()));
        let (sketch, _) = line(sketch, p0, p1, false);
        let (sketch, _) = line(sketch, p2, p1, false);
        let (sketch, _) = line(sketch, p2, p3, false);
        let (sketch, _) = line(sketch, p0, p3, false);
        let Ok(solid) = extrude(&sketch) else {
            panic!("the walk reverses backwards edges");
        };
        assert_eq!(solid.iter_faces().count(), 6);
        assert!(solid.validate(TOL).is_ok());
    }

    #[test]
    fn circle_builds_one_closed_loop() {
        let (sketch, center) = point(Sketch::new(plane()), 0.0, 0.0);
        let (sketch, disk) = circle(sketch, center, 5.0);
        let Ok(profile) = build_profile(&sketch) else {
            panic!("circle is a buildable profile");
        };
        let [
            ProfileLoop::Closed {
                curve_entity,
                curve,
            },
        ] = profile.loops()
        else {
            panic!("a circle is one closed loop");
        };
        assert_eq!(*curve_entity, disk);
        assert!(matches!(curve, Curve2Kind::Circle(_)));
        let Ok(solid) = extrude(&sketch) else {
            panic!("circle extrudes");
        };
        assert!(solid.validate(TOL).is_ok());
    }

    #[test]
    fn arc_and_chord_assemble_into_a_half_disk() {
        let (sketch, center) = point(Sketch::new(plane()), 0.0, 0.0);
        let (sketch, start) = point(sketch, -5.0, 0.0);
        let (sketch, end) = point(sketch, 5.0, 0.0);
        let (sketch, arc_id) = arc(sketch, center, start, end);
        let (sketch, _) = line(sketch, end, start, false);

        let Ok(profile) = build_profile(&sketch) else {
            panic!("arc plus chord is buildable");
        };
        let [ProfileLoop::Open(edges)] = profile.loops() else {
            panic!("half disk is one open loop");
        };
        assert_eq!(edges.len(), 2);
        let Some(Curve2Kind::Arc(forward)) = edges
            .iter()
            .find(|edge| edge.curve_entity() == arc_id)
            .map(|edge| edge.curve())
        else {
            panic!("arc edge present in the loop");
        };
        assert!(
            forward.sweep_rad() > 0.0 && (forward.sweep_rad() - core::f64::consts::PI).abs() < 1e-9,
            "forward ordering sweeps ccw through the bottom semicircle"
        );
        let Ok(solid) = extrude(&sketch) else {
            panic!("half disk extrudes");
        };
        assert!(solid.validate(TOL).is_ok());
    }

    #[test]
    fn reversed_arc_assembles_like_the_forward_arc() {
        let (sketch, center) = point(Sketch::new(plane()), 0.0, 0.0);
        let (sketch, start) = point(sketch, -5.0, 0.0);
        let (sketch, end) = point(sketch, 5.0, 0.0);
        let (sketch, _) = line(sketch, start, end, false);
        let (sketch, arc_id) = arc(sketch, center, start, end);

        let Ok(profile) = build_profile(&sketch) else {
            panic!("arc plus chord is buildable in either order");
        };
        let [ProfileLoop::Open(edges)] = profile.loops() else {
            panic!("half disk is one open loop");
        };
        let Some(Curve2Kind::Arc(reversed)) = edges
            .iter()
            .find(|edge| edge.curve_entity() == arc_id)
            .map(|edge| edge.curve())
        else {
            panic!("arc edge present in the loop");
        };
        assert!(
            reversed.sweep_rad() < 0.0,
            "chord-first ordering makes the walk traverse the arc backward"
        );
        let Ok(solid) = extrude(&sketch) else {
            panic!("reversed-arc half disk extrudes");
        };
        assert!(solid.validate(TOL).is_ok());
    }

    #[test]
    fn branching_vertex_is_distinct_from_open_loop() {
        let (sketch, [p0, p1, p2, p3]) = corners(Sketch::new(plane()));
        let (sketch, _) = line(sketch, p0, p1, false);
        let (sketch, _) = line(sketch, p1, p2, false);
        let (sketch, _) = line(sketch, p2, p3, false);
        let (sketch, _) = line(sketch, p3, p0, false);
        let (sketch, _) = line(sketch, p0, p2, false);
        assert!(matches!(
            build_profile(&sketch),
            Err(BrepError::InvalidProfile {
                reason: ProfileDefect::BranchingVertex
            })
        ));
    }

    #[test]
    fn open_chain_builds_a_chain_loop_that_cannot_extrude() {
        let (sketch, [p0, p1, p2, p3]) = corners(Sketch::new(plane()));
        let (sketch, _) = line(sketch, p0, p1, false);
        let (sketch, _) = line(sketch, p1, p2, false);
        let (sketch, _) = line(sketch, p2, p3, false);
        let Ok(profile) = build_profile(&sketch) else {
            panic!("an open chain builds as a chain loop for thin features");
        };
        let [ProfileLoop::Chain(edges)] = profile.loops() else {
            panic!("three connected segments are one open chain");
        };
        assert_eq!(edges.len(), 3);
        assert!(
            matches!(
                extrude(&sketch),
                Err(BrepError::InvalidProfile {
                    reason: ProfileDefect::OpenLoop
                })
            ),
            "an open chain without a thin wall cannot bound a solid"
        );
    }

    #[test]
    fn construction_geometry_is_excluded() {
        let (sketch, [p0, p1, p2, p3]) = corners(Sketch::new(plane()));
        let (sketch, _) = line(sketch, p0, p1, false);
        let (sketch, _) = line(sketch, p1, p2, false);
        let (sketch, _) = line(sketch, p2, p3, false);
        let (sketch, _) = line(sketch, p3, p0, false);
        let (sketch, diag_a) = point(sketch, 0.0, 0.0);
        let (sketch, diag_b) = point(sketch, 10.0, 5.0);
        let (sketch, _) = line(sketch, diag_a, diag_b, true);

        let Ok(profile) = build_profile(&sketch) else {
            panic!("construction line does not break the profile");
        };
        assert_eq!(profile.loops().len(), 1);
    }

    #[test]
    fn nested_line_loops_extrude_to_a_tube() {
        let (sketch, [o0, o1, o2, o3]) = corners(Sketch::new(plane()));
        let (sketch, _) = line(sketch, o0, o1, false);
        let (sketch, _) = line(sketch, o1, o2, false);
        let (sketch, _) = line(sketch, o2, o3, false);
        let (sketch, _) = line(sketch, o3, o0, false);
        let (sketch, i0) = point(sketch, 3.0, 1.0);
        let (sketch, i1) = point(sketch, 7.0, 1.0);
        let (sketch, i2) = point(sketch, 7.0, 4.0);
        let (sketch, i3) = point(sketch, 3.0, 4.0);
        let (sketch, _) = line(sketch, i0, i1, false);
        let (sketch, _) = line(sketch, i1, i2, false);
        let (sketch, _) = line(sketch, i2, i3, false);
        let (sketch, _) = line(sketch, i3, i0, false);

        let Ok(profile) = build_profile(&sketch) else {
            panic!("rectangle with a rectangular hole is buildable");
        };
        assert_eq!(profile.loops().len(), 2);
        assert!(
            profile
                .loops()
                .iter()
                .all(|loop_| matches!(loop_, ProfileLoop::Open(edges) if edges.len() == 4))
        );
        let Ok(solid) = extrude(&sketch) else {
            panic!("nested loops extrude to a tube");
        };
        assert!(solid.validate(TOL).is_ok());
        assert_eq!(solid.iter_faces().count(), 10);
    }

    #[test]
    fn selecting_one_of_two_disjoint_contours_keeps_only_that_region() {
        let (sketch, c0) = point(Sketch::new(plane()), 0.0, 0.0);
        let (sketch, first) = circle(sketch, c0, 5.0);
        let (sketch, c1) = point(sketch, 30.0, 0.0);
        let (sketch, second) = circle(sketch, c1, 3.0);

        let Ok(all) = build_profile_for(&sketch, ContourSelection::All, None) else {
            panic!("two circles build as two contours");
        };
        assert_eq!(all.loops().len(), 2, "all contours keep both circles");

        let Ok(one) = build_profile_for(&sketch, ContourSelection::Contour(first), None) else {
            panic!("the selected circle builds");
        };
        let [ProfileLoop::Closed { curve_entity, .. }] = one.loops() else {
            panic!("one contour is one closed loop");
        };
        assert_eq!(*curve_entity, first, "the kept loop is the selected circle");
        let _ = second;
    }

    #[test]
    fn selecting_an_outer_contour_keeps_its_holes() {
        let (sketch, [o0, o1, o2, o3]) = corners(Sketch::new(plane()));
        let (sketch, outer) = line(sketch, o0, o1, false);
        let (sketch, _) = line(sketch, o1, o2, false);
        let (sketch, _) = line(sketch, o2, o3, false);
        let (sketch, _) = line(sketch, o3, o0, false);
        let (sketch, i0) = point(sketch, 3.0, 1.0);
        let (sketch, i1) = point(sketch, 7.0, 1.0);
        let (sketch, i2) = point(sketch, 7.0, 4.0);
        let (sketch, i3) = point(sketch, 3.0, 4.0);
        let (sketch, inner) = line(sketch, i0, i1, false);
        let (sketch, _) = line(sketch, i1, i2, false);
        let (sketch, _) = line(sketch, i2, i3, false);
        let (sketch, _) = line(sketch, i3, i0, false);

        let Ok(from_outer) = build_profile_for(&sketch, ContourSelection::Contour(outer), None)
        else {
            panic!("selecting the outer boundary builds its region");
        };
        assert_eq!(
            from_outer.loops().len(),
            2,
            "an outer contour drags its hole along"
        );
        let Ok(from_inner) = build_profile_for(&sketch, ContourSelection::Contour(inner), None)
        else {
            panic!("selecting the inner boundary builds the same region");
        };
        assert_eq!(
            from_inner.loops().len(),
            2,
            "selecting a hole resolves to its containing region"
        );
    }

    #[test]
    fn an_unknown_contour_seed_is_rejected() {
        let (sketch, center) = point(Sketch::new(plane()), 0.0, 0.0);
        let (sketch, disk) = circle(sketch, center, 5.0);
        assert!(
            build_profile_for(&sketch, ContourSelection::Contour(disk), None).is_ok(),
            "the circle curve names its own contour"
        );
        let (sketch, stray) = point(sketch, 1.0, 1.0);
        assert!(
            matches!(
                build_profile_for(&sketch, ContourSelection::Contour(stray), None),
                Err(BrepError::InvalidProfile {
                    reason: ProfileDefect::UnknownContour
                })
            ),
            "a seed that names no contour curve is rejected"
        );
    }
}
