use bone_interop::{StepError, read, write};
use bone_kernel::{
    BrepFace, BrepSolid, Circle2, Curve2Kind, ExtrudeDirection, ExtrudeEndCondition,
    ExtrudeFeature, ExtrudeProfile, ExtrudeSense, Line2, MergeResult, ProfileEdge, ProfileLoop,
    evaluate_extrude,
};
use bone_types::{
    FaceLabel, FeatureId, Length, Plane3, Point2, PositiveLength, SketchEntityId, SketchId,
    StepSchema, Tolerance, UnitVec3, millimeter,
};
use slotmap::{Key, SlotMap};

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

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

fn blind(depth_mm: f64) -> ExtrudeFeature {
    let Ok(depth) = PositiveLength::new(Length::new::<millimeter>(depth_mm)) else {
        panic!("positive depth");
    };
    ExtrudeFeature {
        sketch: SketchId::null(),
        direction: ExtrudeDirection::Normal {
            sense: ExtrudeSense::Forward,
        },
        end_condition: ExtrudeEndCondition::Blind { depth },
        draft: None,
        thin_wall: None,
        merge_result: MergeResult::Merge,
    }
}

fn rectangle(entities: &mut SlotMap<SketchEntityId, ()>) -> ProfileLoop {
    let point = |x: f64, y: f64| Point2::from_mm(x, y);
    let line = |a: Point2, b: Point2| {
        let Ok(segment) = Line2::new(a, b, TOL) else {
            panic!("distinct endpoints");
        };
        Curve2Kind::Line(segment)
    };
    let corners = [
        point(0.0, 0.0),
        point(4.0, 0.0),
        point(4.0, 2.0),
        point(0.0, 2.0),
    ];
    let edges = (0..4)
        .map(|index| {
            ProfileEdge::new(
                line(corners[index], corners[(index + 1) % 4]),
                entities.insert(()),
                entities.insert(()),
            )
        })
        .collect();
    ProfileLoop::Open(edges)
}

fn cylinder(entities: &mut SlotMap<SketchEntityId, ()>) -> ProfileLoop {
    let Ok(disk) = Circle2::new(
        Point2::from_mm(0.0, 0.0),
        Length::new::<millimeter>(5.0),
        TOL,
    ) else {
        panic!("positive radius");
    };
    ProfileLoop::Closed {
        curve: Curve2Kind::Circle(disk),
        curve_entity: entities.insert(()),
    }
}

fn build(loops: Vec<ProfileLoop>, depth_mm: f64) -> BrepSolid {
    let mut features: SlotMap<FeatureId, ()> = SlotMap::with_key();
    let profile = ExtrudeProfile::new(xy_plane(), loops);
    let Ok(solid) = evaluate_extrude(features.insert(()), &profile, &blind(depth_mm)) else {
        panic!("profile extrudes");
    };
    solid
}

fn cube() -> BrepSolid {
    let mut entities: SlotMap<SketchEntityId, ()> = SlotMap::with_key();
    build(vec![rectangle(&mut entities)], 5.0)
}

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

fn import_feature() -> FeatureId {
    let mut features: SlotMap<FeatureId, ()> = SlotMap::with_key();
    features.insert(())
}

fn donut(entities: &mut SlotMap<SketchEntityId, ()>) -> Vec<ProfileLoop> {
    let ring = |radius_mm: f64, entities: &mut SlotMap<SketchEntityId, ()>| {
        let Ok(disk) = Circle2::new(
            Point2::from_mm(0.0, 0.0),
            Length::new::<millimeter>(radius_mm),
            TOL,
        ) else {
            panic!("positive radius");
        };
        ProfileLoop::Closed {
            curve: Curve2Kind::Circle(disk),
            curve_entity: entities.insert(()),
        }
    };
    vec![ring(10.0, entities), ring(4.0, entities)]
}

fn step_round_trip_keeps_labels(solid: &BrepSolid) {
    let Ok(dir) = tempfile::tempdir() else {
        panic!("temp dir");
    };
    let path = dir.path().join("part.step");
    let Ok(()) = write(solid, &path, StepSchema::Ap214) else {
        panic!("write step");
    };
    let Ok(outcome) = read(&path, import_feature()) else {
        panic!("read step");
    };
    assert!(outcome.is_labeled(), "matching sidecar restores labels");
    assert_eq!(face_labels(solid), face_labels(outcome.solid()));
    assert!(outcome.solid().validate(TOL).is_ok());
}

#[test]
fn cube_step_round_trip_keeps_labels() {
    step_round_trip_keeps_labels(&cube());
}

#[test]
fn cylinder_step_round_trip_keeps_labels() {
    let mut entities: SlotMap<SketchEntityId, ()> = SlotMap::with_key();
    let solid = build(vec![cylinder(&mut entities)], 8.0);
    step_round_trip_keeps_labels(&solid);
}

#[test]
fn donut_step_round_trip_keeps_labels() {
    let mut entities: SlotMap<SketchEntityId, ()> = SlotMap::with_key();
    let solid = build(donut(&mut entities), 6.0);
    step_round_trip_keeps_labels(&solid);
}

#[test]
fn apostrophe_in_file_name_round_trips() {
    let solid = cube();
    let Ok(dir) = tempfile::tempdir() else {
        panic!("temp dir");
    };
    let path = dir.path().join("nel's bracket.step");
    let Ok(()) = write(&solid, &path, StepSchema::Ap214) else {
        panic!("write step");
    };
    let Ok(outcome) = read(&path, import_feature()) else {
        panic!("an apostrophe in the file name must not break the header");
    };
    assert!(outcome.is_labeled(), "matching sidecar restores labels");
    assert_eq!(face_labels(&solid), face_labels(outcome.solid()));
}

#[test]
fn step_export_is_byte_deterministic() {
    let solid = cube();
    let Ok(dir) = tempfile::tempdir() else {
        panic!("temp dir");
    };
    let path = dir.path().join("part.step");
    let Ok(()) = write(&solid, &path, StepSchema::Ap214) else {
        panic!("write first");
    };
    let Ok(a) = std::fs::read(&path) else {
        panic!("read first");
    };
    let Ok(()) = write(&solid, &path, StepSchema::Ap214) else {
        panic!("write second");
    };
    let Ok(b) = std::fs::read(&path) else {
        panic!("read second");
    };
    assert_eq!(a, b, "same solid and path write byte-identical step");
}

#[test]
fn missing_sidecar_imports_dumb_body() {
    let solid = cube();
    let Ok(dir) = tempfile::tempdir() else {
        panic!("temp dir");
    };
    let path = dir.path().join("part.step");
    let Ok(()) = write(&solid, &path, StepSchema::Ap214) else {
        panic!("write step");
    };
    let mut labels = dir.path().join("part.step").into_os_string();
    labels.push(".labels");
    let Ok(()) = std::fs::remove_file(std::path::PathBuf::from(labels)) else {
        panic!("remove sidecar");
    };
    let Ok(outcome) = read(&path, import_feature()) else {
        panic!("read step");
    };
    assert!(!outcome.is_labeled(), "no sidecar yields a dumb body");
    assert_eq!(
        outcome.solid().iter_faces().count(),
        solid.iter_faces().count()
    );
}

#[test]
fn ap242_export_is_unsupported() {
    let solid = cube();
    let Ok(dir) = tempfile::tempdir() else {
        panic!("temp dir");
    };
    let path = dir.path().join("part.step");
    assert!(matches!(
        write(&solid, &path, StepSchema::Ap242E2),
        Err(StepError::SchemaUnsupported(StepSchema::Ap242E2))
    ));
}

#[test]
fn cylinder_export_writes_step_and_sidecar() {
    let mut entities: SlotMap<SketchEntityId, ()> = SlotMap::with_key();
    let solid = build(vec![cylinder(&mut entities)], 8.0);
    let Ok(dir) = tempfile::tempdir() else {
        panic!("temp dir");
    };
    let path = dir.path().join("cyl.step");
    let Ok(()) = write(&solid, &path, StepSchema::Ap214) else {
        panic!("write step");
    };
    assert!(path.exists());
    let mut labels = path.clone().into_os_string();
    labels.push(".labels");
    assert!(std::path::PathBuf::from(labels).exists());
}
