use bone_kernel::{
    Arc2, BrepEdge, BrepError, BrepFace, BrepSolid, BrepVertex, Circle2, Curve2Kind,
    ExtrudeDirection, ExtrudeEndCondition, ExtrudeFeature, ExtrudeProfile, ExtrudeSense, Line2,
    MergeResult, ProfileDefect, ProfileEdge, ProfileLoop, TruckGap, evaluate_extrude,
};
use bone_types::{
    Angle, EdgeLabel, FaceLabel, FaceRole, FeatureId, Length, Plane3, Point2, PositiveLength,
    SketchEntityId, SketchId, Tolerance, UnitVec3, VertexLabel, VertexRole, degree, millimeter,
};
use slotmap::{Key, SlotMap};

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

struct Ids {
    features: SlotMap<FeatureId, ()>,
    entities: SlotMap<SketchEntityId, ()>,
}

impl Ids {
    fn new() -> Self {
        Self {
            features: SlotMap::with_key(),
            entities: SlotMap::with_key(),
        }
    }

    fn feature(&mut self) -> FeatureId {
        self.features.insert(())
    }

    fn entity(&mut self) -> SketchEntityId {
        self.entities.insert(())
    }
}

fn length_mm(value: f64) -> Length {
    Length::new::<millimeter>(value)
}

fn point(x: f64, y: f64) -> Point2 {
    Point2::from_mm(x, y)
}

fn line(a: Point2, b: Point2) -> Curve2Kind {
    let Ok(segment) = Line2::new(a, b, TOLERANCE) else {
        panic!("line endpoints are distinct");
    };
    Curve2Kind::Line(segment)
}

fn circle(center: Point2, radius: f64) -> Curve2Kind {
    let Ok(disk) = Circle2::new(center, length_mm(radius), TOLERANCE) else {
        panic!("circle radius is positive");
    };
    Curve2Kind::Circle(disk)
}

fn arc(center: Point2, radius: f64, start_deg: f64, sweep_deg: f64) -> Curve2Kind {
    let Ok(segment) = Arc2::new(
        center,
        length_mm(radius),
        Angle::new::<degree>(start_deg),
        Angle::new::<degree>(sweep_deg),
        TOLERANCE,
    ) else {
        panic!("arc sweep is within range");
    };
    Curve2Kind::Arc(segment)
}

fn xy_plane() -> Plane3 {
    let Ok(plane) = Plane3::new(
        bone_types::Point3::origin(),
        UnitVec3::x_axis(),
        UnitVec3::y_axis(),
        TOLERANCE,
    ) else {
        panic!("x and y axes are orthonormal");
    };
    plane
}

fn rectangle(ids: &mut Ids, x0: f64, y0: f64, width: f64, height: f64) -> ProfileLoop {
    let corners = [
        point(x0, y0),
        point(x0 + width, y0),
        point(x0 + width, y0 + height),
        point(x0, y0 + height),
    ];
    let edges = (0..4)
        .map(|index| {
            let start = corners[index];
            let end = corners[(index + 1) % 4];
            let corner = ids.entity();
            let segment = ids.entity();
            ProfileEdge::new(line(start, end), segment, corner)
        })
        .collect();
    ProfileLoop::Open(edges)
}

fn rectangle_from(
    x0: f64,
    y0: f64,
    width: f64,
    height: f64,
    edge_ids: [SketchEntityId; 4],
    corner_ids: [SketchEntityId; 4],
) -> ProfileLoop {
    let corners = [
        point(x0, y0),
        point(x0 + width, y0),
        point(x0 + width, y0 + height),
        point(x0, y0 + height),
    ];
    let edges = (0..4)
        .map(|index| {
            let start = corners[index];
            let end = corners[(index + 1) % 4];
            ProfileEdge::new(line(start, end), edge_ids[index], corner_ids[index])
        })
        .collect();
    ProfileLoop::Open(edges)
}

fn circle_loop(ids: &mut Ids, center: Point2, radius: f64) -> ProfileLoop {
    let entity = ids.entity();
    ProfileLoop::Closed {
        curve: circle(center, radius),
        curve_entity: entity,
    }
}

fn polygon(ids: &mut Ids, corners: &[Point2]) -> ProfileLoop {
    let count = corners.len();
    let edges = (0..count)
        .map(|index| {
            let corner = ids.entity();
            let segment = ids.entity();
            ProfileEdge::new(
                line(corners[index], corners[(index + 1) % count]),
                segment,
                corner,
            )
        })
        .collect();
    ProfileLoop::Open(edges)
}

fn blind(depth: f64) -> ExtrudeFeature {
    feature(ExtrudeEndCondition::Blind {
        depth: positive(depth),
    })
}

fn midplane(depth: f64) -> ExtrudeFeature {
    feature(ExtrudeEndCondition::MidPlane {
        depth: positive(depth),
    })
}

fn feature(end_condition: ExtrudeEndCondition) -> ExtrudeFeature {
    ExtrudeFeature {
        sketch: SketchId::null(),
        direction: ExtrudeDirection::Normal {
            sense: ExtrudeSense::Forward,
        },
        end_condition,
        draft: None,
        thin_wall: None,
        merge_result: MergeResult::Merge,
    }
}

fn positive(depth: f64) -> PositiveLength {
    let Ok(value) = PositiveLength::new(length_mm(depth)) else {
        panic!("{depth} mm is a positive length");
    };
    value
}

fn evaluate(ids: &mut Ids, profile: &ExtrudeProfile, feature: &ExtrudeFeature) -> BrepSolid {
    let extrude = ids.feature();
    let Ok(solid) = evaluate_extrude(extrude, profile, feature) else {
        panic!("the profile and feature describe a buildable extrude");
    };
    assert!(
        solid.validate(TOLERANCE).is_ok(),
        "extrude is a closed solid"
    );
    solid
}

fn face_labels(solid: &BrepSolid) -> Vec<FaceLabel> {
    solid.iter_faces().map(BrepFace::label).collect()
}

fn edge_labels(solid: &BrepSolid) -> Vec<EdgeLabel> {
    solid.iter_edges().map(BrepEdge::label).collect()
}

fn vertex_labels(solid: &BrepSolid) -> Vec<VertexLabel> {
    solid.iter_vertices().map(BrepVertex::label).collect()
}

fn side_provenance(solid: &BrepSolid) -> Vec<Option<SketchEntityId>> {
    solid
        .iter_faces()
        .map(|face| match face.label().role {
            FaceRole::Side { from, .. } => Some(from),
            _ => None,
        })
        .collect()
}

fn dump(solid: &BrepSolid) -> String {
    let faces = solid
        .iter_faces()
        .map(|face| format!("  {}", face.label()))
        .collect::<Vec<_>>()
        .join("\n");
    let edges = solid
        .iter_edges()
        .map(|edge| format!("  {}", edge.label()))
        .collect::<Vec<_>>()
        .join("\n");
    let vertices = solid
        .iter_vertices()
        .map(|vertex| format!("  {}", vertex.label()))
        .collect::<Vec<_>>()
        .join("\n");
    let counts = format!(
        "counts: faces={} edges={} vertices={} loops={}",
        solid.iter_faces().count(),
        solid.iter_edges().count(),
        solid.iter_vertices().count(),
        solid.iter_loops().count(),
    );
    let bbox = solid.bounding_box().map_or_else(
        || "bbox: none".to_owned(),
        |aabb| {
            let (min_x, min_y, min_z) = aabb.min().coords_mm();
            let (max_x, max_y, max_z) = aabb.max().coords_mm();
            format!(
                "bbox: min=({min_x:.3}, {min_y:.3}, {min_z:.3}) max=({max_x:.3}, {max_y:.3}, {max_z:.3})"
            )
        },
    );
    format!("{counts}\n{bbox}\nfaces:\n{faces}\nedges:\n{edges}\nvertices:\n{vertices}")
}

#[test]
fn unit_cube_extrude() {
    let mut ids = Ids::new();
    let profile = ExtrudeProfile::new(xy_plane(), vec![rectangle(&mut ids, 0.0, 0.0, 1.0, 1.0)]);
    let solid = evaluate(&mut ids, &profile, &blind(1.0));
    insta::assert_snapshot!(dump(&solid));
}

#[test]
fn cylinder_extrude() {
    let mut ids = Ids::new();
    let profile = ExtrudeProfile::new(
        xy_plane(),
        vec![circle_loop(&mut ids, point(0.0, 0.0), 5.0)],
    );
    let solid = evaluate(&mut ids, &profile, &blind(10.0));
    insta::assert_snapshot!(dump(&solid));
}

#[test]
fn donut_extrude() {
    let mut ids = Ids::new();
    let outer = circle_loop(&mut ids, point(0.0, 0.0), 10.0);
    let inner = circle_loop(&mut ids, point(0.0, 0.0), 4.0);
    let profile = ExtrudeProfile::new(xy_plane(), vec![outer, inner]);
    let solid = evaluate(&mut ids, &profile, &blind(6.0));
    insta::assert_snapshot!(dump(&solid));
}

#[test]
fn rectangle_with_two_holes_extrude() {
    let mut ids = Ids::new();
    let outer = rectangle(&mut ids, 0.0, 0.0, 10.0, 6.0);
    let hole_a = rectangle(&mut ids, 1.0, 1.0, 2.0, 2.0);
    let hole_b = rectangle(&mut ids, 6.0, 2.0, 2.0, 2.0);
    let profile = ExtrudeProfile::new(xy_plane(), vec![outer, hole_a, hole_b]);
    let solid = evaluate(&mut ids, &profile, &blind(3.0));
    insta::assert_snapshot!(dump(&solid));
}

#[test]
fn half_disk_extrude() {
    let mut ids = Ids::new();
    let arc_corner = ids.entity();
    let arc_entity = ids.entity();
    let line_corner = ids.entity();
    let line_entity = ids.entity();
    let curved = ProfileEdge::new(
        arc(point(0.0, 0.0), 5.0, 0.0, 180.0),
        arc_entity,
        arc_corner,
    );
    let chord = ProfileEdge::new(
        line(point(-5.0, 0.0), point(5.0, 0.0)),
        line_entity,
        line_corner,
    );
    let profile = ExtrudeProfile::new(xy_plane(), vec![ProfileLoop::Open(vec![curved, chord])]);
    let solid = evaluate(&mut ids, &profile, &blind(2.0));
    insta::assert_snapshot!(dump(&solid));
}

#[test]
fn wide_arc_wedge_extrude() {
    let mut ids = Ids::new();
    let arc_corner = ids.entity();
    let arc_entity = ids.entity();
    let down_corner = ids.entity();
    let down_entity = ids.entity();
    let out_corner = ids.entity();
    let out_entity = ids.entity();
    let curved = ProfileEdge::new(
        arc(point(0.0, 0.0), 5.0, 0.0, 270.0),
        arc_entity,
        arc_corner,
    );
    let radius_down = ProfileEdge::new(
        line(point(0.0, -5.0), point(0.0, 0.0)),
        down_entity,
        down_corner,
    );
    let radius_out = ProfileEdge::new(
        line(point(0.0, 0.0), point(5.0, 0.0)),
        out_entity,
        out_corner,
    );
    let profile = ExtrudeProfile::new(
        xy_plane(),
        vec![ProfileLoop::Open(vec![curved, radius_down, radius_out])],
    );
    let solid = evaluate(&mut ids, &profile, &blind(2.0));
    insta::assert_snapshot!(dump(&solid));
}

#[test]
fn midplane_cube_is_centered() {
    let mut ids = Ids::new();
    let profile = ExtrudeProfile::new(xy_plane(), vec![rectangle(&mut ids, 0.0, 0.0, 2.0, 2.0)]);
    let solid = evaluate(&mut ids, &profile, &midplane(4.0));
    let Some(bbox) = solid.bounding_box() else {
        panic!("solid has geometry");
    };
    let (_, _, min_z) = bbox.min().coords_mm();
    let (_, _, max_z) = bbox.max().coords_mm();
    assert!(
        (min_z + 2.0).abs() < 1.0e-6,
        "midplane base sits at -depth/2"
    );
    assert!(
        (max_z - 2.0).abs() < 1.0e-6,
        "midplane top sits at +depth/2"
    );
}

#[test]
fn evaluation_is_deterministic() {
    let mut ids = Ids::new();
    let plane = xy_plane();
    let cube = ExtrudeProfile::new(plane, vec![rectangle(&mut ids, 0.0, 0.0, 1.0, 1.0)]);
    let cylinder = ExtrudeProfile::new(plane, vec![circle_loop(&mut ids, point(0.0, 0.0), 5.0)]);
    let extrude = ids.feature();
    [cube, cylinder].iter().for_each(|profile| {
        let Ok(first) = evaluate_extrude(extrude, profile, &blind(3.0)) else {
            panic!("profile extrudes");
        };
        let Ok(second) = evaluate_extrude(extrude, profile, &blind(3.0)) else {
            panic!("profile extrudes");
        };
        assert_eq!(dump(&first), dump(&second));
    });
}

proptest::proptest! {
    #[test]
    fn varied_rectangles_are_deterministic(
        width in 0.5f64..50.0,
        height in 0.5f64..50.0,
        depth in 0.5f64..50.0,
    ) {
        let mut ids = Ids::new();
        let profile = ExtrudeProfile::new(xy_plane(), vec![rectangle(&mut ids, 0.0, 0.0, width, height)]);
        let extrude = ids.feature();
        let feature = blind(depth);
        let Ok(first) = evaluate_extrude(extrude, &profile, &feature) else {
            panic!("rectangle extrudes");
        };
        let Ok(second) = evaluate_extrude(extrude, &profile, &feature) else {
            panic!("rectangle extrudes");
        };
        proptest::prop_assert_eq!(dump(&first), dump(&second));
    }
}

proptest::proptest! {
    #[test]
    fn rectangle_label_set_is_stable_across_runs(
        width in 0.5f64..50.0,
        height in 0.5f64..50.0,
        depth in 0.5f64..50.0,
    ) {
        let mut ids = Ids::new();
        let profile = ExtrudeProfile::new(xy_plane(), vec![rectangle(&mut ids, 0.0, 0.0, width, height)]);
        let extrude = ids.feature();
        let feature = blind(depth);
        let Ok(first) = evaluate_extrude(extrude, &profile, &feature) else {
            panic!("rectangle extrudes");
        };
        let Ok(second) = evaluate_extrude(extrude, &profile, &feature) else {
            panic!("rectangle extrudes");
        };
        proptest::prop_assert_eq!(face_labels(&first), face_labels(&second));
        proptest::prop_assert_eq!(edge_labels(&first), edge_labels(&second));
        proptest::prop_assert_eq!(vertex_labels(&first), vertex_labels(&second));
    }
}

#[test]
fn dimension_edit_preserves_label_provenance() {
    let mut ids = Ids::new();
    let edge_ids = [ids.entity(), ids.entity(), ids.entity(), ids.entity()];
    let corner_ids = [ids.entity(), ids.entity(), ids.entity(), ids.entity()];
    let extrude = ids.feature();

    let narrow = ExtrudeProfile::new(
        xy_plane(),
        vec![rectangle_from(0.0, 0.0, 10.0, 6.0, edge_ids, corner_ids)],
    );
    let wide = ExtrudeProfile::new(
        xy_plane(),
        vec![rectangle_from(0.0, 0.0, 25.0, 6.0, edge_ids, corner_ids)],
    );

    let Ok(before) = evaluate_extrude(extrude, &narrow, &blind(3.0)) else {
        panic!("narrow rectangle extrudes");
    };
    let Ok(after) = evaluate_extrude(extrude, &wide, &blind(3.0)) else {
        panic!("wide rectangle extrudes");
    };

    assert_eq!(face_labels(&before), face_labels(&after));
    assert_eq!(edge_labels(&before), edge_labels(&after));
    assert_eq!(vertex_labels(&before), vertex_labels(&after));

    assert_eq!(side_provenance(&before), side_provenance(&after));
    let sides: Vec<SketchEntityId> = side_provenance(&after).into_iter().flatten().collect();
    assert_eq!(sides.len(), 4);
    assert!(sides.iter().all(|id| edge_ids.contains(id)));
}

fn blob_round_trip(solid: &BrepSolid) -> BrepSolid {
    let Ok(blob) = solid.to_blob() else {
        panic!("solid serializes to a blob");
    };
    match BrepSolid::from_blob(&blob, solid.reattach_data()) {
        Ok(restored) => restored,
        Err(error) => panic!("blob reattach failed: {error}"),
    }
}

fn assert_blob_round_trip(solid: &BrepSolid) {
    let restored = blob_round_trip(solid);
    assert_eq!(face_labels(solid), face_labels(&restored));
    assert_eq!(edge_labels(solid), edge_labels(&restored));
    assert_eq!(vertex_labels(solid), vertex_labels(&restored));
    assert_eq!(solid.content_key(), restored.content_key());
    assert_eq!(
        solid.iter_faces().count(),
        restored.iter_faces().count(),
        "face count survives the blob round trip"
    );
    assert!(
        restored.validate(TOLERANCE).is_ok(),
        "restored solid is a closed manifold"
    );
}

#[test]
fn blob_round_trip_preserves_cube_labels() {
    let mut ids = Ids::new();
    let profile = ExtrudeProfile::new(xy_plane(), vec![rectangle(&mut ids, 0.0, 0.0, 2.0, 3.0)]);
    let solid = evaluate(&mut ids, &profile, &blind(5.0));
    assert_blob_round_trip(&solid);
}

#[test]
fn blob_round_trip_preserves_cylinder_labels() {
    let mut ids = Ids::new();
    let profile = ExtrudeProfile::new(
        xy_plane(),
        vec![circle_loop(&mut ids, point(0.0, 0.0), 5.0)],
    );
    let solid = evaluate(&mut ids, &profile, &blind(10.0));
    assert_blob_round_trip(&solid);
    let restored = blob_round_trip(&solid);
    let curved = restored
        .edges_for_render(bone_types::ChordHeightTolerance::from_mm(0.05))
        .iter()
        .any(|edge| matches!(edge.curve(), bone_kernel::EdgeCurve3::Circle(_)));
    assert!(curved, "cylinder cap circles survive as analytic curves");
}

#[test]
fn blob_round_trip_preserves_donut_labels() {
    let mut ids = Ids::new();
    let outer = circle_loop(&mut ids, point(0.0, 0.0), 10.0);
    let inner = circle_loop(&mut ids, point(0.0, 0.0), 4.0);
    let profile = ExtrudeProfile::new(xy_plane(), vec![outer, inner]);
    let solid = evaluate(&mut ids, &profile, &blind(6.0));
    assert_blob_round_trip(&solid);
}

#[test]
fn content_key_is_stable_and_geometry_sensitive() {
    let mut ids = Ids::new();
    let narrow = ExtrudeProfile::new(xy_plane(), vec![rectangle(&mut ids, 0.0, 0.0, 2.0, 2.0)]);
    let extrude = ids.feature();
    let Ok(first) = evaluate_extrude(extrude, &narrow, &blind(4.0)) else {
        panic!("rectangle extrudes");
    };
    let Ok(second) = evaluate_extrude(extrude, &narrow, &blind(4.0)) else {
        panic!("rectangle extrudes");
    };
    assert_eq!(first.content_key(), second.content_key());

    let wide = ExtrudeProfile::new(xy_plane(), vec![rectangle(&mut ids, 0.0, 0.0, 9.0, 2.0)]);
    let Ok(other) = evaluate_extrude(extrude, &wide, &blind(4.0)) else {
        panic!("rectangle extrudes");
    };
    assert_ne!(first.content_key(), other.content_key());
}

fn wrap_step(body: &str) -> String {
    format!(
        "ISO-10303-21;\nHEADER;\nFILE_DESCRIPTION((''),'2;1');\nFILE_NAME('','',(''),(''),'','','');\nFILE_SCHEMA(('AUTOMOTIVE_DESIGN'));\nENDSEC;\nDATA;\n{body}ENDSEC;\nEND-ISO-10303-21;\n"
    )
}

fn assert_cap_vertices(solid: &BrepSolid, depth: f64) {
    solid.iter_vertices().for_each(|vertex| {
        let (_, _, z) = vertex.position().coords_mm();
        match vertex.label().role {
            VertexRole::StartCapVertex { .. } => {
                assert!(z.abs() < 1.0e-6, "start cap vertex at z=0, got {z}");
            }
            VertexRole::EndCapVertex { .. } => {
                assert!(
                    (z - depth).abs() < 1.0e-6,
                    "end cap vertex at z={depth}, got {z}"
                );
            }
            VertexRole::Imported { .. } => {}
        }
    });
}

#[test]
fn step_round_trip_reattaches_cube_labels() {
    let mut ids = Ids::new();
    let profile = ExtrudeProfile::new(xy_plane(), vec![rectangle(&mut ids, 0.0, 0.0, 2.0, 3.0)]);
    let solid = evaluate(&mut ids, &profile, &blind(5.0));
    let Ok(body) = solid.to_step_body() else {
        panic!("solid lowers to a step body");
    };
    let step = wrap_step(&body);
    let feature = ids.feature();
    let Ok(restored) = BrepSolid::from_step(
        &step,
        feature,
        Some((solid.content_key(), solid.reattach_data())),
    ) else {
        panic!("step body reattaches under a matching sidecar");
    };
    assert_eq!(face_labels(&solid), face_labels(&restored));
    assert_eq!(solid.content_key(), restored.content_key());
    assert_cap_vertices(&restored, 5.0);
    assert!(restored.validate(TOLERANCE).is_ok());
}

#[test]
fn step_without_sidecar_imports_as_dumb_body() {
    let mut ids = Ids::new();
    let profile = ExtrudeProfile::new(xy_plane(), vec![rectangle(&mut ids, 0.0, 0.0, 2.0, 3.0)]);
    let solid = evaluate(&mut ids, &profile, &blind(5.0));
    let Ok(body) = solid.to_step_body() else {
        panic!("solid lowers to a step body");
    };
    let step = wrap_step(&body);
    let feature = ids.feature();
    let Ok(imported) = BrepSolid::from_step(&step, feature, None) else {
        panic!("step body imports as a dumb body");
    };
    assert!(
        imported
            .iter_faces()
            .all(|face| matches!(face.label().role, FaceRole::Imported { .. })),
        "a sidecar-less import carries only Imported roles"
    );
    assert_eq!(imported.iter_faces().count(), solid.iter_faces().count());
}

#[test]
fn step_reattach_falls_back_when_count_mismatches() {
    let mut ids = Ids::new();
    let profile = ExtrudeProfile::new(xy_plane(), vec![rectangle(&mut ids, 0.0, 0.0, 2.0, 3.0)]);
    let solid = evaluate(&mut ids, &profile, &blind(5.0));
    let cylinder_loop = circle_loop(&mut ids, point(0.0, 0.0), 5.0);
    let cylinder_profile = ExtrudeProfile::new(xy_plane(), vec![cylinder_loop]);
    let cylinder = evaluate(&mut ids, &cylinder_profile, &blind(8.0));
    assert_ne!(
        solid.reattach_data().faces().len(),
        cylinder.reattach_data().faces().len(),
        "the two solids disagree on entity count"
    );
    let Ok(body) = solid.to_step_body() else {
        panic!("solid lowers to a step body");
    };
    let step = wrap_step(&body);
    let feature = ids.feature();
    let Ok(imported) = BrepSolid::from_step(
        &step,
        feature,
        Some((solid.content_key(), cylinder.reattach_data())),
    ) else {
        panic!("a matching key with a wrong-count reattach degrades, it does not error");
    };
    assert!(
        imported
            .iter_faces()
            .all(|face| matches!(face.label().role, FaceRole::Imported { .. })),
        "a count mismatch drops to a labelless import"
    );
}

#[test]
fn step_reattach_falls_back_when_key_mismatches() {
    let mut ids = Ids::new();
    let profile = ExtrudeProfile::new(xy_plane(), vec![rectangle(&mut ids, 0.0, 0.0, 2.0, 3.0)]);
    let solid = evaluate(&mut ids, &profile, &blind(5.0));
    let Ok(body) = solid.to_step_body() else {
        panic!("solid lowers to a step body");
    };
    let step = wrap_step(&body);
    let feature = ids.feature();
    let wrong_key = bone_types::SolidKey::from_bytes([0u8; 16]);
    let Ok(imported) =
        BrepSolid::from_step(&step, feature, Some((wrong_key, solid.reattach_data())))
    else {
        panic!("a mismatched key still imports");
    };
    assert!(
        imported
            .iter_faces()
            .all(|face| matches!(face.label().role, FaceRole::Imported { .. })),
        "a hash mismatch drops to a labelless import"
    );
}

#[test]
fn open_loop_is_rejected() {
    let mut ids = Ids::new();
    let segment = ids.entity();
    let corner = ids.entity();
    let open = ProfileLoop::Open(vec![ProfileEdge::new(
        line(point(0.0, 0.0), point(1.0, 0.0)),
        segment,
        corner,
    )]);
    let profile = ExtrudeProfile::new(xy_plane(), vec![open]);
    let extrude = ids.feature();
    assert!(matches!(
        evaluate_extrude(extrude, &profile, &blind(1.0)),
        Err(BrepError::InvalidProfile {
            reason: ProfileDefect::OpenLoop
        })
    ));
}

#[test]
fn disjoint_loops_are_rejected() {
    let mut ids = Ids::new();
    let near = rectangle(&mut ids, 0.0, 0.0, 4.0, 4.0);
    let far = rectangle(&mut ids, 10.0, 10.0, 2.0, 2.0);
    let profile = ExtrudeProfile::new(xy_plane(), vec![near, far]);
    let extrude = ids.feature();
    assert!(matches!(
        evaluate_extrude(extrude, &profile, &blind(1.0)),
        Err(BrepError::InvalidProfile {
            reason: ProfileDefect::UncontainedLoop
        })
    ));
}

#[test]
fn near_zero_depth_is_rejected() {
    let mut ids = Ids::new();
    let profile = ExtrudeProfile::new(xy_plane(), vec![rectangle(&mut ids, 0.0, 0.0, 1.0, 1.0)]);
    let extrude = ids.feature();
    assert!(matches!(
        evaluate_extrude(extrude, &profile, &blind(1.0e-9)),
        Err(BrepError::EmptyExtrudeDepth)
    ));
}

#[test]
fn deferred_paths_report_truck_gap() {
    let mut ids = Ids::new();
    let profile = ExtrudeProfile::new(xy_plane(), vec![rectangle(&mut ids, 0.0, 0.0, 1.0, 1.0)]);
    let extrude = ids.feature();
    let through_all = feature(ExtrudeEndCondition::ThroughAll);
    assert!(matches!(
        evaluate_extrude(extrude, &profile, &through_all),
        Err(BrepError::TruckUnsupported {
            detail: TruckGap::ThroughAll
        })
    ));
}

#[test]
fn self_intersecting_loop_is_rejected() {
    let mut ids = Ids::new();
    let bowtie = polygon(
        &mut ids,
        &[
            point(0.0, 0.0),
            point(4.0, 0.0),
            point(1.0, 3.0),
            point(3.0, 3.0),
        ],
    );
    let profile = ExtrudeProfile::new(xy_plane(), vec![bowtie]);
    let extrude = ids.feature();
    assert!(matches!(
        evaluate_extrude(extrude, &profile, &blind(2.0)),
        Err(BrepError::InvalidProfile {
            reason: ProfileDefect::SelfIntersectingLoop
        })
    ));
}

#[test]
fn overlapping_holes_are_rejected() {
    let mut ids = Ids::new();
    let outer = rectangle(&mut ids, 0.0, 0.0, 20.0, 20.0);
    let hole_a = rectangle(&mut ids, 4.0, 4.0, 8.0, 8.0);
    let hole_b = rectangle(&mut ids, 8.0, 8.0, 8.0, 8.0);
    let profile = ExtrudeProfile::new(xy_plane(), vec![outer, hole_a, hole_b]);
    let extrude = ids.feature();
    assert!(matches!(
        evaluate_extrude(extrude, &profile, &blind(2.0)),
        Err(BrepError::InvalidProfile {
            reason: ProfileDefect::OverlappingLoops
        })
    ));
}

#[test]
fn nested_holes_are_rejected() {
    let mut ids = Ids::new();
    let outer = circle_loop(&mut ids, point(0.0, 0.0), 10.0);
    let hole = circle_loop(&mut ids, point(0.0, 0.0), 6.0);
    let nested = circle_loop(&mut ids, point(0.0, 0.0), 3.0);
    let profile = ExtrudeProfile::new(xy_plane(), vec![outer, hole, nested]);
    let extrude = ids.feature();
    assert!(matches!(
        evaluate_extrude(extrude, &profile, &blind(2.0)),
        Err(BrepError::InvalidProfile {
            reason: ProfileDefect::OverlappingLoops
        })
    ));
}

#[test]
fn hole_near_outer_wall_is_accepted() {
    let mut ids = Ids::new();
    let outer = circle_loop(&mut ids, point(0.0, 0.0), 10.0);
    let hole = polygon(
        &mut ids,
        &[
            point(9.932, 0.978),
            point(9.6, 0.978),
            point(9.6, 0.6),
            point(9.932, 0.6),
        ],
    );
    let profile = ExtrudeProfile::new(xy_plane(), vec![outer, hole]);
    let solid = evaluate(&mut ids, &profile, &blind(2.0));
    assert_eq!(solid.iter_faces().count(), 7);
}

#[test]
fn full_turn_arc_as_closed_loop_builds() {
    let mut ids = Ids::new();
    let entity = ids.entity();
    let profile = ExtrudeProfile::new(
        xy_plane(),
        vec![ProfileLoop::Closed {
            curve: arc(point(0.0, 0.0), 5.0, 0.0, 360.0),
            curve_entity: entity,
        }],
    );
    let solid = evaluate(&mut ids, &profile, &blind(10.0));
    assert_eq!(solid.iter_faces().count(), 3);
    assert_eq!(solid.iter_vertices().count(), 2);
}

#[test]
fn full_turn_arc_in_open_chain_is_rejected() {
    let mut ids = Ids::new();
    let segment = ids.entity();
    let corner = ids.entity();
    let loop_ = ProfileLoop::Open(vec![ProfileEdge::new(
        arc(point(0.0, 0.0), 5.0, 0.0, 360.0),
        segment,
        corner,
    )]);
    let profile = ExtrudeProfile::new(xy_plane(), vec![loop_]);
    let extrude = ids.feature();
    assert!(matches!(
        evaluate_extrude(extrude, &profile, &blind(2.0)),
        Err(BrepError::InvalidProfile {
            reason: ProfileDefect::OpenLoop
        })
    ));
}
