use bone_document::{
    Document, EvaluatedSketch, FeatureNode, Sketch3, Sketch3Entity, Sketch3Relation,
    evaluate_extrude, evaluate_sketch,
};
use bone_kernel::{BrepEdge, BrepFace, BrepSolid, ExtrudeFeature};
use bone_types::{
    DocumentId, EdgeLabel, FaceLabel, FeatureId, Length, Point3, PositiveLength, SketchEntityId,
    SketchId, SketchPlaneBasis, Tolerance, UnitVec3, millimeter,
};
use slotmap::KeyData;

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

fn plane() -> SketchPlaneBasis {
    let Ok(basis) = SketchPlaneBasis::new(
        Point3::origin(),
        UnitVec3::x_axis(),
        UnitVec3::y_axis(),
        Tolerance::new(1e-9),
    ) else {
        panic!("xy plane");
    };
    basis
}

fn mm(v: f64) -> Length {
    Length::new::<millimeter>(v)
}

fn sketch_id(idx: u32) -> SketchId {
    SketchId::from(KeyData::from_ffi((1u64 << 32) | u64::from(idx)))
}

fn document_id(idx: u32) -> DocumentId {
    DocumentId::from(KeyData::from_ffi((1u64 << 32) | u64::from(idx)))
}

fn feature_id(idx: u32) -> FeatureId {
    FeatureId::from(KeyData::from_ffi((1u64 << 32) | u64::from(idx)))
}

fn add_point(s: &Sketch3, x: f64, y: f64) -> (Sketch3, SketchEntityId) {
    s.with_entity(Sketch3Entity::point(Point3::from_mm(x, y, 0.0)))
}

fn add_line(s: &Sketch3, a: SketchEntityId, b: SketchEntityId) -> Sketch3 {
    s.with_entity(Sketch3Entity::line(a, b, false)).0
}

fn horizontal_line_sketch(dim_mm: f64) -> Sketch3 {
    let (s, a) = add_point(&Sketch3::on_plane(plane()), 0.0, 0.0);
    let (s, b) = add_point(&s, 1.0, 0.0);
    let s = add_line(&s, a, b);
    let (s, _) = s.with_relation(Sketch3Relation::Fix(a));
    s.with_relation(Sketch3Relation::Distance {
        a,
        b,
        value: mm(dim_mm),
        reference: false,
    })
    .0
}

fn conflicting_sketch() -> Sketch3 {
    let (s, a) = add_point(&Sketch3::on_plane(plane()), 0.0, 0.0);
    let (s, b) = add_point(&s, 1.0, 0.0);
    let (s, _) = s.with_relation(Sketch3Relation::Fix(a));
    let (s, _) = s.with_relation(Sketch3Relation::Distance {
        a,
        b,
        value: mm(5.0),
        reference: false,
    });
    s.with_relation(Sketch3Relation::Distance {
        a,
        b,
        value: mm(10.0),
        reference: false,
    })
    .0
}

#[test]
fn evaluate_sketch_returns_solved_variant_on_success() {
    let sketch = horizontal_line_sketch(10.0);
    let output = evaluate_sketch(&sketch);
    let EvaluatedSketch::Solved(solved) = output else {
        panic!("expected Solved, got {output:?}");
    };
    let order = solved.entity_order();
    let b = order[1];
    let Sketch3Entity::Point(p) = solved.entities()[b] else {
        panic!("b is point");
    };
    let (x, y, _z) = p.at().coords_mm();
    assert!((x - 10.0).abs() < 1e-9, "x = {x}");
    assert!(y.abs() < 1e-9, "y = {y}");
}

#[test]
fn evaluate_sketch_returns_failed_variant_on_conflict() {
    let sketch = conflicting_sketch();
    let output = evaluate_sketch(&sketch);
    assert!(
        matches!(output, EvaluatedSketch::Failed(_)),
        "expected Failed, got {output:?}"
    );
}

#[test]
fn document_resolves_feature_id_back_to_sketch() {
    let mut doc = Document::new(document_id(1), "d".to_owned());
    let sid = sketch_id(7);
    doc.insert_sketch(sid, "S".to_owned(), horizontal_line_sketch(10.0));
    let feature = doc.feature_tree().iter().find_map(|(fid, node)| {
        matches!(node, FeatureNode::Sketch(id) if id == sid).then_some(fid)
    });
    let Some(fid) = feature else {
        panic!("sketch feature present");
    };
    let Some(sketch) = doc.sketch_of_feature(fid) else {
        panic!("sketch addressable through feature id");
    };
    let Some(direct) = doc.sketch3(sid) else {
        panic!("sketch in map");
    };
    assert_eq!(sketch, direct);
}

#[test]
fn removed_feature_id_is_never_reused() {
    let mut doc = Document::new(document_id(1), "d".to_owned());
    let sid_a = sketch_id(10);
    let sid_b = sketch_id(11);
    doc.insert_sketch(sid_a, "A".to_owned(), horizontal_line_sketch(10.0));
    let Some(fid_a) = doc.feature_tree().iter().find_map(|(fid, node)| {
        matches!(node, FeatureNode::Sketch(id) if id == sid_a).then_some(fid)
    }) else {
        panic!("A feature present");
    };

    let Some(sketch_a) = doc.sketch3(sid_a) else {
        panic!("A in map");
    };
    let EvaluatedSketch::Solved(solved_a) = evaluate_sketch(sketch_a) else {
        panic!("A solves");
    };

    doc.remove_sketch(sid_a);
    doc.insert_sketch(sid_b, "B".to_owned(), horizontal_line_sketch(20.0));
    let Some(fid_b) = doc.feature_tree().iter().find_map(|(fid, node)| {
        matches!(node, FeatureNode::Sketch(id) if id == sid_b).then_some(fid)
    }) else {
        panic!("B feature present");
    };
    assert_ne!(
        fid_a, fid_b,
        "a removed feature id is never handed to a later feature"
    );

    let Some(sketch_b) = doc.sketch3(sid_b) else {
        panic!("B in map");
    };
    let EvaluatedSketch::Solved(solved_b) = evaluate_sketch(sketch_b) else {
        panic!("B solves");
    };
    assert_ne!(
        solved_a, solved_b,
        "distinct feature ids cache distinct geometry"
    );
}

fn rectangle_wh(width_mm: f64, height_mm: f64) -> Sketch3 {
    let (s, c0) = add_point(&Sketch3::on_plane(plane()), 0.0, 0.0);
    let (s, c1) = add_point(&s, width_mm, 0.0);
    let (s, c2) = add_point(&s, width_mm, height_mm);
    let (s, c3) = add_point(&s, 0.0, height_mm);
    let s = add_line(&s, c0, c1);
    let s = add_line(&s, c1, c2);
    let s = add_line(&s, c2, c3);
    add_line(&s, c3, c0)
}

fn blind(depth_mm: f64) -> ExtrudeFeature {
    let Ok(depth) = PositiveLength::new(mm(depth_mm)) else {
        panic!("{depth_mm} mm is positive");
    };
    ExtrudeFeature::blind(SketchId::default(), depth)
}

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

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

#[test]
fn evaluate_extrude_reports_failed_sketch() {
    let extrude = evaluate_extrude(
        feature_id(9),
        &evaluate_sketch(&conflicting_sketch()),
        &blind(5.0),
    );
    assert!(extrude.result().is_err());
    assert!(extrude.solid().is_none());
    assert!(extrude.generation().is_none());
}

proptest::proptest! {
    #[test]
    fn extrude_re_evaluates_under_edited_width(width_mm in 2.0f64..40.0) {
        let extrude_feature = feature_id(42);
        let feature = blind(10.0);
        let base = rectangle_wh(10.0, 5.0);
        let baseline = evaluate_extrude(extrude_feature, &evaluate_sketch(&base), &feature);

        let edited = rectangle_wh(width_mm, 5.0);
        let widened = evaluate_extrude(extrude_feature, &evaluate_sketch(&edited), &feature);

        let (Some(before), Some(after)) = (baseline.solid(), widened.solid()) else {
            panic!("both widths extrude");
        };
        proptest::prop_assert_eq!(face_labels(before), face_labels(after));
        proptest::prop_assert_eq!(edge_labels(before), edge_labels(after));
        proptest::prop_assert!(after.validate(TOL).is_ok());
        let Some(bbox) = after.bounding_box() else {
            panic!("solid has a bounding box");
        };
        let (dx, _, _) = bbox.extent().coords_mm();
        proptest::prop_assert!((dx - width_mm).abs() < 1e-6, "x-extent {} tracks width {}", dx, width_mm);
    }
}
