use bone_kernel::{
    Arc2, Arc3, BrepError, BrepSolid, Circle3, Curve2Kind, Curve3Kind, ExtrudeProfile, Line2,
    Line3, ProfileDefect, ProfileEdge, ProfileLoop, SweepDefect, SweepDirection, SweepFeature,
    SweepOrientation, SweepPath, SweepProfileKind, SweepRails, SweepTangency, SweepTwist, ThinWall,
    ThinWallDirection, TruckGap, TwistUnit, evaluate_sweep,
};
use bone_types::{
    Angle, AngleTolerance, BodyOrdinal, BodyRef, ChordHeightTolerance, FaceRole, FeatureId, Length,
    Plane3, Point2, Point3, PositiveLength, SketchEntityId, SketchId, Tolerance, UnitVec3,
    millimeter, radian,
};
use core::f64::consts::{FRAC_PI_2, PI, TAU};
use slotmap::{Key, SlotMap};

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

fn sweep_gap(result: &Result<BrepSolid, BrepError>) -> Option<TruckGap> {
    match result {
        Err(BrepError::TruckUnsupported { detail }) => Some(*detail),
        _ => None,
    }
}

fn sweep_defect(result: &Result<BrepSolid, BrepError>) -> Option<SweepDefect> {
    match result {
        Err(BrepError::InvalidSweep { reason }) => Some(*reason),
        _ => None,
    }
}

fn profile_defect(result: &Result<BrepSolid, BrepError>) -> Option<ProfileDefect> {
    match result {
        Err(BrepError::InvalidProfile { reason }) => Some(*reason),
        _ => None,
    }
}

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 point(x: f64, y: f64) -> Point2 {
    Point2::from_mm(x, y)
}

fn pt(x: f64, y: f64, z: f64) -> Point3 {
    Point3::from_mm(x, y, z)
}

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

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

fn xz_plane() -> Plane3 {
    plane(Point3::origin(), UnitVec3::x_axis(), UnitVec3::z_axis())
}

fn xy_plane() -> Plane3 {
    plane(Point3::origin(), UnitVec3::x_axis(), UnitVec3::y_axis())
}

fn ring(ids: &mut Ids, corners: &[Point2], closed: bool) -> Vec<ProfileEdge> {
    let n = corners.len();
    let count = if closed { n } else { n - 1 };
    (0..count)
        .map(|i| {
            ProfileEdge::new(
                line(corners[i], corners[(i + 1) % n]),
                ids.entity(),
                ids.entity(),
            )
        })
        .collect()
}

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),
    ];
    ProfileLoop::Open(ring(ids, &corners, true))
}

fn rectangle_cw(ids: &mut Ids, x0: f64, y0: f64, width: f64, height: f64) -> ProfileLoop {
    let corners = [
        point(x0, y0),
        point(x0, y0 + height),
        point(x0 + width, y0 + height),
        point(x0 + width, y0),
    ];
    ProfileLoop::Open(ring(ids, &corners, true))
}

fn open_chain(ids: &mut Ids) -> ProfileLoop {
    let corners = [
        point(0.0, 0.0),
        point(2.0, 0.0),
        point(2.0, 1.0),
        point(0.0, 1.0),
    ];
    ProfileLoop::Chain(ring(ids, &corners, false))
}

fn bowtie(ids: &mut Ids) -> ProfileLoop {
    let corners = [
        point(0.0, 0.0),
        point(2.0, 2.0),
        point(2.0, 0.0),
        point(0.0, 2.0),
    ];
    ProfileLoop::Open(ring(ids, &corners, true))
}

fn disc(ids: &mut Ids, radius_mm: f64) -> ProfileLoop {
    let Ok(circle) = bone_kernel::Circle2::new(
        Point2::origin(),
        Length::new::<millimeter>(radius_mm),
        TOLERANCE,
    ) else {
        panic!("circle radius is positive");
    };
    ProfileLoop::Closed {
        curve: bone_kernel::Curve2Kind::Circle(circle),
        curve_entity: ids.entity(),
    }
}

fn disc_as_arcs(ids: &mut Ids, radius_mm: f64) -> ProfileLoop {
    let edges = (0..4)
        .map(|quadrant| {
            let start = FRAC_PI_2 * f64::from(quadrant);
            let Ok(arc) = Arc2::new(
                Point2::origin(),
                Length::new::<millimeter>(radius_mm),
                Angle::new::<radian>(start),
                Angle::new::<radian>(FRAC_PI_2),
                TOLERANCE,
            ) else {
                panic!("quarter arc is valid");
            };
            ProfileEdge::new(Curve2Kind::Arc(arc), ids.entity(), ids.entity())
        })
        .collect();
    ProfileLoop::Open(edges)
}

fn ellipse_loop(ids: &mut Ids, semi_major_mm: f64, semi_minor_mm: f64) -> ProfileLoop {
    let Ok(ellipse) = bone_kernel::Ellipse2::new(
        Point2::origin(),
        Length::new::<millimeter>(semi_major_mm),
        Length::new::<millimeter>(semi_minor_mm),
        Angle::new::<radian>(0.0),
        TOLERANCE,
    ) else {
        panic!("ellipse axes are positive");
    };
    ProfileLoop::Closed {
        curve: bone_kernel::Curve2Kind::Ellipse(ellipse),
        curve_entity: ids.entity(),
    }
}

fn xy_rect(ids: &mut Ids, x: f64, y: f64, w: f64, h: f64) -> ExtrudeProfile {
    ExtrudeProfile::new(xy_plane(), vec![rectangle(ids, x, y, w, h)])
}

fn xz_rect(ids: &mut Ids, x: f64, y: f64, w: f64, h: f64) -> ExtrudeProfile {
    ExtrudeProfile::new(xz_plane(), vec![rectangle(ids, x, y, w, h)])
}

fn xy_loop(loop_: ProfileLoop) -> ExtrudeProfile {
    ExtrudeProfile::new(xy_plane(), vec![loop_])
}

fn circle_path(normal_plane: Plane3, radius_mm: f64) -> Curve3Kind {
    let Ok(circle) = Circle3::new(
        normal_plane,
        Length::new::<millimeter>(radius_mm),
        TOLERANCE,
    ) else {
        panic!("circle radius is positive");
    };
    circle.as_kind()
}

fn line_path(start: Point3, end: Point3) -> Curve3Kind {
    let Ok(segment) = Line3::new(start, end, TOLERANCE) else {
        panic!("line endpoints are distinct");
    };
    segment.as_kind()
}

fn zline(len: f64) -> Vec<Curve3Kind> {
    vec![line_path(Point3::origin(), pt(0.0, 0.0, len))]
}

fn xline(len: f64) -> Vec<Curve3Kind> {
    vec![line_path(Point3::origin(), pt(len, 0.0, 0.0))]
}

fn arc3(base: Plane3, radius_mm: f64, start_rad: f64, sweep_rad: f64) -> Arc3 {
    let Ok(arc) = Arc3::new(
        base,
        Length::new::<millimeter>(radius_mm),
        Angle::new::<radian>(start_rad),
        Angle::new::<radian>(sweep_rad),
        TOLERANCE,
    ) else {
        panic!("arc parameters are valid");
    };
    arc
}

fn arc_path_xz(center: Point3, radius_mm: f64, start_rad: f64, sweep_rad: f64) -> Curve3Kind {
    let arc_plane = plane(center, UnitVec3::x_axis(), UnitVec3::z_axis());
    arc3(arc_plane, radius_mm, start_rad, sweep_rad).as_kind()
}

fn arc10() -> [Curve3Kind; 1] {
    [arc_path_xz(pt(10.0, 0.0, 0.0), 10.0, PI, -FRAC_PI_2)]
}

fn line_arc_line_bend(radius: f64, rise: f64, run: f64) -> [Curve3Kind; 3] {
    [
        line_path(Point3::origin(), pt(0.0, 0.0, rise)),
        arc_path_xz(pt(radius, 0.0, rise), radius, PI, -FRAC_PI_2),
        line_path(
            pt(radius, 0.0, rise + radius),
            pt(radius + run, 0.0, rise + radius),
        ),
    ]
}

fn right_angle_corner_path() -> [Curve3Kind; 2] {
    [
        line_path(Point3::origin(), pt(0.0, 0.0, 4.0)),
        line_path(pt(0.0, 0.0, 4.0), pt(4.0, 0.0, 4.0)),
    ]
}

fn line_arc_corner_path() -> [Curve3Kind; 2] {
    let Ok(line) = Line3::new(Point3::origin(), pt(2.0, 0.0, 0.0), TOLERANCE) else {
        panic!("distinct endpoints");
    };
    let arc_plane = plane(pt(1.0, 0.0, 0.0), UnitVec3::x_axis(), UnitVec3::y_axis());
    [
        Curve3Kind::Line(line),
        arc3(arc_plane, 1.0, 0.0, FRAC_PI_2).as_kind(),
    ]
}

fn two_arc_circle(radius_mm: f64) -> [Curve3Kind; 2] {
    [
        arc3(xy_plane(), radius_mm, 0.0, PI).as_kind(),
        arc3(xy_plane(), radius_mm, PI, PI).as_kind(),
    ]
}

fn off_perpendicular_path() -> [Curve3Kind; 1] {
    [line_path(Point3::origin(), pt(4.0, 0.0, 0.0))]
}

fn out_of_plane_bend() -> [Curve3Kind; 2] {
    let first_plane = plane(pt(0.0, 10.0, 0.0), UnitVec3::x_axis(), UnitVec3::y_axis());
    let second_plane = plane(pt(10.0, 10.0, 5.0), UnitVec3::y_axis(), UnitVec3::z_axis());
    [
        arc3(first_plane, 10.0, -FRAC_PI_2, FRAC_PI_2).as_kind(),
        arc3(second_plane, 5.0, -FRAC_PI_2, FRAC_PI_2).as_kind(),
    ]
}

fn azimuth_arc_guide() -> Vec<Curve3Kind> {
    let Ok(tilt) = bone_types::Vec3::from_mm(0.0, 1.0, 1.0).try_normalize(TOLERANCE) else {
        panic!("nonzero tilt axis");
    };
    let guide_plane = plane(pt(0.0, 0.0, 4.0), UnitVec3::x_axis(), tilt);
    let radius = 50.0_f64.sqrt();
    let reach = (0.8_f64).asin();
    vec![arc3(guide_plane, radius, -reach, 2.0 * reach).as_kind()]
}

fn boss(profile_kind: SweepProfileKind, orientation: SweepOrientation) -> SweepFeature {
    SweepFeature {
        profile: SketchId::null(),
        path: SweepPath::Sketch(SketchId::null()),
        guides: Vec::new(),
        profile_kind,
        orientation,
        direction: SweepDirection::DirectionOne,
        start_tangency: SweepTangency::None,
        end_tangency: SweepTangency::None,
        alignment: bone_kernel::SweepAlignment::MinimumTwist,
        twist: None,
        thin_wall: None,
        flip_side: false,
        operation: bone_kernel::ExtrudeOperation::default(),
        scope: bone_kernel::FeatureScope::default(),
    }
}

fn follow() -> SweepFeature {
    boss(SweepProfileKind::Sketch, SweepOrientation::FollowPath)
}

fn keep_normal() -> SweepFeature {
    boss(
        SweepProfileKind::Sketch,
        SweepOrientation::KeepNormalConstant,
    )
}

fn directed(direction: SweepDirection) -> SweepFeature {
    SweepFeature {
        direction,
        ..follow()
    }
}

fn all_faces() -> SweepFeature {
    SweepFeature {
        alignment: bone_kernel::SweepAlignment::AllFaces,
        ..follow()
    }
}

fn positive_mm(value_mm: f64) -> PositiveLength {
    let Ok(value) = PositiveLength::new(Length::new::<millimeter>(value_mm)) else {
        panic!("{value_mm} mm is a positive length");
    };
    value
}

fn circular_boss(diameter_mm: f64) -> SweepFeature {
    boss(
        SweepProfileKind::Circular {
            diameter: positive_mm(diameter_mm),
        },
        SweepOrientation::FollowPath,
    )
}

fn circular_tube(diameter_mm: f64, wall_mm: f64) -> SweepFeature {
    SweepFeature {
        thin_wall: Some(thin_spec(ThinWallDirection::Inward, wall_mm, None, false)),
        ..circular_boss(diameter_mm)
    }
}

fn thin_spec(
    direction: ThinWallDirection,
    wall_mm: f64,
    second_mm: Option<f64>,
    cap_ends: bool,
) -> ThinWall {
    ThinWall {
        thickness: positive_mm(wall_mm),
        direction,
        thickness2: second_mm.map(positive_mm),
        cap_ends,
        auto_fillet: None,
    }
}

fn thin_boss(thin: ThinWall) -> SweepFeature {
    SweepFeature {
        thin_wall: Some(thin),
        ..follow()
    }
}

fn twist(degrees: f64) -> SweepTwist {
    SweepTwist::new(
        Angle::new::<radian>(degrees.to_radians()),
        TwistUnit::Degrees,
    )
}

fn twist_rad(radians: f64) -> SweepTwist {
    SweepTwist::new(Angle::new::<radian>(radians), TwistUnit::Radians)
}

fn direction_vector() -> bone_kernel::SweepAlignment {
    bone_kernel::SweepAlignment::DirectionVector {
        axis: bone_kernel::RevolveAxis::SketchLine(SketchEntityId::null()),
    }
}

fn dir_rails(direction: UnitVec3) -> SweepRails<'static> {
    SweepRails {
        guides: &[],
        direction: Some(direction),
        tool: None,
        bodies: &[],
    }
}

fn body_rails<'a>(bodies: &'a [&'a BrepSolid]) -> SweepRails<'a> {
    SweepRails {
        guides: &[],
        direction: None,
        tool: None,
        bodies,
    }
}

fn mid_line_setup(ids: &mut Ids, plane_z: f64) -> (ExtrudeProfile, [Curve3Kind; 1]) {
    let section = plane(
        pt(0.0, 0.0, plane_z),
        UnitVec3::x_axis(),
        UnitVec3::y_axis(),
    );
    let profile = ExtrudeProfile::new(section, vec![rectangle(ids, 0.0, 0.0, 2.0, 1.0)]);
    (profile, [line_path(Point3::origin(), pt(0.0, 0.0, 10.0))])
}

fn cylinder_tool(ids: &mut Ids, start: Point3, end: Point3, diameter_mm: f64) -> BrepSolid {
    let profile = xy_rect(ids, 0.0, 0.0, 1.0, 1.0);
    build(
        ids,
        &profile,
        &[line_path(start, end)],
        &circular_boss(diameter_mm),
    )
}

fn tool_rails(tool: &BrepSolid) -> SweepRails<'_> {
    SweepRails {
        guides: &[],
        direction: None,
        tool: Some(tool),
        bodies: &[],
    }
}

fn solid_tool_cut() -> SweepFeature {
    SweepFeature {
        operation: bone_kernel::ExtrudeOperation::Cut,
        ..boss(
            SweepProfileKind::Solid {
                tool: BodyRef::new(FeatureId::default(), BodyOrdinal::origin()),
            },
            SweepOrientation::FollowPath,
        )
    }
}

fn eval(
    ids: &mut Ids,
    profile: &ExtrudeProfile,
    path: &[Curve3Kind],
    feature: &SweepFeature,
) -> Result<BrepSolid, BrepError> {
    evaluate_sweep(ids.feature(), profile, path, SweepRails::default(), feature)
}

fn build(
    ids: &mut Ids,
    profile: &ExtrudeProfile,
    path: &[Curve3Kind],
    feature: &SweepFeature,
) -> BrepSolid {
    let solid = match eval(ids, profile, path, feature) {
        Ok(solid) => solid,
        Err(error) => panic!("the profile and path describe a buildable sweep: {error:?}"),
    };
    assert!(
        solid.validate(TOLERANCE).is_ok(),
        "sweep is a closed manifold solid"
    );
    solid
}

fn build_guided(
    ids: &mut Ids,
    profile: &ExtrudeProfile,
    path: &[Curve3Kind],
    guides: &[Vec<Curve3Kind>],
    feature: &SweepFeature,
) -> BrepSolid {
    let rails = SweepRails {
        guides,
        direction: None,
        tool: None,
        bodies: &[],
    };
    let solid = match evaluate_sweep(ids.feature(), profile, path, rails, feature) {
        Ok(solid) => solid,
        Err(error) => panic!("the guided sweep builds: {error:?}"),
    };
    assert!(
        solid.validate(TOLERANCE).is_ok(),
        "guided sweep is a closed manifold solid"
    );
    solid
}

fn mesh_volume(solid: &BrepSolid) -> f64 {
    mesh_volume_at(solid, 0.0002, 0.01)
}

fn mesh_volume_at(solid: &BrepSolid, chord_mm: f64, angle_rad: f64) -> f64 {
    let Ok(mesh) = solid.tessellate(
        ChordHeightTolerance::from_mm(chord_mm),
        AngleTolerance::from_radians(angle_rad),
    ) else {
        panic!("a valid sweep tessellates");
    };
    mesh.faces()
        .iter()
        .flat_map(|slab| {
            let positions = slab.positions().to_vec();
            slab.triangles().iter().map(move |triangle| {
                let p = |index: u32| positions[index as usize].coords_mm();
                let (ax, ay, az) = p(triangle[0]);
                let (bx, by, bz) = p(triangle[1]);
                let (cx, cy, cz) = p(triangle[2]);
                let cross = (by * cz - bz * cy, bz * cx - bx * cz, bx * cy - by * cx);
                (ax * cross.0 + ay * cross.1 + az * cross.2) / 6.0
            })
        })
        .sum()
}

fn dump(solid: &BrepSolid) -> String {
    let faces: Vec<String> = solid
        .iter_faces()
        .map(|face| format!("  {}", face.label()))
        .collect();
    format!(
        "faces:\n{}\ncounts: faces={} edges={} vertices={}",
        faces.join("\n"),
        solid.iter_faces().count(),
        solid.iter_edges().count(),
        solid.iter_vertices().count(),
    )
}

fn face_census(solid: &BrepSolid) -> (usize, usize, usize) {
    solid
        .iter_faces()
        .fold((0, 0, 0), |(starts, ends, sides), face| {
            match face.label().role {
                FaceRole::StartCap => (starts + 1, ends, sides),
                FaceRole::EndCap => (starts, ends + 1, sides),
                FaceRole::Side { .. } => (starts, ends, sides + 1),
                FaceRole::Imported { .. } => (starts, ends, sides),
            }
        })
}

fn max_z(solid: &BrepSolid) -> f64 {
    solid
        .iter_vertices()
        .map(|vertex| vertex.position().coords_mm().2)
        .fold(f64::MIN, f64::max)
}

fn max_y(solid: &BrepSolid) -> f64 {
    solid
        .iter_vertices()
        .map(|vertex| vertex.position().coords_mm().1)
        .fold(f64::MIN, f64::max)
}

type Fixture = dyn FnOnce(&mut Ids) -> (ExtrudeProfile, Vec<Curve3Kind>, SweepFeature);

fn vol(setup: Box<Fixture>, expected: f64, tol: f64) {
    vol_at(setup, expected, tol, 0.0002, 0.01);
}

fn vol_at(setup: Box<Fixture>, expected: f64, tol: f64, chord: f64, angle: f64) {
    let mut ids = Ids::new();
    let (profile, path, feature) = setup(&mut ids);
    let solid = build(&mut ids, &profile, &path, &feature);
    let volume = mesh_volume_at(&solid, chord, angle).abs();
    assert!(
        (volume - expected).abs() < tol,
        "a swept solid matches its analytic volume: got {volume}, want {expected}"
    );
}

fn expect_defect(setup: Box<Fixture>, expected: SweepDefect) {
    let mut ids = Ids::new();
    let (profile, path, feature) = setup(&mut ids);
    let result = eval(&mut ids, &profile, &path, &feature);
    assert_eq!(
        sweep_defect(&result),
        Some(expected),
        "an invalid sweep configuration reports its typed defect, not a fabricated solid"
    );
}

#[test]
#[rustfmt::skip]
fn smooth_reductions_match_analytic_volume() {
    let area = 1.0 * 1.0 - 0.7 * 0.7;
    let bend20 = 3.0 + 20.0 * FRAC_PI_2 + 3.0;
    let bend13 = 3.0 + 1.3 * FRAC_PI_2 + 3.0;
    vol(Box::new(|i| (xz_rect(i, 2.0, 0.0, 1.0, 1.0), vec![circle_path(xy_plane(), 2.5)], follow())), TAU * 2.5, 0.05);
    vol(Box::new(|i| (xy_rect(i, 0.0, 0.0, 2.0, 1.0), zline(5.0), circular_boss(2.0))), PI * 5.0, 0.05);
    vol(Box::new(|i| (xy_rect(i, 0.0, 0.0, 1.0, 1.0), vec![circle_path(xy_plane(), 5.0)], circular_boss(2.0))), 2.0 * PI * PI * 5.0, 2.0 * PI * PI * 5.0 * 0.01);
    vol(Box::new(|i| (xy_rect(i, 0.0, 0.0, 1.0, 1.0), vec![arc_path_xz(Point3::origin(), 5.0, 0.0, FRAC_PI_2)], circular_boss(2.0))), PI * 5.0 * FRAC_PI_2, PI * 5.0 * FRAC_PI_2 * 0.01);
    vol(Box::new(|i| (xy_rect(i, 0.0, 0.0, 1.0, 1.0), zline(5.0), circular_tube(2.0, 0.3))), PI * area * 5.0, PI * area * 5.0 * 0.01);
    vol(Box::new(|i| (xy_rect(i, 0.0, 0.0, 1.0, 1.0), vec![circle_path(xy_plane(), 5.0)], circular_tube(2.0, 0.3))), 2.0 * PI * PI * 5.0 * area, 2.0 * PI * PI * 5.0 * area * 0.02);
    vol(Box::new(|i| (xy_loop(disc_as_arcs(i, 1.0)), xline(4.0), follow())), PI * 4.0, PI * 4.0 * 0.02);
    vol(Box::new(|i| (xy_loop(disc(i, 1.0)), zline(5.0), follow())), PI * 5.0, 0.02);
    vol(Box::new(|i| (xy_rect(i, 0.0, 0.0, 2.0, 1.0), zline(4.0), follow())), 8.0, 1.0e-6);
    vol(Box::new(|i| (xy_rect(i, 0.0, 0.0, 2.0, 1.0), off_perpendicular_path().to_vec(), follow())), 8.0, 1.0e-2);
    vol(Box::new(|i| (xy_loop(disc(i, 1.0)), off_perpendicular_path().to_vec(), follow())), PI * 4.0, PI * 4.0 * 0.02);
    vol(Box::new(|i| (xy_loop(ellipse_loop(i, 2.0, 1.0)), off_perpendicular_path().to_vec(), follow())), PI * 2.0 * 4.0, PI * 2.0 * 4.0 * 0.02);
    vol(Box::new(|i| { let outer = rectangle(i, 0.0, 0.0, 4.0, 4.0); let hole = rectangle_cw(i, 1.0, 1.0, 2.0, 2.0); (ExtrudeProfile::new(xy_plane(), vec![outer, hole]), off_perpendicular_path().to_vec(), follow()) }), 4.0f64.mul_add(4.0, -(2.0 * 2.0)) * 4.0, 1.0e-2);
    vol_at(Box::new(|i| (xy_rect(i, -1.0, -0.5, 2.0, 1.0), line_arc_line_bend(20.0, 3.0, 3.0).to_vec(), follow())), 2.0 * bend20, 2.0 * bend20 * 0.02, 0.1, 0.15);
    vol_at(Box::new(|i| (xy_rect(i, 0.0, 0.0, 1.0, 1.0), line_arc_line_bend(1.3, 3.0, 3.0).to_vec(), circular_boss(2.0))), PI * bend13, PI * bend13 * 0.05, 0.02, 0.03);
    vol_at(Box::new(|i| (xy_rect(i, 0.0, 0.0, 1.0, 1.0), line_arc_line_bend(20.0, 3.0, 3.0).to_vec(), circular_boss(2.0))), PI * bend20, PI * bend20 * 0.03, 0.05, 0.05);
}

#[test]
#[rustfmt::skip]
fn miter_reductions_match_analytic_volume() {
    let staircase = vec![
        line_path(Point3::origin(), pt(0.0, 0.0, 3.0)),
        line_path(pt(0.0, 0.0, 3.0), pt(3.0, 0.0, 3.0)),
        line_path(pt(3.0, 0.0, 3.0), pt(3.0, 0.0, 6.0)),
    ];
    let oblique_path = vec![
        line_path(Point3::origin(), pt(0.0, 0.0, 4.0)),
        line_path(pt(0.0, 0.0, 4.0), pt(4.0, 0.0, 8.0)),
    ];
    let oblique = 4.0 * (4.0 + (4.0_f64 * 4.0 + 4.0 * 4.0).sqrt());
    let yz = plane(Point3::origin(), UnitVec3::y_axis(), UnitVec3::z_axis());
    vol(Box::new(move |i| (xy_rect(i, -0.5, -0.5, 1.0, 1.0), staircase, follow())), 9.0, 9.0 * 0.005);
    vol(Box::new(|i| (xy_rect(i, 0.0, 0.0, 1.0, 1.0), right_angle_corner_path().to_vec(), circular_boss(2.0))), PI * 8.0, PI * 8.0 * 0.01);
    vol(Box::new(move |i| (xy_rect(i, -1.0, -1.0, 2.0, 2.0), oblique_path, follow())), oblique, oblique * 0.005);
    vol_at(Box::new(move |i| (ExtrudeProfile::new(yz, vec![rectangle(i, -0.2, -0.2, 0.4, 0.4)]), line_arc_corner_path().to_vec(), follow())), 0.16 * (2.0 + FRAC_PI_2), 0.16 * (2.0 + FRAC_PI_2) * 0.035, 0.003, 0.02);
    vol(Box::new(|i| (xy_loop(ellipse_loop(i, 2.0, 1.0)), right_angle_corner_path().to_vec(), follow())), PI * 2.0 * 8.0, PI * 2.0 * 8.0 * 0.01);
}

#[test]
#[rustfmt::skip]
fn thin_walls_match_spec_volume() {
    let spec = |d, t, t2, cap| thin_boss(thin_spec(d, t, t2, cap));
    vol(Box::new(move |i| (xy_rect(i, 0.0, 0.0, 4.0, 4.0), zline(5.0), spec(ThinWallDirection::Inward, 0.5, None, false))), (4.0 * 4.0 - 3.0 * 3.0) * 5.0, (4.0 * 4.0 - 3.0 * 3.0) * 5.0 * 0.01);
    vol(Box::new(move |i| (xy_rect(i, 0.0, 0.0, 4.0, 4.0), zline(5.0), spec(ThinWallDirection::MidPlane, 0.5, None, false))), (4.5 * 4.5 - 3.5 * 3.5) * 5.0, (4.5 * 4.5 - 3.5 * 3.5) * 5.0 * 0.01);
    vol(Box::new(move |i| (xy_rect(i, 0.0, 0.0, 4.0, 4.0), zline(5.0), spec(ThinWallDirection::TwoDirection, 0.5, Some(0.25), false))), (5.0 * 5.0 - 3.5 * 3.5) * 5.0, (5.0 * 5.0 - 3.5 * 3.5) * 5.0 * 0.01);
    vol(Box::new(move |i| (xy_rect(i, 0.0, 0.0, 4.0, 4.0), zline(5.0), spec(ThinWallDirection::Inward, 0.5, None, true))), 4.0 * 4.0 * 5.0 - 3.0 * 3.0 * 4.0, (4.0 * 4.0 * 5.0 - 3.0 * 3.0 * 4.0) * 0.01);
    vol(Box::new(move |i| (xy_rect(i, 0.0, 0.0, 2.0, 1.0), off_perpendicular_path().to_vec(), spec(ThinWallDirection::Inward, 0.2, None, false))), 2.0f64.mul_add(1.0, -(1.6 * 0.6)) * 4.0, 1.0e-2);
    vol(Box::new(move |i| (xy_rect(i, 0.0, 0.0, 2.0, 1.0), off_perpendicular_path().to_vec(), spec(ThinWallDirection::Inward, 0.2, None, true))), 2.0f64.mul_add(1.0, 0.0) * 4.0 - 1.6 * 0.6 * (4.0 - 2.0 * 0.2), 1.0e-2);
}

#[test]
#[rustfmt::skip]
fn invalid_sweep_configurations_report_typed_defects() {
    expect_defect(Box::new(|i| (xy_rect(i, -6.0, -6.0, 12.0, 12.0), right_angle_corner_path().to_vec(), follow())), SweepDefect::CornerTooTight);
    expect_defect(Box::new(|i| (xy_rect(i, -1.0, -1.0, 2.0, 2.0), vec![line_path(Point3::origin(), pt(0.0, 0.0, 4.0)), line_path(pt(0.0, 0.0, 4.0), pt(0.0, 0.0, 1.0))], follow())), SweepDefect::CornerTooTight);
    expect_defect(Box::new(|i| (xy_rect(i, -1.0, -1.0, 2.0, 2.0), vec![line_path(Point3::origin(), pt(0.0, 0.0, 4.0)), line_path(pt(0.0, 0.0, 4.0), pt(0.1, 0.0, 0.5))], follow())), SweepDefect::CornerTooTight);
    expect_defect(Box::new(|i| (xy_rect(i, -2.0, -1.0, 4.0, 2.0), line_arc_line_bend(1.0, 2.0, 2.0).to_vec(), follow())), SweepDefect::PathTooTight);
    expect_defect(Box::new(|i| (xy_rect(i, 0.0, 0.0, 2.0, 1.0), vec![line_path(Point3::origin(), pt(0.0, 0.0, 4.0)), line_path(pt(0.0, 0.0, 6.0), pt(0.0, 0.0, 10.0))], follow())), SweepDefect::DisconnectedPath);
    expect_defect(Box::new(|i| { let (profile, path) = mid_line_setup(i, 20.0); (profile, path.to_vec(), directed(SweepDirection::DirectionOne)) }), SweepDefect::PathMissesProfile);
    expect_defect(Box::new(|i| { let (profile, path) = mid_line_setup(i, 0.0); (profile, path.to_vec(), directed(SweepDirection::DirectionTwo)) }), SweepDefect::EmptySpan);
    expect_defect(Box::new(|i| { let (profile, path) = mid_line_setup(i, 4.0); (profile, path.to_vec(), SweepFeature { end_tangency: SweepTangency::PathTangent, ..directed(SweepDirection::Bidirectional) }) }), SweepDefect::TangencyBidirectional);
}

#[test]
fn degenerate_profiles_are_rejected_not_silently_repaired() {
    [
        (
            open_chain as fn(&mut Ids) -> ProfileLoop,
            ProfileDefect::OpenLoop,
        ),
        (bowtie, ProfileDefect::SelfIntersectingLoop),
    ]
    .into_iter()
    .for_each(|(loop_fn, expected)| {
        let mut ids = Ids::new();
        let profile = xy_loop(loop_fn(&mut ids));
        let result = eval(&mut ids, &profile, &off_perpendicular_path(), &follow());
        assert_eq!(
            profile_defect(&result),
            Some(expected),
            "a degenerate profile is refused, not silently closed"
        );
    });
}

#[test]
fn a_capped_thin_sweep_along_an_arc_seals_a_recessed_void() {
    let mut ids = Ids::new();
    let profile_plane = plane(pt(5.0, 0.0, 0.0), UnitVec3::x_axis(), UnitVec3::y_axis());
    let profile = ExtrudeProfile::new(
        profile_plane,
        vec![rectangle(&mut ids, -1.0, -0.5, 2.0, 1.0)],
    );
    let feature = thin_boss(thin_spec(ThinWallDirection::Inward, 0.2, None, true));
    let path = [arc_path_xz(Point3::origin(), 5.0, 0.0, FRAC_PI_2)];
    let solid = build(&mut ids, &profile, &path, &feature);
    let volume = mesh_volume_at(&solid, 0.005, 0.02).abs();
    let outer = 2.0 * 5.0 * FRAC_PI_2;
    let inner = 0.96 * 5.0 * (FRAC_PI_2 - 2.0 * 0.2 / 5.0);
    let expected = outer - inner;
    assert!(
        (volume - expected).abs() / expected < 0.03,
        "a capped thin frame swept along a quarter arc seals a recessed void: got {volume}, want {expected}"
    );
    let (starts, ends, _) = face_census(&solid);
    assert_eq!(
        (starts, ends),
        (2, 2),
        "the outer wall cap and the inner void cap stay distinct faces at each end: got {starts} start, {ends} end"
    );
}

#[test]
fn a_right_angle_miter_elbow_caps_extends_and_fills() {
    let mut ids = Ids::new();
    let profile = xy_rect(&mut ids, -1.0, -1.0, 2.0, 2.0);
    let solid = build(&mut ids, &profile, &right_angle_corner_path(), &follow());
    let expected = 2.0 * 2.0 * 8.0;
    let volume = mesh_volume(&solid);
    assert!(
        (volume - expected).abs() / expected < 0.005,
        "a right-angle rectangle sweep miters into an elbow of area times centerline: got {volume}, want {expected}"
    );
    let (starts, ends, sides) = face_census(&solid);
    assert_eq!(
        (starts, ends),
        (1, 1),
        "the elbow keeps one start cap and one end cap"
    );
    assert_eq!(
        sides,
        8,
        "two straight runs each carry the four-edge profile into four side faces: got {}",
        dump(&solid)
    );
    assert!(
        (max_z(&solid) - 5.0).abs() < 1.0e-6,
        "the bisecting miter carries the inner corner to z = 5, one profile half-width past the z = 4 vertex, not a square cap: got {}",
        max_z(&solid)
    );
}

#[test]
fn a_square_and_asymmetric_profile_sweep_a_torus() {
    let mut ids = Ids::new();
    let rho = 3.0;
    let profile_plane = plane(pt(rho, 0.0, 0.0), UnitVec3::x_axis(), UnitVec3::z_axis());
    let square = ExtrudeProfile::new(
        profile_plane,
        vec![rectangle(&mut ids, -0.3, -0.3, 0.6, 0.6)],
    );
    let square_solid = build(&mut ids, &square, &two_arc_circle(rho), &follow());
    let square_volume = mesh_volume_at(&square_solid, 0.005, 0.03).abs();
    let square_expected = 0.36 * 2.0 * PI * rho;
    assert!(
        (square_volume - square_expected).abs() / square_expected < 0.03,
        "a 0.6 mm square swept around a radius-3 circle is a {square_expected} mm^3 torus: got {square_volume}"
    );
    let asym = ExtrudeProfile::new(
        profile_plane,
        vec![rectangle(&mut ids, -0.3, -0.1, 0.6, 0.2)],
    );
    let asym_solid = build(&mut ids, &asym, &two_arc_circle(rho), &follow());
    let (lo, hi) = asym_solid
        .iter_vertices()
        .map(|vertex| vertex.position().coords_mm().2)
        .fold((f64::MAX, f64::MIN), |(lo, hi), z| (lo.min(z), hi.max(z)));
    assert!(
        hi - lo < 0.4,
        "the 0.2 mm axial side stays axial rather than the 0.6 mm radial side twisting onto it: got {}",
        hi - lo
    );
    let asym_volume = mesh_volume_at(&asym_solid, 0.005, 0.03).abs();
    let asym_expected = 0.6 * 0.2 * 2.0 * PI * rho;
    assert!(
        (asym_volume - asym_expected).abs() / asym_expected < 0.03,
        "the asymmetric profile still sweeps a {asym_expected} mm^3 torus: got {asym_volume}"
    );
}

#[test]
fn a_twisted_corner_on_a_curved_run_stays_gated() {
    let mut ids = Ids::new();
    let profile = xy_rect(&mut ids, -0.5, -0.5, 1.0, 1.0);
    let feature = SweepFeature {
        twist: Some(twist(90.0)),
        ..follow()
    };
    let error = eval(&mut ids, &profile, &line_arc_corner_path(), &feature);
    assert_eq!(
        sweep_gap(&error),
        Some(TruckGap::SweepPathCorner),
        "twist along a mitered curved corner is not yet supported and stays gated"
    );
}

#[test]
fn a_reversed_profile_winding_still_builds_an_outward_solid() {
    let mut ids = Ids::new();
    let standard = xy_rect(&mut ids, 0.0, 0.0, 2.0, 1.0);
    let reversed =
        ExtrudeProfile::new(xy_plane(), vec![rectangle_cw(&mut ids, 0.0, 0.0, 2.0, 1.0)]);
    let standard_solid = build(&mut ids, &standard, &off_perpendicular_path(), &follow());
    let reversed_solid = build(&mut ids, &reversed, &off_perpendicular_path(), &follow());
    let standard_volume = mesh_volume(&standard_solid);
    let reversed_volume = mesh_volume(&reversed_solid);
    assert!(
        standard_volume > 0.0,
        "a counter-clockwise profile builds an outward solid: got {standard_volume}"
    );
    assert!(
        reversed_volume > 0.0,
        "a reversed winding is normalized to an outward solid, not an inverted void: got {reversed_volume}"
    );
    assert!(
        (standard_volume - reversed_volume).abs() < 1.0e-3,
        "both windings describe the same solid: {standard_volume} vs {reversed_volume}"
    );
}

#[test]
fn a_keep_normal_arc_sweep_holds_parallel_and_tessellates_watertight()
-> Result<(), Box<dyn std::error::Error>> {
    use std::collections::HashMap;
    let mut ids = Ids::new();
    let profile = xz_rect(&mut ids, 2.0, 0.0, 1.0, 1.0);
    let path = [arc_path_xz(Point3::origin(), 4.0, 0.0, FRAC_PI_2)];
    let solid = build(&mut ids, &profile, &path, &keep_normal());
    let follow_solid = build(&mut ids, &profile, &path, &follow());
    let keep_normal_volume = mesh_volume_at(&solid, 0.05, 0.25);
    let follow_path_volume = mesh_volume_at(&follow_solid, 0.05, 0.25);
    assert!(
        (keep_normal_volume - 4.0).abs() < 0.04,
        "keep-normal holds the section parallel, so it fills area times the 4 mm Z-extent: got {keep_normal_volume}\n{}",
        dump(&solid)
    );
    assert!(
        (keep_normal_volume - follow_path_volume).abs() > 0.04,
        "keep-normal must not collapse into the follow-path revolve: keep-normal {keep_normal_volume}, follow-path {follow_path_volume}"
    );
    let mesh = solid.tessellate(
        ChordHeightTolerance::from_mm(0.05),
        AngleTolerance::from_radians(0.25),
    )?;
    let mut edges: HashMap<[(u64, u64, u64); 2], i32> = HashMap::new();
    mesh.faces().iter().for_each(|slab| {
        let pos = slab.positions();
        slab.triangles().iter().for_each(|tri| {
            let key = |i: u32| {
                let (x, y, z) = pos[i as usize].coords_mm();
                (x.to_bits(), y.to_bits(), z.to_bits())
            };
            [(tri[0], tri[1]), (tri[1], tri[2]), (tri[2], tri[0])]
                .into_iter()
                .for_each(|(a, b)| {
                    let (ka, kb) = (key(a), key(b));
                    let ordered = if ka <= kb { [ka, kb] } else { [kb, ka] };
                    *edges.entry(ordered).or_insert(0) += if ka <= kb { 1 } else { -1 };
                });
        });
    });
    let open = edges.values().filter(|balance| **balance != 0).count();
    assert_eq!(
        open, 0,
        "every mesh edge pairs with an oppositely-wound twin, so the coarse skin is closed"
    );
    assert!(
        mesh.validate(TOLERANCE).is_ok(),
        "the coarse skin mesh winds consistently and is manifold"
    );
    Ok(())
}

#[test]
fn a_twisted_prism_keeps_volume_side_keys_and_rotates_the_far_cap() {
    let mut ids = Ids::new();
    let profile = xy_rect(&mut ids, -1.0, -1.0, 2.0, 2.0);
    let path = zline(6.0);
    let half = SweepFeature {
        twist: Some(twist(180.0)),
        ..follow()
    };
    let half_solid = build(&mut ids, &profile, &path, &half);
    let half_volume = mesh_volume_at(&half_solid, 0.02, 0.1);
    assert!(
        (half_volume - 24.0).abs() < 0.3,
        "a twisted prism keeps the untwisted enclosed volume: got {half_volume}"
    );
    let twisted = |angle_rad: f64| SweepFeature {
        twist: Some(twist_rad(angle_rad)),
        ..follow()
    };
    let strong = build(&mut ids, &profile, &path, &twisted(FRAC_PI_2));
    let gentle = build(&mut ids, &profile, &path, &twisted(FRAC_PI_2 / 4.0));
    let side_keys = |solid: &BrepSolid| {
        let mut keys: Vec<SketchEntityId> = solid
            .iter_faces()
            .filter_map(|face| match face.label().role {
                FaceRole::Side { from, .. } => Some(from),
                _ => None,
            })
            .collect();
        keys.sort_unstable();
        keys
    };
    assert_eq!(
        side_keys(&strong),
        side_keys(&gentle),
        "changing the twist angle must not re-key the side faces"
    );
    assert_eq!(
        face_census(&strong),
        (1, 1, 4),
        "a quarter-turn twisted skin keeps one start cap, one end cap, and four sides: {}",
        dump(&strong)
    );
    assert_eq!(
        face_census(&gentle),
        (1, 1, 4),
        "a gentler twist keeps the same cap and side census: {}",
        dump(&gentle)
    );

    let cap_profile = xy_rect(&mut ids, -1.0, -0.5, 2.0, 1.0);
    let cap_path = zline(4.0);
    let plain = build(&mut ids, &cap_profile, &cap_path, &follow());
    let quarter = SweepFeature {
        twist: Some(twist(90.0)),
        ..follow()
    };
    let quarter_solid = build(&mut ids, &cap_profile, &cap_path, &quarter);
    let far_cap_reach_x = |solid: &BrepSolid| {
        solid
            .iter_vertices()
            .filter_map(|vertex| {
                let (x, _y, z) = vertex.position().coords_mm();
                (z > 3.5).then_some(x.abs())
            })
            .fold(0.0_f64, f64::max)
    };
    let plain_x = far_cap_reach_x(&plain);
    let twisted_x = far_cap_reach_x(&quarter_solid);
    assert!(
        (plain_x - 1.0).abs() < 1.0e-6,
        "the untwisted far cap keeps the long axis on x: got {plain_x}"
    );
    assert!(
        (twisted_x - 0.5).abs() < 1.0e-6,
        "a quarter-turn twist rotates the far cap's long axis off x: got {twisted_x}"
    );
}

#[test]
fn a_mid_line_sweep_spans_the_expected_volume_by_direction() {
    let cases: [(SweepFeature, f64, f64); 4] = [
        (directed(SweepDirection::DirectionOne), 12.0, 1.0e-3),
        (directed(SweepDirection::DirectionTwo), 8.0, 1.0e-3),
        (directed(SweepDirection::Bidirectional), 20.0, 1.0e-3),
        (
            SweepFeature {
                twist: Some(twist_rad(FRAC_PI_2)),
                ..directed(SweepDirection::Bidirectional)
            },
            20.0,
            20.0 * 0.01,
        ),
    ];
    cases.into_iter().for_each(|(feature, expected, tol)| {
        let mut ids = Ids::new();
        let (profile, path) = mid_line_setup(&mut ids, 4.0);
        let solid = build(&mut ids, &profile, &path, &feature);
        let volume = mesh_volume(&solid).abs();
        assert!(
            (volume - expected).abs() < tol,
            "the mid-line sweep spans its direction volume: got {volume}, want {expected}"
        );
    });
}

#[test]
fn a_mid_arc_profile_splits_the_revolve_by_direction() {
    let mut ids = Ids::new();
    let profile = xz_rect(&mut ids, 2.0, 0.0, 1.0, 1.0);
    let path = [arc3(xy_plane(), 2.5, -FRAC_PI_2, PI).as_kind()];
    let quarter = 1.0 * FRAC_PI_2 * 2.5;
    let one = build(
        &mut ids,
        &profile,
        &path,
        &directed(SweepDirection::DirectionOne),
    );
    let one_volume = mesh_volume(&one).abs();
    assert!(
        (one_volume - quarter).abs() < 0.05,
        "direction 1 revolves the quarter turn past the profile plane: got {one_volume}, want {quarter}"
    );
    let both = build(
        &mut ids,
        &profile,
        &path,
        &directed(SweepDirection::Bidirectional),
    );
    let both_volume = mesh_volume(&both).abs();
    assert!(
        (both_volume - 2.0 * quarter).abs() < 0.05,
        "bidirectional revolves the whole half turn through the profile plane: got {both_volume}"
    );
}

#[test]
fn a_mid_arm_elbow_spans_the_expected_volume_by_direction() {
    [
        (SweepDirection::Bidirectional, 2.0 * 2.0 * 8.0),
        (SweepDirection::DirectionOne, 2.0 * 2.0 * 6.0),
    ]
    .into_iter()
    .for_each(|(direction, expected)| {
        let mut ids = Ids::new();
        let section = plane(pt(0.0, 0.0, 2.0), UnitVec3::x_axis(), UnitVec3::y_axis());
        let profile = ExtrudeProfile::new(section, vec![rectangle(&mut ids, -1.0, -1.0, 2.0, 2.0)]);
        let solid = build(
            &mut ids,
            &profile,
            &right_angle_corner_path(),
            &directed(direction),
        );
        let volume = mesh_volume(&solid).abs();
        assert!(
            (volume - expected).abs() / expected < 0.005,
            "seeding the miter mid-arm spans its direction volume: got {volume}, want {expected}"
        );
    });
}

#[test]
fn a_guide_scales_and_a_short_guide_truncates_the_sweep() {
    let mut ids = Ids::new();
    let profile = xy_loop(disc(&mut ids, 2.0));
    let path = zline(8.0);
    let horn_guide = vec![line_path(pt(2.0, 0.0, 0.0), pt(0.5, 0.0, 8.0))];
    let horn = build_guided(&mut ids, &profile, &path, &[horn_guide], &follow());
    let horn_expected = 14.0 * PI;
    let horn_volume = mesh_volume(&horn).abs();
    assert!(
        (horn_volume - horn_expected).abs() / horn_expected < 0.02,
        "a linearly converging guide tapers the cylinder into a horn: got {horn_volume}, want {horn_expected}"
    );
    let short_guide = vec![line_path(pt(2.0, 0.0, 0.0), pt(1.4, 0.0, 4.0))];
    let short = build_guided(&mut ids, &profile, &path, &[short_guide], &follow());
    let short_expected = PI * (8.0 - 1.4_f64.powi(3)) / 0.45;
    let short_volume = mesh_volume(&short).abs();
    assert!(
        (short_volume - short_expected).abs() / short_expected < 0.03,
        "the sweep stops where the shortest guide ends: got {short_volume}, want {short_expected}"
    );
    assert!(
        (max_z(&short) - 4.0).abs() < 0.1,
        "the truncated sweep ends at the guide terminus: got z {}",
        max_z(&short)
    );
}

#[test]
fn two_guides_span_the_section_anisotropically() {
    let mut ids = Ids::new();
    let profile = xy_rect(&mut ids, -1.0, 0.0, 2.0, 1.0);
    let path = zline(8.0);
    let first = vec![line_path(pt(1.0, 0.0, 0.0), pt(2.0, 0.0, 8.0))];
    let second = vec![line_path(pt(0.0, 1.0, 0.0), pt(0.0, 3.0, 8.0))];
    let solid = build_guided(&mut ids, &profile, &path, &[first, second], &follow());
    let expected = 16.0 * (1.0 + 1.5 + 2.0 / 3.0);
    let volume = mesh_volume(&solid).abs();
    assert!(
        (volume - expected).abs() / expected < 0.02,
        "two guides stretch width and height independently: got {volume}, want {expected}"
    );
}

#[test]
fn the_first_guide_orientation_twists_the_section_with_the_guide_azimuth() {
    let mut ids = Ids::new();
    let contact_x = 50.0_f64.sqrt() * 0.6;
    let profile = xy_rect(&mut ids, contact_x - 1.0, -4.0, 2.0, 1.0);
    let path = zline(8.0);
    let tracked = build_guided(
        &mut ids,
        &profile,
        &path,
        &[azimuth_arc_guide()],
        &boss(
            SweepProfileKind::Sketch,
            SweepOrientation::FollowPathAndFirstGuide,
        ),
    );
    let plain = build_guided(&mut ids, &profile, &path, &[azimuth_arc_guide()], &follow());
    assert!(
        max_y(&tracked) > 3.0,
        "tracking guide 1 swings the section to the far azimuth: max y {}",
        max_y(&tracked)
    );
    assert!(
        max_y(&plain) < 0.0,
        "plain follow-path only scales toward the guide, it never rotates: max y {}",
        max_y(&plain)
    );
}

#[test]
fn guide_configuration_defects_are_typed() {
    let mut ids = Ids::new();
    let profile = xy_loop(disc(&mut ids, 2.0));
    let path = zline(8.0);
    let touching = || vec![line_path(pt(2.0, 0.0, 0.0), pt(1.0, 0.0, 8.0))];
    let missing = || vec![line_path(pt(5.0, 0.0, 0.0), pt(5.0, 0.0, 8.0))];
    let run = |ids: &mut Ids, guides: &[Vec<Curve3Kind>], feature: &SweepFeature| {
        let rails = SweepRails {
            guides,
            direction: None,
            tool: None,
            bodies: &[],
        };
        evaluate_sweep(ids.feature(), &profile, &path, rails, feature)
    };
    let cases: [(Vec<Vec<Curve3Kind>>, SweepFeature, SweepDefect); 5] = [
        (vec![missing()], follow(), SweepDefect::GuideMissesProfile),
        (
            vec![touching()],
            SweepFeature {
                direction: SweepDirection::Bidirectional,
                ..follow()
            },
            SweepDefect::GuideBidirectional,
        ),
        (
            vec![touching()],
            SweepFeature {
                twist: Some(twist_rad(1.0)),
                ..follow()
            },
            SweepDefect::GuideTwist,
        ),
        (
            vec![touching()],
            keep_normal(),
            SweepDefect::GuideKeepNormal,
        ),
        (
            vec![touching()],
            boss(
                SweepProfileKind::Sketch,
                SweepOrientation::FollowFirstAndSecondGuide,
            ),
            SweepDefect::OrientationNeedsGuides,
        ),
    ];
    cases.into_iter().for_each(|(guides, feature, expected)| {
        assert_eq!(
            sweep_defect(&run(&mut ids, &guides, &feature)),
            Some(expected),
            "a guide misconfiguration reports its typed defect"
        );
    });
    assert_eq!(
        sweep_gap(&run(
            &mut ids,
            &[touching(), touching(), touching()],
            &follow()
        )),
        Some(TruckGap::SweepGuideCount),
        "more than two guides stays an honest gap"
    );
}

#[test]
fn a_keep_normal_arc_skin_labels_caps_reshapes_with_tangency_and_caps_a_path_vertex() {
    let mut ids = Ids::new();
    let profile = xy_rect(&mut ids, -1.0, -1.0, 2.0, 2.0);
    let path = arc10();
    let plain = build(&mut ids, &profile, &path, &keep_normal());
    assert_eq!(
        face_census(&plain),
        (1, 1, 4),
        "a keep-normal skin keeps one start cap, one end cap, and one side face per profile segment: {}",
        dump(&plain)
    );
    let tangent_feature = SweepFeature {
        start_tangency: SweepTangency::PathTangent,
        end_tangency: SweepTangency::PathTangent,
        ..keep_normal()
    };
    let clamped = build(&mut ids, &profile, &path, &tangent_feature);
    let plain_volume = mesh_volume_at(&plain, 0.1, 0.15).abs();
    let clamped_volume = mesh_volume_at(&clamped, 0.1, 0.15).abs();
    let perpendicular = 4.0 * 10.0 * FRAC_PI_2;
    assert!(
        clamped_volume > plain_volume * 1.1,
        "path tangency reorients the ends toward the path, enlarging a keep-normal skin past its sheared volume: {plain_volume} vs {clamped_volume}"
    );
    assert!(
        clamped_volume < perpendicular * 1.05,
        "the reshaped skin stays bounded by a fully path-perpendicular sweep: {clamped_volume} vs {perpendicular}"
    );

    let on_path = xy_rect(&mut ids, 0.0, 0.0, 2.0, 1.0);
    let shifted = xy_rect(&mut ids, 1.0, 0.0, 2.0, 1.0);
    let on_solid = build(&mut ids, &on_path, &path, &keep_normal());
    let off_solid = build(&mut ids, &shifted, &path, &keep_normal());
    let (starts, ends, _) = face_census(&on_solid);
    assert_eq!(
        (starts, ends),
        (1, 1),
        "a profile corner sitting on the path start still caps both ends: {}",
        dump(&on_solid)
    );
    let on_volume = mesh_volume(&on_solid).abs();
    let off_volume = mesh_volume(&off_solid).abs();
    assert!(
        on_volume > 0.0,
        "the on-path skin encloses volume: got {on_volume}"
    );
    assert!(
        off_volume > 0.0,
        "the shifted skin encloses volume: got {off_volume}"
    );
    assert!(
        (on_volume - off_volume).abs() / off_volume < 0.02,
        "shifting the profile off the path start keeps the transported volume: got {on_volume} vs {off_volume}"
    );
}

#[test]
fn a_direction_vector_locks_the_up_axis() -> Result<(), Box<dyn std::error::Error>> {
    let mut ids = Ids::new();
    let locked = || SweepFeature {
        alignment: direction_vector(),
        ..follow()
    };
    let straight_profile = xy_rect(&mut ids, 0.0, 0.0, 2.0, 1.0);
    let straight_path = zline(6.0);
    let straight = evaluate_sweep(
        ids.feature(),
        &straight_profile,
        &straight_path,
        dir_rails(UnitVec3::x_axis()),
        &locked(),
    )?;
    let straight_volume = mesh_volume(&straight).abs();
    assert!(
        (straight_volume - 12.0).abs() < 1.0e-3,
        "a straight direction-locked sweep is still the drawn prism: got {straight_volume}"
    );
    let arc = arc10();
    let swing_profile = xy_rect(&mut ids, -1.0, -0.25, 2.0, 0.5);
    let follow_solid = build(&mut ids, &swing_profile, &arc, &follow());
    let tilted = bone_types::Vec3::from_mm(1.0, 1.0, 0.0).try_normalize(TOLERANCE)?;
    let rolled = evaluate_sweep(
        ids.feature(),
        &swing_profile,
        &arc,
        dir_rails(tilted),
        &locked(),
    )?;
    assert!(
        (max_y(&rolled) - max_y(&follow_solid)).abs() > 0.05,
        "locking the up-vector to a tilted direction rolls the end section out of the arc plane: {} vs {}",
        max_y(&rolled),
        max_y(&follow_solid)
    );
    let refuse_profile = xy_rect(&mut ids, 0.0, 0.0, 2.0, 1.0);
    let result = evaluate_sweep(
        ids.feature(),
        &refuse_profile,
        &arc,
        dir_rails(UnitVec3::z_axis()),
        &locked(),
    );
    assert_eq!(
        sweep_defect(&result),
        Some(SweepDefect::AlignmentAlongPath),
        "the quarter arc starts tangent to z, so a z direction vector cannot orient the frame"
    );
    Ok(())
}

#[test]
fn planar_alignment_accepts_a_flat_path_and_refuses_a_bent_one() {
    let mut ids = Ids::new();
    let profile = xy_rect(&mut ids, -1.0, -0.5, 2.0, 1.0);
    let feature = SweepFeature {
        alignment: bone_kernel::SweepAlignment::NonePlanar,
        ..follow()
    };
    let solid = build(&mut ids, &profile, &arc10(), &feature);
    assert!(
        mesh_volume(&solid).abs() > 1.0,
        "planar alignment builds along a planar arc"
    );
    let result = eval(&mut ids, &profile, &out_of_plane_bend(), &feature);
    assert_eq!(
        sweep_defect(&result),
        Some(SweepDefect::PathNotPlanar),
        "planar alignment must refuse a path that leaves its plane"
    );
}

#[test]
fn all_faces_alignment_needs_a_body() {
    let mut ids = Ids::new();
    let (profile, path) = mid_line_setup(&mut ids, 0.0);
    let feature = all_faces();
    let result = eval(&mut ids, &profile, &path, &feature);
    assert!(
        matches!(result, Err(BrepError::SweepAlignmentUnresolved)),
        "all-faces alignment needs an in-scope face the path lies on"
    );
}

#[test]
fn all_faces_alignment_lays_the_section_on_the_face_normal()
-> Result<(), Box<dyn std::error::Error>> {
    let mut ids = Ids::new();
    let plate_profile = xy_rect(&mut ids, -10.0, -10.0, 20.0, 20.0);
    let plate = build(&mut ids, &plate_profile, &zline(4.0), &follow());
    let profile_plane = plane(pt(-5.0, 0.0, 4.0), UnitVec3::y_axis(), UnitVec3::z_axis());
    let profile = ExtrudeProfile::new(
        profile_plane,
        vec![rectangle(&mut ids, -0.1, -0.5, 0.2, 1.0)],
    );
    let path = [line_path(pt(-5.0, 0.0, 4.0), pt(5.0, 0.0, 4.0))];
    let feature = all_faces();
    let bodies = [&plate];
    let solid = evaluate_sweep(
        ids.feature(),
        &profile,
        &path,
        body_rails(&bodies),
        &feature,
    )?;
    let heights: Vec<f64> = solid
        .iter_vertices()
        .map(|vertex| vertex.position().coords_mm().2)
        .collect();
    assert!(
        heights.iter().all(|&z| (3.85..=4.15).contains(&z)),
        "the section up-vector tracks the +Z face normal, so the thin ribbon hugs z=4: got {heights:?}"
    );
    Ok(())
}

#[test]
fn all_faces_alignment_wraps_a_ribbon_onto_a_cylinder() -> Result<(), Box<dyn std::error::Error>> {
    let mut ids = Ids::new();
    let cylinder = cylinder_tool(&mut ids, Point3::origin(), pt(0.0, 0.0, 10.0), 6.0);
    let profile_plane = plane(pt(3.0, 0.0, 5.0), UnitVec3::x_axis(), UnitVec3::z_axis());
    let profile = ExtrudeProfile::new(
        profile_plane,
        vec![rectangle(&mut ids, -0.2, -0.5, 0.4, 1.0)],
    );
    let arc_plane = plane(pt(0.0, 0.0, 5.0), UnitVec3::x_axis(), UnitVec3::y_axis());
    let arc = [arc3(arc_plane, 3.0, 0.0, FRAC_PI_2).as_kind()];
    let feature = all_faces();
    let bodies = [&cylinder];
    let solid = evaluate_sweep(ids.feature(), &profile, &arc, body_rails(&bodies), &feature)?;
    let radii: Vec<f64> = solid
        .iter_vertices()
        .map(|vertex| {
            let (x, y, _) = vertex.position().coords_mm();
            x.hypot(y)
        })
        .collect();
    assert!(
        radii.iter().all(|&r| (2.7..=3.3).contains(&r)),
        "the section up-vector tracks the varying radial face normal, so every vertex hugs radius 3: got {radii:?}"
    );
    Ok(())
}

#[test]
fn a_cylindrical_tool_sweeps_a_coaxial_envelope() -> Result<(), Box<dyn std::error::Error>> {
    let mut ids = Ids::new();
    let tool = cylinder_tool(&mut ids, Point3::origin(), pt(0.0, 0.0, 4.0), 2.0);
    let profile = xy_rect(&mut ids, 0.0, 0.0, 1.0, 1.0);
    let path = zline(6.0);
    let envelope = evaluate_sweep(
        ids.feature(),
        &profile,
        &path,
        tool_rails(&tool),
        &solid_tool_cut(),
    )?;
    let expected = PI * 1.0 * 1.0 * 10.0;
    let volume = mesh_volume(&envelope).abs();
    assert!(
        (volume - expected).abs() / expected < 0.02,
        "a cylinder tool swept along its own axis is one longer cylinder: got {volume}, want {expected}"
    );
    let (min_z, max_z) = envelope
        .iter_vertices()
        .map(|vertex| vertex.position().coords_mm().2)
        .fold((f64::MAX, f64::MIN), |(lo, hi), z| (lo.min(z), hi.max(z)));
    assert!(
        min_z.abs() < 1.0e-6 && (max_z - 10.0).abs() < 1.0e-6,
        "the envelope spans from the rear cap to the far cap plus the travel: got {min_z}..{max_z}"
    );
    Ok(())
}

#[test]
fn a_solid_tool_refuses_invalid_configurations() {
    let mut ids = Ids::new();
    let tool = cylinder_tool(&mut ids, Point3::origin(), pt(0.0, 0.0, 4.0), 2.0);
    let profile = xy_rect(&mut ids, 0.0, 0.0, 1.0, 1.0);
    let cases: [(SweepFeature, Vec<Curve3Kind>, SweepDefect); 3] = [
        (
            boss(
                SweepProfileKind::Solid {
                    tool: BodyRef::new(FeatureId::default(), BodyOrdinal::origin()),
                },
                SweepOrientation::FollowPath,
            ),
            zline(6.0),
            SweepDefect::SolidToolBoss,
        ),
        (
            solid_tool_cut(),
            right_angle_corner_path().to_vec(),
            SweepDefect::SolidToolCorner,
        ),
        (
            solid_tool_cut(),
            vec![line_path(pt(0.0, 0.0, 20.0), pt(0.0, 0.0, 26.0))],
            SweepDefect::SolidToolOffBody,
        ),
    ];
    cases.into_iter().for_each(|(feature, path, defect)| {
        let result = evaluate_sweep(ids.feature(), &profile, &path, tool_rails(&tool), &feature);
        assert_eq!(
            sweep_defect(&result),
            Some(defect),
            "a solid tool refuses an invalid configuration"
        );
    });
}

#[test]
fn a_non_cylindrical_tool_is_gated() {
    let mut ids = Ids::new();
    let prism_profile = xy_rect(&mut ids, 0.0, 0.0, 2.0, 2.0);
    let tool = build(&mut ids, &prism_profile, &zline(4.0), &follow());
    let profile = xy_rect(&mut ids, 0.0, 0.0, 1.0, 1.0);
    let result = evaluate_sweep(
        ids.feature(),
        &profile,
        &zline(6.0),
        tool_rails(&tool),
        &solid_tool_cut(),
    );
    assert_eq!(
        sweep_gap(&result),
        Some(TruckGap::SweepSolidTool),
        "a box prism tool body stays an honest gap"
    );
}

#[test]
fn a_solid_tool_envelope_stays_gated() {
    let mut ids = Ids::new();
    let tool = cylinder_tool(&mut ids, Point3::origin(), pt(0.0, 0.0, 4.0), 2.0);
    let profile = xy_rect(&mut ids, 0.0, 0.0, 1.0, 1.0);
    let cases: [(SweepFeature, Vec<Curve3Kind>); 3] = [
        (solid_tool_cut(), xline(6.0)),
        (
            SweepFeature {
                twist: Some(twist_rad(1.0)),
                ..solid_tool_cut()
            },
            zline(6.0),
        ),
        (
            SweepFeature {
                direction: SweepDirection::Bidirectional,
                ..solid_tool_cut()
            },
            zline(6.0),
        ),
    ];
    cases.into_iter().for_each(|(feature, path)| {
        let result = evaluate_sweep(ids.feature(), &profile, &path, tool_rails(&tool), &feature);
        assert_eq!(
            sweep_gap(&result),
            Some(TruckGap::SweepSolidEnvelope),
            "a solid tool sweep past the cylindrical fast path stays an honest gap"
        );
    });
}

fn assert_builds_identically_twice(fixture: impl Fn(&mut Ids) -> BrepSolid) {
    let mut first_ids = Ids::new();
    let first = fixture(&mut first_ids);
    let mut second_ids = Ids::new();
    let second = fixture(&mut second_ids);
    assert_eq!(
        dump(&first),
        dump(&second),
        "sweep topology is deterministic"
    );
    assert!(
        (mesh_volume(&first) - mesh_volume(&second)).abs() < f64::EPSILON,
        "sweep geometry is deterministic"
    );
}

#[test]
fn a_sweep_builds_identically_twice() {
    let revolved = |ids: &mut Ids| {
        let profile = xz_rect(ids, 2.0, 0.0, 1.0, 1.0);
        build(ids, &profile, &[circle_path(xy_plane(), 2.5)], &follow())
    };
    let mitered = |ids: &mut Ids| {
        let profile = xy_rect(ids, -1.0, -1.0, 2.0, 2.0);
        build(ids, &profile, &right_angle_corner_path(), &follow())
    };
    assert_builds_identically_twice(revolved);
    assert_builds_identically_twice(mitered);
}
