use bone_document::{
    ArcData, CircleData, DimensionValue, LineData, Sketch, SketchDimension, SketchEntity,
    SketchRelation,
};
use bone_kernel::{Aabb2, Arc2, arc_bounding_box};
use bone_types::{
    Angle, Length, Point2, SketchDimensionId, SketchEntityId, SketchRelationId, Tolerance, Vec2,
};
use core::f64::consts::FRAC_1_SQRT_2;
use uom::si::angle::degree;
use uom::si::length::millimeter;

use crate::pick::{EntityKindTag, PickId, PickIdError, PickIndex};

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

#[derive(Copy, Clone, Debug, PartialEq)]
pub struct ScenePoint {
    at: Point2,
    entity: SketchEntityId,
    pick: PickId,
}

impl ScenePoint {
    #[must_use]
    pub const fn at(self) -> Point2 {
        self.at
    }

    #[must_use]
    pub const fn pick(self) -> PickId {
        self.pick
    }
}

#[derive(Copy, Clone, Debug, PartialEq)]
pub struct SceneLine {
    a: Point2,
    b: Point2,
    entity: SketchEntityId,
    pick: PickId,
    for_construction: bool,
}

impl SceneLine {
    #[must_use]
    pub const fn a(self) -> Point2 {
        self.a
    }

    #[must_use]
    pub const fn b(self) -> Point2 {
        self.b
    }

    #[must_use]
    pub const fn pick(self) -> PickId {
        self.pick
    }

    #[must_use]
    pub const fn for_construction(self) -> bool {
        self.for_construction
    }
}

#[derive(Copy, Clone, Debug, PartialEq)]
pub struct SceneArc {
    center: Point2,
    radius: Length,
    start_angle: Angle,
    sweep_angle: Angle,
    entity: SketchEntityId,
    pick: PickId,
    for_construction: bool,
}

impl SceneArc {
    #[must_use]
    pub const fn center(self) -> Point2 {
        self.center
    }

    #[must_use]
    pub const fn radius(self) -> Length {
        self.radius
    }

    #[must_use]
    pub const fn start_angle(self) -> Angle {
        self.start_angle
    }

    #[must_use]
    pub const fn sweep_angle(self) -> Angle {
        self.sweep_angle
    }

    #[must_use]
    pub const fn pick(self) -> PickId {
        self.pick
    }

    #[must_use]
    pub const fn for_construction(self) -> bool {
        self.for_construction
    }
}

#[derive(Copy, Clone, Debug, PartialEq)]
pub struct SceneCircle {
    center: Point2,
    radius: Length,
    entity: SketchEntityId,
    pick: PickId,
    for_construction: bool,
}

impl SceneCircle {
    #[must_use]
    pub const fn center(self) -> Point2 {
        self.center
    }

    #[must_use]
    pub const fn radius(self) -> Length {
        self.radius
    }

    #[must_use]
    pub const fn pick(self) -> PickId {
        self.pick
    }

    #[must_use]
    pub const fn for_construction(self) -> bool {
        self.for_construction
    }
}

#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
#[repr(u32)]
pub enum RelationGlyphKind {
    Coincident = 0,
    Horizontal = 1,
    Vertical = 2,
    Parallel = 3,
    Perpendicular = 4,
    Tangent = 5,
    Equal = 6,
    Concentric = 7,
    Fix = 8,
    Midpoint = 9,
    Symmetric = 10,
}

impl RelationGlyphKind {
    #[must_use]
    pub const fn tile_index(self) -> u32 {
        self as u32
    }

    #[must_use]
    pub const fn from_index(idx: u32) -> Option<Self> {
        match idx {
            0 => Some(Self::Coincident),
            1 => Some(Self::Horizontal),
            2 => Some(Self::Vertical),
            3 => Some(Self::Parallel),
            4 => Some(Self::Perpendicular),
            5 => Some(Self::Tangent),
            6 => Some(Self::Equal),
            7 => Some(Self::Concentric),
            8 => Some(Self::Fix),
            9 => Some(Self::Midpoint),
            10 => Some(Self::Symmetric),
            _ => None,
        }
    }

    #[must_use]
    pub const fn from_relation(rel: SketchRelation) -> Self {
        match rel {
            SketchRelation::Coincident(_, _) => Self::Coincident,
            SketchRelation::Horizontal(_) => Self::Horizontal,
            SketchRelation::Vertical(_) => Self::Vertical,
            SketchRelation::Parallel(_, _) => Self::Parallel,
            SketchRelation::Perpendicular(_, _) => Self::Perpendicular,
            SketchRelation::Tangent(_, _) => Self::Tangent,
            SketchRelation::Equal(_, _) => Self::Equal,
            SketchRelation::Concentric(_, _) => Self::Concentric,
            SketchRelation::Midpoint { .. } => Self::Midpoint,
            SketchRelation::Symmetric { .. } => Self::Symmetric,
            SketchRelation::Fix(_) => Self::Fix,
        }
    }

    #[must_use]
    pub const fn all() -> [Self; 11] {
        [
            Self::Coincident,
            Self::Horizontal,
            Self::Vertical,
            Self::Parallel,
            Self::Perpendicular,
            Self::Tangent,
            Self::Equal,
            Self::Concentric,
            Self::Fix,
            Self::Midpoint,
            Self::Symmetric,
        ]
    }
}

#[derive(Copy, Clone, Debug, PartialEq)]
pub struct SceneRelationGlyph {
    anchor_mm: Point2,
    offset_dir: Vec2,
    kind: RelationGlyphKind,
    relation: SketchRelationId,
    pick: PickId,
}

impl SceneRelationGlyph {
    #[must_use]
    pub const fn anchor_mm(self) -> Point2 {
        self.anchor_mm
    }

    #[must_use]
    pub const fn offset_dir(self) -> Vec2 {
        self.offset_dir
    }

    #[must_use]
    pub const fn kind(self) -> RelationGlyphKind {
        self.kind
    }

    #[must_use]
    pub const fn relation(self) -> SketchRelationId {
        self.relation
    }

    #[must_use]
    pub const fn pick(self) -> PickId {
        self.pick
    }
}

#[derive(Clone, Debug, PartialEq)]
pub struct SceneDimension {
    anchor_mm: Point2,
    text: String,
    dimension: SketchDimensionId,
    pick: PickId,
}

impl SceneDimension {
    #[must_use]
    pub const fn anchor_mm(&self) -> Point2 {
        self.anchor_mm
    }

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

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

    #[must_use]
    pub const fn pick(&self) -> PickId {
        self.pick
    }
}

#[derive(Clone, Debug, Default, PartialEq)]
pub struct SketchScene {
    points: Vec<ScenePoint>,
    lines: Vec<SceneLine>,
    arcs: Vec<SceneArc>,
    circles: Vec<SceneCircle>,
    relations: Vec<SceneRelationGlyph>,
    dimensions: Vec<SceneDimension>,
}

impl SketchScene {
    #[must_use]
    pub const fn empty() -> Self {
        Self {
            points: Vec::new(),
            lines: Vec::new(),
            arcs: Vec::new(),
            circles: Vec::new(),
            relations: Vec::new(),
            dimensions: Vec::new(),
        }
    }

    pub fn extract(sketch: &Sketch) -> Result<Self, PickIdError> {
        let with_entities = sketch
            .entity_order()
            .iter()
            .copied()
            .try_fold(Self::empty(), |acc, id| acc.push_from_sketch(sketch, id))?;
        let with_relations = sketch
            .relation_order()
            .iter()
            .copied()
            .try_fold(with_entities, |acc, id| acc.push_relation(sketch, id))?;
        sketch
            .dimension_order()
            .iter()
            .copied()
            .try_fold(with_relations, |acc, id| acc.push_dimension(sketch, id))
    }

    #[must_use]
    pub fn points(&self) -> &[ScenePoint] {
        &self.points
    }

    #[must_use]
    pub fn lines(&self) -> &[SceneLine] {
        &self.lines
    }

    #[must_use]
    pub fn arcs(&self) -> &[SceneArc] {
        &self.arcs
    }

    #[must_use]
    pub fn circles(&self) -> &[SceneCircle] {
        &self.circles
    }

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

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

    #[must_use]
    pub fn is_empty(&self) -> bool {
        self.points.is_empty()
            && self.lines.is_empty()
            && self.arcs.is_empty()
            && self.circles.is_empty()
            && self.relations.is_empty()
            && self.dimensions.is_empty()
    }

    #[must_use]
    pub fn aabb(&self) -> Option<Aabb2> {
        let points = self
            .points
            .iter()
            .map(|p| Aabb2::from_corners(p.at(), p.at()));
        let lines = self.lines.iter().map(|l| Aabb2::from_corners(l.a(), l.b()));
        let circles = self.circles.iter().map(|c| {
            let (cx, cy) = c.center().coords_mm();
            let r = c.radius().get::<millimeter>();
            Aabb2::from_corners(
                Point2::from_mm(cx - r, cy - r),
                Point2::from_mm(cx + r, cy + r),
            )
        });
        let arcs = self
            .arcs
            .iter()
            .map(|a| arc_bounding_box(a.center(), a.radius(), a.start_angle(), a.sweep_angle()));
        points
            .chain(lines)
            .chain(circles)
            .chain(arcs)
            .reduce(|a, b| a.extend_point(b.min()).extend_point(b.max()))
    }

    pub fn pick_index(&self) -> Result<PickIndex, PickIdError> {
        let entities = self
            .points
            .iter()
            .map(|p| (p.entity, EntityKindTag::Point))
            .chain(self.lines.iter().map(|l| (l.entity, EntityKindTag::Line)))
            .chain(self.arcs.iter().map(|a| (a.entity, EntityKindTag::Arc)))
            .chain(
                self.circles
                    .iter()
                    .map(|c| (c.entity, EntityKindTag::Circle)),
            );
        PickIndex::build(
            entities,
            self.relations.iter().map(|g| g.relation),
            self.dimensions.iter().map(|d| d.dimension),
        )
    }

    fn push_relation(mut self, sketch: &Sketch, id: SketchRelationId) -> Result<Self, PickIdError> {
        let Some(rel) = sketch.relations().get(id).copied() else {
            return Ok(self);
        };
        let Some((anchor, offset_dir)) = relation_anchor(sketch, rel) else {
            return Ok(self);
        };
        self.relations.push(SceneRelationGlyph {
            anchor_mm: anchor,
            offset_dir,
            kind: RelationGlyphKind::from_relation(rel),
            relation: id,
            pick: PickId::relation(id)?,
        });
        Ok(self)
    }

    fn push_dimension(
        mut self,
        sketch: &Sketch,
        id: SketchDimensionId,
    ) -> Result<Self, PickIdError> {
        let Some(dim) = sketch.dimensions().get(id).copied() else {
            return Ok(self);
        };
        let Some(anchor) = dimension_anchor(sketch, dim) else {
            return Ok(self);
        };
        self.dimensions.push(SceneDimension {
            anchor_mm: anchor,
            text: format_dimension(dim),
            dimension: id,
            pick: PickId::dimension(id)?,
        });
        Ok(self)
    }

    fn push_from_sketch(
        mut self,
        sketch: &Sketch,
        id: SketchEntityId,
    ) -> Result<Self, PickIdError> {
        let Some(entity) = sketch.entities().get(id).copied() else {
            unreachable!(
                "entity_order references id {id:?} missing from entities — Sketch invariants broken"
            );
        };
        match entity {
            SketchEntity::Point(p) => {
                self.points.push(ScenePoint {
                    at: p.at(),
                    entity: id,
                    pick: PickId::point(id)?,
                });
            }
            SketchEntity::Line(l) => {
                self.lines.push(SceneLine {
                    a: point_position(sketch, l.a()),
                    b: point_position(sketch, l.b()),
                    entity: id,
                    pick: PickId::line(id)?,
                    for_construction: l.for_construction(),
                });
            }
            SketchEntity::Arc(a) => {
                if let Some(arc) = derive_arc(sketch, id, a, PickId::arc(id)?) {
                    self.arcs.push(arc);
                }
            }
            SketchEntity::Circle(c) => {
                self.circles.push(SceneCircle {
                    center: point_position(sketch, c.center()),
                    radius: c.radius(),
                    entity: id,
                    pick: PickId::circle(id)?,
                    for_construction: c.for_construction(),
                });
            }
        }
        Ok(self)
    }
}

fn point_position(sketch: &Sketch, id: SketchEntityId) -> Point2 {
    let Some(entity) = sketch.entities().get(id) else {
        unreachable!(
            "SketchEntityId {id:?} missing from sketch entities — Sketch invariants broken"
        );
    };
    let SketchEntity::Point(p) = entity else {
        unreachable!(
            "expected Point at {id:?}, got {:?} — Sketch::apply guarantees references resolve to Points",
            entity.kind()
        );
    };
    p.at()
}

fn relation_anchor(sketch: &Sketch, rel: SketchRelation) -> Option<(Point2, Vec2)> {
    rel.references()
        .into_iter()
        .next()
        .and_then(|id| entity_anchor(sketch, id))
}

fn entity_anchor(sketch: &Sketch, id: SketchEntityId) -> Option<(Point2, Vec2)> {
    let e = sketch.entities().get(id)?;
    Some(match *e {
        SketchEntity::Point(p) => (p.at(), Vec2::from_mm(FRAC_1_SQRT_2, FRAC_1_SQRT_2)),
        SketchEntity::Line(l) => line_anchor(sketch, l),
        SketchEntity::Arc(a) => arc_anchor(sketch, a),
        SketchEntity::Circle(c) => circle_anchor(sketch, c),
    })
}

fn line_anchor(sketch: &Sketch, line: LineData) -> (Point2, Vec2) {
    let (ax, ay) = point_position(sketch, line.a()).coords_mm();
    let (bx, by) = point_position(sketch, line.b()).coords_mm();
    let mid = Point2::from_mm(0.5 * (ax + bx), 0.5 * (ay + by));
    let tangent = Vec2::from_mm(bx - ax, by - ay);
    (mid, unit_or(tangent.perp_ccw(), Vec2::from_mm(0.0, 1.0)))
}

fn arc_anchor(sketch: &Sketch, arc: ArcData) -> (Point2, Vec2) {
    let center = point_position(sketch, arc.center());
    let start = point_position(sketch, arc.start());
    let (cx, cy) = center.coords_mm();
    let (sx, sy) = start.coords_mm();
    let radial = Vec2::from_mm(sx - cx, sy - cy);
    (start, unit_or(radial, Vec2::from_mm(1.0, 0.0)))
}

fn circle_anchor(sketch: &Sketch, circle: CircleData) -> (Point2, Vec2) {
    let (cx, cy) = point_position(sketch, circle.center()).coords_mm();
    let r_mm = circle.radius().get::<millimeter>();
    (Point2::from_mm(cx + r_mm, cy), Vec2::from_mm(1.0, 0.0))
}

fn dimension_anchor(sketch: &Sketch, dim: SketchDimension) -> Option<Point2> {
    match dim {
        SketchDimension::Linear { a, b, .. } | SketchDimension::Angular { a, b, .. } => {
            let (ax, ay) = entity_anchor(sketch, a)?.0.coords_mm();
            let (bx, by) = entity_anchor(sketch, b)?.0.coords_mm();
            Some(Point2::from_mm(0.5 * (ax + bx), 0.5 * (ay + by)))
        }
        SketchDimension::Radius { target, .. } | SketchDimension::Diameter { target, .. } => {
            target_center(sketch, target)
        }
    }
}

fn target_center(sketch: &Sketch, id: SketchEntityId) -> Option<Point2> {
    match sketch.entities().get(id)? {
        SketchEntity::Arc(a) => Some(point_position(sketch, a.center())),
        SketchEntity::Circle(c) => Some(point_position(sketch, c.center())),
        _ => None,
    }
}

fn format_dimension(dim: SketchDimension) -> String {
    let body = match (dim, dim.value()) {
        (SketchDimension::Linear { .. }, DimensionValue::Length(len)) => {
            format!("{:.2} mm", len.get::<millimeter>())
        }
        (SketchDimension::Radius { .. }, DimensionValue::Length(len)) => {
            format!("R {:.2}", len.get::<millimeter>())
        }
        (SketchDimension::Diameter { .. }, DimensionValue::Length(len)) => {
            format!("D {:.2}", len.get::<millimeter>())
        }
        (SketchDimension::Angular { .. }, DimensionValue::Angle(a)) => {
            format!("{:.1}°", a.get::<degree>())
        }
        _ => unreachable!(
            "SketchDimension::value pins Length for Linear/Radius/Diameter and Angle for Angular"
        ),
    };
    match dim.kind() {
        bone_document::DimensionKind::Driving => body,
        bone_document::DimensionKind::Driven => format!("({body})"),
    }
}

fn unit_or(v: Vec2, fallback: Vec2) -> Vec2 {
    let len = v.norm_mm();
    if len > 1e-9 {
        let (x, y) = v.coords_mm();
        Vec2::from_mm(x / len, y / len)
    } else {
        fallback
    }
}

fn derive_arc(
    sketch: &Sketch,
    id: SketchEntityId,
    data: bone_document::ArcData,
    pick: PickId,
) -> Option<SceneArc> {
    let center = point_position(sketch, data.center());
    let start = point_position(sketch, data.start());
    let end = point_position(sketch, data.end());
    let arc = Arc2::from_center_start_end(center, start, end, ARC_TOLERANCE).ok()?;
    Some(SceneArc {
        center: arc.center(),
        radius: arc.radius(),
        start_angle: arc.start_angle(),
        sweep_angle: arc.sweep_angle(),
        entity: id,
        pick,
        for_construction: data.for_construction(),
    })
}

#[cfg(test)]
mod tests {
    use super::*;
    use bone_document::{EditOutcome, SketchEdit};
    use bone_types::{Point3, SketchPlaneBasis, Tolerance, UnitVec3};
    use uom::si::angle::radian;
    use uom::si::length::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 add_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 add_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 add_circle(
        sketch: Sketch,
        center: SketchEntityId,
        radius_mm: f64,
    ) -> (Sketch, SketchEntityId) {
        let Ok((next, EditOutcome::Entity(id))) = sketch.apply(SketchEdit::AddEntity(
            SketchEntity::circle(center, len_mm(radius_mm), false),
        )) else {
            panic!("add circle");
        };
        (next, id)
    }

    fn add_arc_ids(
        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)
    }

    #[test]
    fn empty_scene_has_no_items() {
        let s = Sketch::new(plane());
        let Ok(scene) = SketchScene::extract(&s) else {
            panic!("extract empty");
        };
        assert!(scene.is_empty());
    }

    #[test]
    fn rectangle_extracts_four_points_and_four_lines() {
        let s = Sketch::new(plane());
        let (s, p0) = add_point(s, 0.0, 0.0);
        let (s, p1) = add_point(s, 10.0, 0.0);
        let (s, p2) = add_point(s, 10.0, 5.0);
        let (s, p3) = add_point(s, 0.0, 5.0);
        let (s, _) = add_line(s, p0, p1, false);
        let (s, _) = add_line(s, p1, p2, false);
        let (s, _) = add_line(s, p2, p3, false);
        let (s, _) = add_line(s, p3, p0, false);
        let Ok(scene) = SketchScene::extract(&s) else {
            panic!("extract");
        };
        assert_eq!(scene.points().len(), 4);
        assert_eq!(scene.lines().len(), 4);
        assert!(scene.arcs().is_empty());
        assert!(scene.circles().is_empty());
        assert_eq!(scene.lines()[0].a(), Point2::from_mm(0.0, 0.0));
        assert_eq!(scene.lines()[0].b(), Point2::from_mm(10.0, 0.0));
    }

    #[test]
    fn construction_flag_propagates_to_line() {
        let s = Sketch::new(plane());
        let (s, a) = add_point(s, 0.0, 0.0);
        let (s, b) = add_point(s, 3.0, 4.0);
        let (s, _) = add_line(s, a, b, true);
        let Ok(scene) = SketchScene::extract(&s) else {
            panic!("extract");
        };
        assert!(scene.lines()[0].for_construction());
    }

    #[test]
    fn circle_extracts_with_center_and_radius() {
        let s = Sketch::new(plane());
        let (s, c) = add_point(s, 1.0, 2.0);
        let (s, _) = add_circle(s, c, 4.0);
        let Ok(scene) = SketchScene::extract(&s) else {
            panic!("extract");
        };
        assert_eq!(scene.circles().len(), 1);
        assert_eq!(scene.circles()[0].center(), Point2::from_mm(1.0, 2.0));
        assert!((scene.circles()[0].radius().get::<millimeter>() - 4.0).abs() < 1e-12);
    }

    #[test]
    fn arc_derives_radius_and_ccw_sweep() {
        let s = Sketch::new(plane());
        let (s, c) = add_point(s, 0.0, 0.0);
        let (s, start) = add_point(s, 5.0, 0.0);
        let (s, end) = add_point(s, 0.0, 5.0);
        let (s, _) = add_arc_ids(s, c, start, end);
        let Ok(scene) = SketchScene::extract(&s) else {
            panic!("extract");
        };
        let arc = scene.arcs()[0];
        assert!((arc.radius().get::<millimeter>() - 5.0).abs() < 1e-12);
        let start_rad = arc.start_angle().get::<radian>();
        let sweep_rad = arc.sweep_angle().get::<radian>();
        assert!(start_rad.abs() < 1e-12);
        assert!((sweep_rad - core::f64::consts::FRAC_PI_2).abs() < 1e-12);
    }

    #[test]
    fn pick_index_round_trips_each_scene_item() {
        use crate::pick::PickedItem;
        let s = Sketch::new(plane());
        let (s, p0) = add_point(s, 0.0, 0.0);
        let (s, p1) = add_point(s, 1.0, 0.0);
        let (s, line) = add_line(s, p0, p1, false);
        let (s, cc) = add_point(s, 5.0, 0.0);
        let (s, circle) = add_circle(s, cc, 2.0);
        let (s, ac) = add_point(s, 10.0, 0.0);
        let (s, a_start) = add_point(s, 12.0, 0.0);
        let (s, a_end) = add_point(s, 10.0, 2.0);
        let (s, arc) = add_arc_ids(s, ac, a_start, a_end);
        let Ok(scene) = SketchScene::extract(&s) else {
            panic!("extract");
        };
        let Ok(index) = scene.pick_index() else {
            panic!("pick index");
        };
        assert_eq!(
            scene.points()[0].pick().unpack(&index),
            Some(PickedItem::Point(p0))
        );
        assert_eq!(
            scene.lines()[0].pick().unpack(&index),
            Some(PickedItem::Line(line))
        );
        assert_eq!(
            scene.arcs()[0].pick().unpack(&index),
            Some(PickedItem::Arc(arc))
        );
        assert_eq!(
            scene.circles()[0].pick().unpack(&index),
            Some(PickedItem::Circle(circle))
        );
    }

    #[test]
    fn arc_coincident_start_and_end_is_dropped() {
        let s = Sketch::new(plane());
        let (s, c) = add_point(s, 0.0, 0.0);
        let (s, start) = add_point(s, 5.0, 0.0);
        let (s, end) = add_point(s, 5.0, 0.0);
        let (s, _) = add_arc_ids(s, c, start, end);
        let Ok(scene) = SketchScene::extract(&s) else {
            panic!("extract");
        };
        assert!(scene.arcs().is_empty());
    }

    #[test]
    fn arc_with_zero_radius_is_dropped() {
        let s = Sketch::new(plane());
        let (s, c) = add_point(s, 0.0, 0.0);
        let (s, start) = add_point(s, 0.0, 0.0);
        let (s, end) = add_point(s, 5.0, 0.0);
        let (s, _) = add_arc_ids(s, c, start, end);
        let Ok(scene) = SketchScene::extract(&s) else {
            panic!("extract");
        };
        assert!(scene.arcs().is_empty());
    }

    #[test]
    fn arc_sweep_follows_ccw_convention() {
        let s = Sketch::new(plane());
        let (s, c) = add_point(s, 0.0, 0.0);
        let (s, start) = add_point(s, 0.0, 5.0);
        let (s, end) = add_point(s, 5.0, 0.0);
        let (s, _) = add_arc_ids(s, c, start, end);
        let Ok(scene) = SketchScene::extract(&s) else {
            panic!("extract");
        };
        let arc = scene.arcs()[0];
        let start_rad = arc.start_angle().get::<radian>();
        let sweep_rad = arc.sweep_angle().get::<radian>();
        assert!((start_rad - core::f64::consts::FRAC_PI_2).abs() < 1e-12);
        assert!((sweep_rad - 3.0 * core::f64::consts::FRAC_PI_2).abs() < 1e-12);
    }
}
