use bone_kernel::TruckGap;
use bone_kernel::{
    Arc2, Bezier2, BrepEdge, BrepError, BrepSolid, Circle2, Curve2Kind, Ellipse2, ExtrudeProfile,
    ExtrudeSense, Line2, ProfileDefect, ProfileEdge, ProfileLoop, ResolvedRevolve, RevolveAngle,
    RevolveAxis, RevolveDirection2, RevolveEndCondition, RevolveFeature, RevolveScene, ThinPlan,
    ThinWall, ThinWallDirection, evaluate_revolve, resolve_revolve,
};
use bone_types::{
    Angle, AngleTolerance, Axis3, ChordHeightTolerance, EdgeRole, FaceLabel, FaceRole, FeatureId,
    Length, Plane3, Point2, Point3, PositiveLength, SideKind, SketchEntityId, SketchId, Tolerance,
    UnitVec3, VertexLabel, VertexRole, millimeter, radian,
};
use core::f64::consts::{PI, TAU};
use slotmap::SlotMap;

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

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

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

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

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

    fn sketch(&mut self) -> SketchId {
        self.sketches.insert(())
    }
}

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 xz_plane() -> Plane3 {
    let Ok(plane) = Plane3::new(
        Point3::origin(),
        UnitVec3::x_axis(),
        UnitVec3::z_axis(),
        TOLERANCE,
    ) else {
        panic!("x and z axes are orthonormal");
    };
    plane
}

fn z_axis() -> Axis3 {
    Axis3::new(Point3::origin(), UnitVec3::z_axis())
}

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];
            ProfileEdge::new(line(start, end), ids.entity(), ids.entity())
        })
        .collect();
    ProfileLoop::Open(edges)
}

fn plan(forward_rad: f64, backward_rad: f64) -> ResolvedRevolve {
    ResolvedRevolve {
        axis: z_axis(),
        forward_rad,
        backward_rad,
        thin: None,
    }
}

fn build(ids: &mut Ids, profile: &ExtrudeProfile, plan: &ResolvedRevolve) -> BrepSolid {
    let revolve = ids.feature();
    let solid = match evaluate_revolve(revolve, profile, plan) {
        Ok(solid) => solid,
        Err(error) => panic!("the profile and plan describe a buildable revolve: {error:?}"),
    };
    assert!(
        solid.validate(TOLERANCE).is_ok(),
        "revolve is a closed manifold solid"
    );
    solid
}

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 mesh_volume(solid: &BrepSolid) -> f64 {
    let Ok(mesh) = solid.tessellate(
        ChordHeightTolerance::from_mm(0.0002),
        AngleTolerance::from_radians(0.01),
    ) else {
        panic!("a valid revolve 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 edge_roles(solid: &BrepSolid) -> Vec<EdgeRole> {
    solid.iter_edges().map(|edge| edge.label().role).collect()
}

fn vertex_roles(solid: &BrepSolid) -> Vec<VertexRole> {
    solid
        .iter_vertices()
        .map(|vertex| vertex.label().role)
        .collect()
}

#[test]
fn a_partial_revolve_names_every_edge_and_vertex_from_the_profile() {
    let mut ids = Ids::new();
    let profile = ExtrudeProfile::new(xz_plane(), vec![rectangle(&mut ids, 2.0, 0.0, 1.0, 1.0)]);
    let solid = build(&mut ids, &profile, &plan(PI / 2.0, 0.0));
    let edges = edge_roles(&solid);
    let vertices = vertex_roles(&solid);
    assert!(
        !edges
            .iter()
            .any(|role| matches!(role, EdgeRole::Imported { .. })),
        "a quarter revolve derives every edge name from the profile: {edges:?}"
    );
    assert!(
        !vertices
            .iter()
            .any(|role| matches!(role, VertexRole::Imported { .. })),
        "a quarter revolve derives every vertex name from the profile: {vertices:?}"
    );
    assert!(
        edges
            .iter()
            .any(|role| matches!(role, EdgeRole::StartCapEdge { .. })),
        "a partial revolve carries start-cap edges: {edges:?}"
    );
    assert!(
        edges
            .iter()
            .any(|role| matches!(role, EdgeRole::EndCapEdge { .. })),
        "a partial revolve carries end-cap edges: {edges:?}"
    );
    assert!(
        edges.iter().any(|role| matches!(
            role,
            EdgeRole::SideEdge {
                side: SideKind::Corner,
                ..
            }
        )),
        "the rib between two side faces is a corner edge: {edges:?}"
    );
    assert!(
        vertices
            .iter()
            .any(|role| matches!(role, VertexRole::StartCapVertex { .. })),
        "a partial revolve carries start-cap vertices: {vertices:?}"
    );
    assert!(
        vertices
            .iter()
            .any(|role| matches!(role, VertexRole::EndCapVertex { .. })),
        "a partial revolve carries end-cap vertices: {vertices:?}"
    );
}

#[test]
fn a_full_revolve_names_ribs_and_seams_without_falling_back_to_import_ordinals() {
    let mut ids = Ids::new();
    let profile = ExtrudeProfile::new(xz_plane(), vec![rectangle(&mut ids, 2.0, 0.0, 1.0, 1.0)]);
    let solid = build(&mut ids, &profile, &plan(TAU, 0.0));
    let edges = edge_roles(&solid);
    assert!(
        !edges
            .iter()
            .any(|role| matches!(role, EdgeRole::Imported { .. })),
        "a full revolve names every edge as a rib or a seam: {edges:?}"
    );
    assert!(
        edges.iter().any(|role| matches!(
            role,
            EdgeRole::SideEdge {
                side: SideKind::Corner,
                ..
            }
        )),
        "adjacent side faces meet at a corner rib: {edges:?}"
    );
    assert!(
        !edges.iter().any(|role| matches!(
            role,
            EdgeRole::StartCapEdge { .. } | EdgeRole::EndCapEdge { .. }
        )),
        "a full revolve has no cap edges: {edges:?}"
    );
}

#[test]
fn full_revolve_of_an_offset_rectangle_is_a_ring() {
    let mut ids = Ids::new();
    let profile = ExtrudeProfile::new(xz_plane(), vec![rectangle(&mut ids, 2.0, 0.0, 1.0, 1.0)]);
    let solid = build(&mut ids, &profile, &plan(TAU, 0.0));
    insta::assert_snapshot!(dump(&solid));
}

#[test]
fn quarter_revolve_of_an_offset_rectangle_has_caps() {
    let mut ids = Ids::new();
    let profile = ExtrudeProfile::new(xz_plane(), vec![rectangle(&mut ids, 2.0, 0.0, 1.0, 1.0)]);
    let solid = build(&mut ids, &profile, &plan(PI / 2.0, 0.0));
    insta::assert_snapshot!(dump(&solid));
}

#[test]
fn midplane_revolve_spans_both_sides() {
    let mut ids = Ids::new();
    let profile = ExtrudeProfile::new(xz_plane(), vec![rectangle(&mut ids, 2.0, 0.0, 1.0, 1.0)]);
    let solid = build(&mut ids, &profile, &plan(PI / 4.0, PI / 4.0));
    insta::assert_snapshot!(dump(&solid));
}

#[test]
fn revolve_evaluation_is_deterministic() {
    let mut ids = Ids::new();
    let profile = ExtrudeProfile::new(xz_plane(), vec![rectangle(&mut ids, 2.0, 0.0, 1.0, 1.0)]);
    let revolve = ids.feature();
    [plan(TAU, 0.0), plan(PI / 2.0, 0.0)]
        .iter()
        .for_each(|plan| {
            let Ok(first) = evaluate_revolve(revolve, &profile, plan) else {
                panic!("profile revolves");
            };
            let Ok(second) = evaluate_revolve(revolve, &profile, plan) else {
                panic!("profile revolves");
            };
            assert_eq!(first.to_blob().ok(), second.to_blob().ok());
            assert_eq!(dump(&first), dump(&second));
        });
}

proptest::proptest! {
    #![proptest_config(proptest::prelude::ProptestConfig::with_cases(64))]
    #[test]
    fn varied_revolves_are_deterministic(
        offset in 0.5f64..40.0,
        width in 0.5f64..20.0,
        height in 0.5f64..20.0,
        angle in 0.2f64..TAU,
    ) {
        let mut ids = Ids::new();
        let profile = ExtrudeProfile::new(
            xz_plane(),
            vec![rectangle(&mut ids, offset, 0.0, width, height)],
        );
        let revolve = ids.feature();
        let plan = plan(angle, 0.0);
        let Ok(first) = evaluate_revolve(revolve, &profile, &plan) else {
            panic!("offset rectangle revolves");
        };
        let Ok(second) = evaluate_revolve(revolve, &profile, &plan) else {
            panic!("offset rectangle revolves");
        };
        proptest::prop_assert!(first.validate(TOLERANCE).is_ok());
        let expected = angle * (offset + width / 2.0) * width * height;
        let volume = mesh_volume(&first);
        proptest::prop_assert!(
            (volume - expected).abs() < expected * 1.0e-3,
            "every revolve is outward with the Pappus volume; got {} want {}",
            volume,
            expected
        );
        proptest::prop_assert_eq!(dump(&first), dump(&second));
    }
}

#[test]
fn a_revolve_of_a_profile_on_the_far_side_of_the_axis_is_outward() {
    let mut ids = Ids::new();
    let profile = ExtrudeProfile::new(xz_plane(), vec![rectangle(&mut ids, -3.0, 0.0, 1.0, 1.0)]);
    let solid = build(&mut ids, &profile, &plan(TAU, 0.0));
    let expected = TAU * 2.5 * 1.0;
    let volume = mesh_volume(&solid);
    assert!(
        (volume - expected).abs() < 1.0e-3,
        "a profile on the far side of the axis revolves outward too; got {volume}"
    );
}

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

fn ellipse(ids: &mut Ids, cx: f64, cy: f64, semi_major: f64, semi_minor: f64) -> ProfileLoop {
    let Ok(curve) = Ellipse2::new(
        point(cx, cy),
        Length::new::<millimeter>(semi_major),
        Length::new::<millimeter>(semi_minor),
        Angle::new::<radian>(0.0),
        TOLERANCE,
    ) else {
        panic!("ellipse semi-axes are positive");
    };
    ProfileLoop::Closed {
        curve: Curve2Kind::Ellipse(curve),
        curve_entity: ids.entity(),
    }
}

fn straight_bezier(a: Point2, b: Point2) -> Curve2Kind {
    let (ax, ay) = a.coords_mm();
    let (bx, by) = b.coords_mm();
    let third = Point2::from_mm(ax + (bx - ax) / 3.0, ay + (by - ay) / 3.0);
    let two_thirds = Point2::from_mm(ax + 2.0 * (bx - ax) / 3.0, ay + 2.0 * (by - ay) / 3.0);
    Curve2Kind::Bezier(Bezier2::new([a, third, two_thirds, b]))
}

fn bezier_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| {
            ProfileEdge::new(
                straight_bezier(corners[index], corners[(index + 1) % 4]),
                ids.entity(),
                ids.entity(),
            )
        })
        .collect();
    ProfileLoop::Open(edges)
}

fn cap_azimuth_deg(solid: &BrepSolid, role: FaceRole) -> f64 {
    let pos: std::collections::HashMap<_, _> = solid
        .iter_vertices()
        .map(|vertex| (vertex.id(), vertex.position().coords_mm()))
        .collect();
    let Some(face) = solid.iter_faces().find(|face| face.label().role == role) else {
        panic!("cap face is present");
    };
    let edges = solid.face_boundary_edges(face.id());
    let (sx, sy, count) = solid
        .iter_edges()
        .filter(|edge| edges.contains(&edge.id()))
        .flat_map(BrepEdge::vertices)
        .fold((0.0, 0.0, 0.0), |(ax, ay, n), vertex| {
            let (x, y, _) = pos[&vertex];
            (ax + x, ay + y, n + 1.0)
        });
    (sy / count).atan2(sx / count).to_degrees()
}

fn azimuth_delta_deg(a: f64, b: f64) -> f64 {
    let delta = (a - b).rem_euclid(360.0);
    delta.min(360.0 - delta)
}

#[test]
fn cap_roles_track_the_sweep_extremes_across_180_degrees() {
    [
        (90.0_f64, 0.0_f64, 0.0_f64, 90.0_f64),
        (270.0, 0.0, 0.0, 270.0),
        (45.0, 45.0, -45.0, 45.0),
    ]
    .into_iter()
    .for_each(|(forward, backward, start_az, end_az)| {
        let mut ids = Ids::new();
        let profile =
            ExtrudeProfile::new(xz_plane(), vec![rectangle(&mut ids, 2.0, 0.0, 1.0, 1.0)]);
        let solid = build(
            &mut ids,
            &profile,
            &plan(forward.to_radians(), backward.to_radians()),
        );
        assert!(
            azimuth_delta_deg(cap_azimuth_deg(&solid, FaceRole::StartCap), start_az) < 1.0e-6,
            "start cap rides the -backward extreme for forward={forward} backward={backward}"
        );
        assert!(
            azimuth_delta_deg(cap_azimuth_deg(&solid, FaceRole::EndCap), end_az) < 1.0e-6,
            "end cap rides the +forward extreme for forward={forward} backward={backward}"
        );
    });
}

fn arc(center: Point2, start: Point2, end: Point2) -> Curve2Kind {
    let Ok(segment) = Arc2::from_center_start_end(center, start, end, TOLERANCE) else {
        panic!("arc endpoints describe a well-formed arc");
    };
    Curve2Kind::Arc(segment)
}

fn open_loop(ids: &mut Ids, curves: &[Curve2Kind]) -> ProfileLoop {
    let edges = curves
        .iter()
        .map(|curve| ProfileEdge::new(*curve, ids.entity(), ids.entity()))
        .collect();
    ProfileLoop::Open(edges)
}

#[test]
fn a_rectangle_with_an_axial_edge_revolves_to_a_solid_cylinder() {
    let mut ids = Ids::new();
    let (radius, height) = (1.5, 4.0);
    let profile = ExtrudeProfile::new(
        xz_plane(),
        vec![rectangle(&mut ids, 0.0, 0.0, radius, height)],
    );
    let solid = build(&mut ids, &profile, &plan(TAU, 0.0));
    let expected = PI * radius * radius * height;
    let volume = mesh_volume(&solid);
    assert!(
        (volume - expected).abs() < expected * 2.0e-3,
        "an axial-edge rectangle revolves to a solid cylinder; got {volume} want {expected}"
    );
    assert!(
        solid
            .iter_faces()
            .all(|face| matches!(face.label().role, FaceRole::Side { .. })),
        "the bored cylinder is bounded entirely by surfaces of revolution"
    );
}

#[test]
fn a_triangle_with_an_axial_edge_revolves_to_a_cone() {
    let mut ids = Ids::new();
    let (radius, height) = (2.0, 3.0);
    let loop_ = open_loop(
        &mut ids,
        &[
            line(point(0.0, 0.0), point(radius, 0.0)),
            line(point(radius, 0.0), point(0.0, height)),
            line(point(0.0, height), point(0.0, 0.0)),
        ],
    );
    let profile = ExtrudeProfile::new(xz_plane(), vec![loop_]);
    let solid = build(&mut ids, &profile, &plan(TAU, 0.0));
    let expected = PI * radius * radius * height / 3.0;
    let volume = mesh_volume(&solid);
    assert!(
        (volume - expected).abs() < expected * 2.0e-3,
        "an axial-edge triangle revolves to a cone; got {volume} want {expected}"
    );
}

#[test]
fn a_semicircle_against_the_axis_revolves_to_a_sphere() {
    let mut ids = Ids::new();
    let radius = 2.0;
    let loop_ = open_loop(
        &mut ids,
        &[
            arc(point(0.0, 0.0), point(0.0, -radius), point(0.0, radius)),
            line(point(0.0, radius), point(0.0, -radius)),
        ],
    );
    let profile = ExtrudeProfile::new(xz_plane(), vec![loop_]);
    let solid = build(&mut ids, &profile, &plan(TAU, 0.0));
    let expected = 4.0 / 3.0 * PI * radius * radius * radius;
    let volume = mesh_volume(&solid);
    assert!(
        (volume - expected).abs() < expected * 2.0e-3,
        "a semicircle against the axis revolves to a sphere; got {volume} want {expected}"
    );
}

#[test]
fn a_partial_revolve_of_an_axis_touching_profile_is_gated() {
    let mut ids = Ids::new();
    let profile = ExtrudeProfile::new(xz_plane(), vec![rectangle(&mut ids, 0.0, 0.0, 1.0, 1.0)]);
    let revolve = ids.feature();
    assert!(matches!(
        evaluate_revolve(revolve, &profile, &plan(PI, 0.0)),
        Err(BrepError::TruckUnsupported {
            detail: TruckGap::RevolvePolePartial
        })
    ));
}

#[test]
fn a_profile_touching_the_axis_at_one_vertex_is_rejected() {
    let mut ids = Ids::new();
    let corners = [point(0.0, 1.0), point(1.0, 0.0), point(1.0, 2.0)];
    let edges = (0..3)
        .map(|index| {
            ProfileEdge::new(
                line(corners[index], corners[(index + 1) % 3]),
                ids.entity(),
                ids.entity(),
            )
        })
        .collect();
    let profile = ExtrudeProfile::new(xz_plane(), vec![ProfileLoop::Open(edges)]);
    let revolve = ids.feature();
    assert!(matches!(
        evaluate_revolve(revolve, &profile, &plan(PI / 2.0, 0.0)),
        Err(BrepError::RevolveProfileTouchesAxis)
    ));
}

#[test]
fn a_profile_spanning_both_sides_of_the_axis_is_rejected() {
    let mut ids = Ids::new();
    let profile = ExtrudeProfile::new(xz_plane(), vec![rectangle(&mut ids, -0.5, 0.0, 1.0, 1.0)]);
    let revolve = ids.feature();
    assert!(matches!(
        evaluate_revolve(revolve, &profile, &plan(PI / 2.0, 0.0)),
        Err(BrepError::RevolveProfileCrossesAxis)
    ));
}

#[test]
fn a_full_revolve_of_a_circle_is_a_torus() {
    let mut ids = Ids::new();
    let profile = ExtrudeProfile::new(xz_plane(), vec![circle(&mut ids, 5.0, 0.0, 1.0)]);
    let solid = build(&mut ids, &profile, &plan(TAU, 0.0));
    assert_eq!(
        solid.iter_faces().count(),
        1,
        "a full torus is a single periodic toroidal face"
    );
    assert!(
        solid
            .iter_faces()
            .all(|face| matches!(face.label().role, FaceRole::Side { .. })),
        "the toroidal surface is a side face attributed to the profile circle"
    );
    assert!(
        solid.bounding_box().is_some(),
        "an edge-free torus still frames via its tessellated faces"
    );
    let Some(bbox) = solid.bounding_box() else {
        panic!("torus has a bounding box");
    };
    let (min, max) = (bbox.min().coords_mm(), bbox.max().coords_mm());
    assert!(
        (max.0 - 6.0).abs() < 0.05 && (min.0 + 6.0).abs() < 0.05,
        "the torus spans the major plus minor radius in plane: min={min:?} max={max:?}"
    );
}

#[test]
fn a_partial_revolve_of_a_circle_caps_the_torus() {
    let mut ids = Ids::new();
    let profile = ExtrudeProfile::new(xz_plane(), vec![circle(&mut ids, 5.0, 0.0, 1.0)]);
    let solid = build(&mut ids, &profile, &plan(PI / 2.0, 0.0));
    assert_eq!(
        solid.iter_faces().count(),
        3,
        "a partial torus is the toroidal side plus a start and end cap"
    );
    let caps = solid
        .iter_faces()
        .filter(|face| matches!(face.label().role, FaceRole::StartCap | FaceRole::EndCap))
        .count();
    assert_eq!(caps, 2, "a partial torus carries a start and end cap disk");
}

#[test]
fn a_full_revolve_of_an_ellipse_is_an_elliptic_torus() {
    let mut ids = Ids::new();
    let (semi_major, semi_minor, centroid) = (1.5, 0.8, 5.0);
    let profile = ExtrudeProfile::new(
        xz_plane(),
        vec![ellipse(&mut ids, centroid, 0.0, semi_major, semi_minor)],
    );
    let solid = build(&mut ids, &profile, &plan(TAU, 0.0));
    assert!(
        solid
            .iter_faces()
            .all(|face| matches!(face.label().role, FaceRole::Side { .. })),
        "every face of a full elliptic torus is a side attributed to the profile ellipse"
    );
    let pappus = TAU * centroid * (PI * semi_major * semi_minor);
    let volume = mesh_volume(&solid).abs();
    assert!(
        (volume - pappus).abs() / pappus < 0.01,
        "elliptic torus volume tracks Pappus: got {volume}, expected {pappus}"
    );
}

#[test]
fn a_partial_revolve_of_an_ellipse_caps_the_solid() {
    let mut ids = Ids::new();
    let profile = ExtrudeProfile::new(xz_plane(), vec![ellipse(&mut ids, 5.0, 0.0, 1.5, 0.8)]);
    let solid = build(&mut ids, &profile, &plan(PI / 2.0, 0.0));
    let caps = solid
        .iter_faces()
        .filter(|face| matches!(face.label().role, FaceRole::StartCap | FaceRole::EndCap))
        .count();
    assert_eq!(
        caps, 2,
        "a partial elliptic revolve carries a start and end cap"
    );
}

#[test]
fn a_full_revolve_of_a_closed_bezier_loop_is_a_washer() {
    let mut ids = Ids::new();
    let (inner, outer, height) = (3.0, 5.0, 2.0);
    let profile = ExtrudeProfile::new(
        xz_plane(),
        vec![bezier_rectangle(
            &mut ids,
            inner,
            0.0,
            outer - inner,
            height,
        )],
    );
    let solid = build(&mut ids, &profile, &plan(TAU, 0.0));
    let washer = PI * (outer * outer - inner * inner) * height;
    let volume = mesh_volume(&solid).abs();
    assert!(
        (volume - washer).abs() / washer < 0.01,
        "a revolved closed bezier rectangle is a washer: got {volume}, expected {washer}"
    );
}

fn thin_plan(forward_rad: f64, backward_rad: f64, outer_mm: f64, inner_mm: f64) -> ResolvedRevolve {
    ResolvedRevolve {
        axis: z_axis(),
        forward_rad,
        backward_rad,
        thin: Some(ThinPlan {
            outer_mm,
            inner_mm,
            cap_ends: false,
            auto_fillet_mm: None,
        }),
    }
}

#[test]
fn a_full_thin_revolve_walls_the_closed_profile() {
    let mut ids = Ids::new();
    let profile = ExtrudeProfile::new(xz_plane(), vec![rectangle(&mut ids, 2.0, 0.0, 1.0, 2.0)]);
    let solid = build(&mut ids, &profile, &thin_plan(TAU, 0.0, 0.2, 0.2));
    assert_eq!(
        solid.iter_faces().count(),
        8,
        "a full thin revolve walls the rectangle into outer and inner rings, eight sides, no caps"
    );
}

#[test]
fn a_thin_revolve_auto_fillets_the_profile_corners() {
    let mut ids = Ids::new();
    let profile = ExtrudeProfile::new(xz_plane(), vec![rectangle(&mut ids, 2.0, 0.0, 1.0, 2.0)]);
    let plan = ResolvedRevolve {
        axis: z_axis(),
        forward_rad: TAU,
        backward_rad: 0.0,
        thin: Some(ThinPlan {
            outer_mm: 0.15,
            inner_mm: 0.15,
            cap_ends: false,
            auto_fillet_mm: Some(0.3),
        }),
    };
    let solid = build(&mut ids, &profile, &plan);
    assert!(
        solid.iter_faces().count() > 8,
        "auto-filleted corners add arc side faces beyond the eight straight walls; got {}",
        solid.iter_faces().count()
    );
}

fn open_chain(ids: &mut Ids, points: &[Point2]) -> ProfileLoop {
    let edges = points
        .windows(2)
        .map(|pair| ProfileEdge::new(line(pair[0], pair[1]), ids.entity(), ids.entity()))
        .collect();
    ProfileLoop::Chain(edges)
}

#[test]
fn a_thin_revolve_of_an_open_line_walls_a_tube() {
    let mut ids = Ids::new();
    let chain = open_chain(&mut ids, &[point(2.0, 0.0), point(2.0, 3.0)]);
    let profile = ExtrudeProfile::new(xz_plane(), vec![chain]);
    let solid = build(&mut ids, &profile, &thin_plan(TAU, 0.0, 0.2, 0.2));
    assert_eq!(
        solid.iter_faces().count(),
        4,
        "an open line revolved with wall thickness builds a tube of four faces"
    );
}

#[test]
fn a_thin_revolve_of_a_bent_open_chain_stays_a_manifold() {
    let mut ids = Ids::new();
    let chain = open_chain(
        &mut ids,
        &[point(3.0, 0.0), point(3.0, 2.0), point(1.5, 3.5)],
    );
    let profile = ExtrudeProfile::new(xz_plane(), vec![chain]);
    let solid = build(&mut ids, &profile, &thin_plan(PI / 2.0, 0.0, 0.15, 0.15));
    assert!(
        solid.iter_faces().count() >= 6,
        "a bent open chain walls into at least its mitred segments plus caps"
    );
}

#[test]
fn a_lone_line_revolve_without_thickness_is_rejected_as_zero_area() {
    let mut ids = Ids::new();
    let chain = open_chain(&mut ids, &[point(2.0, 0.0), point(2.0, 3.0)]);
    let profile = ExtrudeProfile::new(xz_plane(), vec![chain]);
    let revolve = ids.feature();
    assert!(matches!(
        evaluate_revolve(revolve, &profile, &plan(TAU, 0.0)),
        Err(BrepError::InvalidProfile {
            reason: ProfileDefect::ZeroArea
        })
    ));
}

#[test]
fn an_open_chain_revolve_without_thickness_auto_closes_to_a_solid() {
    let mut ids = Ids::new();
    let chain = open_chain(
        &mut ids,
        &[point(3.0, 0.0), point(3.0, 3.0), point(1.5, 4.0)],
    );
    let profile = ExtrudeProfile::new(xz_plane(), vec![chain]);
    let solid = build(&mut ids, &profile, &plan(TAU, 0.0));
    assert!(
        solid.iter_faces().count() >= 1,
        "a multi-segment open profile auto-closes its endpoints into a solid of revolution"
    );
}

#[test]
fn a_partial_thin_revolve_caps_the_walled_ring() {
    let mut ids = Ids::new();
    let profile = ExtrudeProfile::new(xz_plane(), vec![rectangle(&mut ids, 2.0, 0.0, 1.0, 2.0)]);
    let solid = build(&mut ids, &profile, &thin_plan(PI / 2.0, 0.0, 0.2, 0.2));
    let caps = solid
        .iter_faces()
        .filter(|face| matches!(face.label().role, FaceRole::StartCap | FaceRole::EndCap))
        .count();
    assert_eq!(
        caps, 2,
        "a partial thin revolve carries a start and end cap"
    );
    assert_eq!(
        solid.iter_faces().count(),
        10,
        "eight walls plus two annular caps"
    );
}

fn angle(rad: f64) -> RevolveAngle {
    let Ok(value) = RevolveAngle::new(Angle::new::<radian>(rad)) else {
        panic!("rad lies within (0, 2π]");
    };
    value
}

fn direction2(rad: f64) -> RevolveDirection2 {
    RevolveDirection2::blind(angle(rad))
}

#[test]
fn resolve_revolve_splits_directions_by_sense_and_midplane() {
    let mut ids = Ids::new();
    let sketch = ids.sketch();
    let axis = RevolveAxis::SketchLine(ids.entity());
    let z = z_axis();
    let feature = |end_condition, sense, second| RevolveFeature {
        sense,
        end_condition,
        direction2: second,
        ..RevolveFeature::blind(sketch, axis, angle(1.0))
    };
    let split = |feature: &RevolveFeature| {
        let Ok(resolved) = resolve_revolve(feature, &RevolveScene::angles(z)) else {
            panic!("feature resolves to a finite span");
        };
        (resolved.forward_rad, resolved.backward_rad)
    };

    let blind = RevolveEndCondition::Blind { angle: angle(1.0) };
    assert_eq!(
        split(&feature(blind, ExtrudeSense::Forward, None)),
        (1.0, 0.0)
    );
    assert_eq!(
        split(&feature(blind, ExtrudeSense::Reverse, None)),
        (0.0, 1.0)
    );

    let mid = RevolveEndCondition::MidPlane { angle: angle(2.0) };
    assert_eq!(
        split(&feature(mid, ExtrudeSense::Reverse, None)),
        (1.0, 1.0)
    );

    assert_eq!(
        split(&feature(
            blind,
            ExtrudeSense::Forward,
            Some(direction2(0.5))
        )),
        (1.0, 0.5)
    );
    assert_eq!(
        split(&feature(
            blind,
            ExtrudeSense::Reverse,
            Some(direction2(0.5))
        )),
        (0.5, 1.0)
    );
}

#[test]
fn resolve_revolve_rejects_over_full_turn_and_midplane_direction2() {
    let mut ids = Ids::new();
    let sketch = ids.sketch();
    let axis = RevolveAxis::SketchLine(ids.entity());
    let z = z_axis();

    let over = RevolveFeature {
        direction2: Some(direction2(5.0)),
        ..RevolveFeature::blind(sketch, axis, angle(5.0))
    };
    assert!(matches!(
        resolve_revolve(&over, &RevolveScene::angles(z)),
        Err(BrepError::RevolveAngleOutOfRange)
    ));

    let midplane_with_second = RevolveFeature {
        end_condition: RevolveEndCondition::MidPlane { angle: angle(1.0) },
        direction2: Some(direction2(1.0)),
        ..RevolveFeature::blind(sketch, axis, angle(1.0))
    };
    assert!(matches!(
        resolve_revolve(&midplane_with_second, &RevolveScene::angles(z)),
        Err(BrepError::TruckUnsupported {
            detail: TruckGap::MidPlaneDirection2
        })
    ));
}

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

fn revolve_with(ids: &mut Ids, end_condition: RevolveEndCondition) -> RevolveFeature {
    let axis = RevolveAxis::SketchLine(ids.entity());
    RevolveFeature {
        end_condition,
        ..RevolveFeature::blind(ids.sketch(), axis, angle(1.0))
    }
}

fn seam_vertex(ids: &mut Ids) -> VertexLabel {
    VertexLabel {
        feature: ids.feature(),
        role: VertexRole::Seam { from: ids.entity() },
    }
}

#[test]
fn resolve_revolve_up_to_vertex_resolves_to_the_vertex_azimuth() {
    let mut ids = Ids::new();
    let profile = ExtrudeProfile::new(xz_plane(), vec![rectangle(&mut ids, 2.0, 0.0, 1.0, 1.0)]);
    let vertex = seam_vertex(&mut ids);
    let target = Point3::from_mm(0.0, 2.5, 0.0);
    let no_face: &dyn Fn(FaceLabel) -> Option<Plane3> = &|_| None;
    let at_vertex: &dyn Fn(VertexLabel) -> Option<Point3> = &|_| Some(target);
    let scene = RevolveScene::new(z_axis(), &profile, no_face, at_vertex);
    let feature = revolve_with(&mut ids, RevolveEndCondition::UpToVertex { vertex });

    let Ok(resolved) = resolve_revolve(&feature, &scene) else {
        panic!("an up-to-vertex revolve resolves");
    };
    assert!(
        (resolved.forward_rad - PI / 2.0).abs() < 1.0e-9,
        "a forward quarter turn reaches the +y vertex; got {}",
        resolved.forward_rad
    );
    assert!(
        resolved.backward_rad.abs() < 1.0e-9,
        "a single forward direction leaves no backward sweep"
    );

    let solid = build(&mut ids, &profile, &resolved);
    let volume = mesh_volume(&solid);
    let expected = (PI / 2.0) * 2.5 * 1.0;
    assert!(
        (volume - expected).abs() < 1.0e-3,
        "the resolved plan builds the quarter ring; got {volume}, want {expected}"
    );
}

#[test]
fn resolve_revolve_up_to_surface_resolves_to_the_axis_plane_azimuth() {
    let mut ids = Ids::new();
    let profile = ExtrudeProfile::new(xz_plane(), vec![rectangle(&mut ids, 2.0, 0.0, 1.0, 1.0)]);
    let surface = FaceLabel {
        feature: ids.feature(),
        role: FaceRole::StartCap,
    };
    let plane = yz_plane_normal_x();
    let at_face: &dyn Fn(FaceLabel) -> Option<Plane3> = &|_| Some(plane);
    let no_vertex: &dyn Fn(VertexLabel) -> Option<Point3> = &|_| None;
    let scene = RevolveScene::new(z_axis(), &profile, at_face, no_vertex);
    let feature = revolve_with(&mut ids, RevolveEndCondition::UpToSurface { surface });

    let Ok(resolved) = resolve_revolve(&feature, &scene) else {
        panic!("an up-to-surface revolve resolves");
    };
    assert!(
        (resolved.forward_rad - PI / 2.0).abs() < 1.0e-9,
        "the profile sweeps a quarter turn onto the axis-containing yz plane; got {}",
        resolved.forward_rad
    );
}

#[test]
fn resolve_revolve_offset_from_surface_adds_the_signed_angular_offset() {
    let mut ids = Ids::new();
    let profile = ExtrudeProfile::new(xz_plane(), vec![rectangle(&mut ids, 2.0, 0.0, 1.0, 1.0)]);
    let surface = FaceLabel {
        feature: ids.feature(),
        role: FaceRole::StartCap,
    };
    let plane = yz_plane_normal_x();
    let at_face: &dyn Fn(FaceLabel) -> Option<Plane3> = &|_| Some(plane);
    let no_vertex: &dyn Fn(VertexLabel) -> Option<Point3> = &|_| None;
    let scene = RevolveScene::new(z_axis(), &profile, at_face, no_vertex);

    let offset = |reverse_offset| RevolveEndCondition::OffsetFromSurface {
        surface,
        offset: Angle::new::<radian>(PI / 4.0),
        reverse_offset,
    };
    let forward = revolve_with(&mut ids, offset(false));
    let Ok(resolved) = resolve_revolve(&forward, &scene) else {
        panic!("an offset-from-surface revolve resolves");
    };
    assert!(
        (resolved.forward_rad - 3.0 * PI / 4.0).abs() < 1.0e-9,
        "the offset adds to the surface azimuth; got {}",
        resolved.forward_rad
    );

    let reversed = revolve_with(&mut ids, offset(true));
    let Ok(resolved) = resolve_revolve(&reversed, &scene) else {
        panic!("a reversed offset-from-surface revolve resolves");
    };
    assert!(
        (resolved.forward_rad - PI / 4.0).abs() < 1.0e-9,
        "the reverse offset subtracts from the surface azimuth; got {}",
        resolved.forward_rad
    );
}

#[test]
fn resolve_revolve_direction2_takes_its_own_up_to_condition() {
    let mut ids = Ids::new();
    let profile = ExtrudeProfile::new(xz_plane(), vec![rectangle(&mut ids, 2.0, 0.0, 1.0, 1.0)]);
    let vertex = seam_vertex(&mut ids);
    let back = Point3::from_mm(0.0, -2.5, 0.0);
    let no_face: &dyn Fn(FaceLabel) -> Option<Plane3> = &|_| None;
    let at_vertex: &dyn Fn(VertexLabel) -> Option<Point3> = &|_| Some(back);
    let scene = RevolveScene::new(z_axis(), &profile, no_face, at_vertex);

    let with_dir2 = |reverse| RevolveFeature {
        direction2: Some(RevolveDirection2 {
            end_condition: RevolveEndCondition::UpToVertex { vertex },
            reverse,
        }),
        ..revolve_with(
            &mut Ids::new(),
            RevolveEndCondition::Blind {
                angle: angle(PI / 2.0),
            },
        )
    };

    let Ok(resolved) = resolve_revolve(&with_dir2(false), &scene) else {
        panic!("a direction-2 up-to-vertex revolve resolves");
    };
    assert!(
        (resolved.forward_rad - PI / 2.0).abs() < 1.0e-9
            && (resolved.backward_rad - PI / 2.0).abs() < 1.0e-9,
        "direction 1 holds its blind quarter while direction 2 reaches the -y vertex; got {:?}",
        (resolved.forward_rad, resolved.backward_rad)
    );

    let Ok(reversed) = resolve_revolve(&with_dir2(true), &scene) else {
        panic!("a reversed direction-2 revolve resolves");
    };
    assert!(
        (reversed.backward_rad - 3.0 * PI / 2.0).abs() < 1.0e-9,
        "a reversed direction 2 takes the long way to the vertex; got {}",
        reversed.backward_rad
    );
}

#[test]
fn a_full_revolve_has_the_pappus_volume_and_outward_orientation() {
    let mut ids = Ids::new();
    let profile = ExtrudeProfile::new(xz_plane(), vec![rectangle(&mut ids, 2.0, 0.0, 1.0, 1.0)]);
    let solid = build(&mut ids, &profile, &plan(TAU, 0.0));
    let expected = TAU * 2.5 * 1.0;
    let volume = mesh_volume(&solid);
    assert!(
        (volume - expected).abs() < 1.0e-3,
        "Pappus volume of the [2,3]x[0,1] ring is theta*radius*area; got {volume}, want {expected}"
    );
}

#[test]
fn a_quarter_revolve_has_a_quarter_of_the_ring_volume() {
    let mut ids = Ids::new();
    let profile = ExtrudeProfile::new(xz_plane(), vec![rectangle(&mut ids, 2.0, 0.0, 1.0, 1.0)]);
    let solid = build(&mut ids, &profile, &plan(PI / 2.0, 0.0));
    let expected = (PI / 2.0) * 2.5 * 1.0;
    let volume = mesh_volume(&solid);
    assert!(
        (volume - expected).abs() < 1.0e-3,
        "a quarter revolve carves a quarter of the ring; got {volume}, want {expected}"
    );
}

#[test]
fn a_revolve_of_a_profile_with_a_hole_is_a_hollow_ring() {
    let mut ids = Ids::new();
    let outer = rectangle(&mut ids, 2.0, 0.0, 2.0, 2.0);
    let hole = rectangle(&mut ids, 2.5, 0.5, 1.0, 1.0);
    let profile = ExtrudeProfile::new(xz_plane(), vec![outer, hole]);
    let solid = build(&mut ids, &profile, &plan(TAU, 0.0));
    let sides = solid
        .iter_faces()
        .filter(|face| matches!(face.label().role, FaceRole::Side { .. }))
        .count();
    assert_eq!(
        sides, 8,
        "the outer wall and the hole each revolve into four side faces"
    );
    assert!(
        !edge_roles(&solid)
            .iter()
            .any(|role| matches!(role, EdgeRole::Imported { .. })),
        "a profile-with-hole revolve names every edge from the profile"
    );
    let outer_volume = TAU * 3.0 * (2.0 * 2.0);
    let hole_volume = TAU * 3.0 * (1.0 * 1.0);
    let expected = outer_volume - hole_volume;
    let volume = mesh_volume(&solid);
    assert!(
        (volume - expected).abs() < 1.0e-2,
        "the hole hollows the ring; got {volume}, want {expected}"
    );
}

#[test]
fn evaluate_revolve_guards_a_malformed_plan() {
    let mut ids = Ids::new();
    let profile = ExtrudeProfile::new(xz_plane(), vec![rectangle(&mut ids, 2.0, 0.0, 1.0, 1.0)]);
    let revolve = ids.feature();
    [plan(2.0 * TAU, 0.0), plan(-1.0, 2.0), plan(0.0, 0.0)]
        .iter()
        .for_each(|bad| {
            assert!(matches!(
                evaluate_revolve(revolve, &profile, bad),
                Err(BrepError::RevolveAngleOutOfRange)
            ));
        });
}

fn thin_wall(direction: ThinWallDirection, first_mm: f64, second_mm: Option<f64>) -> ThinWall {
    let positive = |value: f64| {
        let Ok(length) = PositiveLength::new(Length::new::<millimeter>(value)) else {
            panic!("{value} mm is a positive thickness");
        };
        length
    };
    ThinWall {
        thickness: positive(first_mm),
        direction,
        thickness2: second_mm.map(positive),
        cap_ends: false,
        auto_fillet: None,
    }
}

#[test]
fn resolve_revolve_maps_each_thin_direction_to_its_walls() {
    let mut ids = Ids::new();
    let sketch = ids.sketch();
    let axis = RevolveAxis::SketchLine(ids.entity());
    let z = z_axis();
    let walls = |thin: ThinWall| {
        let feature = RevolveFeature {
            thin_wall: Some(thin),
            ..RevolveFeature::blind(sketch, axis, angle(1.0))
        };
        let Ok(resolved) = resolve_revolve(&feature, &RevolveScene::angles(z)) else {
            panic!("a thin revolve resolves to a finite span");
        };
        let Some(plan) = resolved.thin else {
            panic!("a thin wall resolves to a thin plan");
        };
        (plan.outer_mm, plan.inner_mm)
    };

    assert_eq!(
        walls(thin_wall(ThinWallDirection::Outward, 2.0, None)),
        (2.0, 0.0),
        "outward shells the whole thickness onto the outer wall"
    );
    assert_eq!(
        walls(thin_wall(ThinWallDirection::Inward, 2.0, None)),
        (0.0, 2.0),
        "inward shells the whole thickness onto the inner wall"
    );
    assert_eq!(
        walls(thin_wall(ThinWallDirection::MidPlane, 2.0, None)),
        (1.0, 1.0),
        "mid-plane splits the thickness across both walls"
    );
    assert_eq!(
        walls(thin_wall(ThinWallDirection::TwoDirection, 2.0, Some(0.5))),
        (2.0, 0.5),
        "two-direction takes the outer thickness and a separate inner thickness"
    );
    assert_eq!(
        walls(thin_wall(ThinWallDirection::TwoDirection, 2.0, None)),
        (2.0, 2.0),
        "two-direction without a second thickness mirrors the first onto both walls"
    );
}

#[test]
fn an_open_profile_loop_that_does_not_close_is_rejected() {
    let mut ids = Ids::new();
    let corners = [
        point(2.0, 0.0),
        point(3.0, 0.0),
        point(3.0, 1.0),
        point(2.0, 1.0),
    ];
    let edges = (0..3)
        .map(|index| {
            ProfileEdge::new(
                line(corners[index], corners[index + 1]),
                ids.entity(),
                ids.entity(),
            )
        })
        .collect();
    let profile = ExtrudeProfile::new(xz_plane(), vec![ProfileLoop::Open(edges)]);
    let revolve = ids.feature();
    assert!(matches!(
        evaluate_revolve(revolve, &profile, &plan(TAU, 0.0)),
        Err(BrepError::InvalidProfile {
            reason: ProfileDefect::OpenLoop
        })
    ));
}

#[test]
fn a_self_intersecting_profile_loop_is_rejected() {
    let mut ids = Ids::new();
    let corners = [
        point(2.0, 0.0),
        point(3.0, 2.0),
        point(2.0, 2.0),
        point(3.0, 0.0),
    ];
    let edges = (0..4)
        .map(|index| {
            ProfileEdge::new(
                line(corners[index], corners[(index + 1) % 4]),
                ids.entity(),
                ids.entity(),
            )
        })
        .collect();
    let profile = ExtrudeProfile::new(xz_plane(), vec![ProfileLoop::Open(edges)]);
    let revolve = ids.feature();
    assert!(matches!(
        evaluate_revolve(revolve, &profile, &plan(TAU, 0.0)),
        Err(BrepError::InvalidProfile {
            reason: ProfileDefect::SelfIntersectingLoop
        })
    ));
}
