use bone_document::{
    DimensionKind, Document, EditOutcome, EvaluatedSketch, FeatureCache, FeatureNode, Sketch,
    SketchDimension, SketchEdit, SketchEntity, SketchRelation, evaluate_sketch,
};
use bone_types::{
    DocumentId, FeatureId, Length, Point2, Point3, SketchId, SketchPlaneBasis, Tolerance, UnitVec3,
    millimeter,
};
use slotmap::KeyData;

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 horizontal_line_sketch(dim_mm: f64) -> Sketch {
    let Ok((s, EditOutcome::Entity(a))) = Sketch::new(plane()).apply(SketchEdit::AddEntity(
        SketchEntity::point(Point2::from_mm(0.0, 0.0)),
    )) else {
        panic!("a");
    };
    let Ok((s, EditOutcome::Entity(b))) = s.apply(SketchEdit::AddEntity(SketchEntity::point(
        Point2::from_mm(1.0, 0.0),
    ))) else {
        panic!("b");
    };
    let Ok((s, EditOutcome::Entity(line))) =
        s.apply(SketchEdit::AddEntity(SketchEntity::line(a, b, false)))
    else {
        panic!("line");
    };
    let Ok((s, _)) = s.apply(SketchEdit::AddRelation(SketchRelation::Fix(a))) else {
        panic!("fix");
    };
    let Ok((s, _)) = s.apply(SketchEdit::AddRelation(SketchRelation::Horizontal(line))) else {
        panic!("horizontal");
    };
    let Ok((s, _)) = s.apply(SketchEdit::AddDimension(SketchDimension::Linear {
        a,
        b,
        value: mm(dim_mm),
        kind: DimensionKind::Driving,
    })) else {
        panic!("dim");
    };
    s
}

fn conflicting_sketch() -> Sketch {
    let Ok((s, EditOutcome::Entity(a))) = Sketch::new(plane()).apply(SketchEdit::AddEntity(
        SketchEntity::point(Point2::from_mm(0.0, 0.0)),
    )) else {
        panic!("a");
    };
    let Ok((s, EditOutcome::Entity(b))) = s.apply(SketchEdit::AddEntity(SketchEntity::point(
        Point2::from_mm(1.0, 0.0),
    ))) else {
        panic!("b");
    };
    let Ok((s, _)) = s.apply(SketchEdit::AddRelation(SketchRelation::Fix(a))) else {
        panic!("fix");
    };
    let Ok((s, _)) = s.apply(SketchEdit::AddDimension(SketchDimension::Linear {
        a,
        b,
        value: mm(5.0),
        kind: DimensionKind::Driving,
    })) else {
        panic!("dim1");
    };
    let Ok((s, _)) = s.apply(SketchEdit::AddDimension(SketchDimension::Linear {
        a,
        b,
        value: mm(10.0),
        kind: DimensionKind::Driving,
    })) else {
        panic!("dim2");
    };
    s
}

#[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 SketchEntity::Point(p) = solved.entities()[b] else {
        panic!("b is point");
    };
    let (x, y) = 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 feature_cache_reuses_solution_for_identical_input() {
    let sketch = horizontal_line_sketch(10.0);
    let mut cache = FeatureCache::new();
    let fid = feature_id(7);
    let first = cache.evaluate(fid, &sketch).clone();
    let second = cache.evaluate(fid, &sketch).clone();
    assert_eq!(first, second);
    assert_eq!(cache.len(), 1);
    assert!(cache.lookup(fid).is_some());
}

#[test]
fn feature_cache_refreshes_on_input_change() {
    let mut cache = FeatureCache::new();
    let fid = feature_id(7);
    let first = cache.evaluate(fid, &horizontal_line_sketch(10.0)).clone();
    let second = cache.evaluate(fid, &horizontal_line_sketch(20.0)).clone();
    let EvaluatedSketch::Solved(first_solved) = first else {
        panic!("first Solved");
    };
    let EvaluatedSketch::Solved(second_solved) = second else {
        panic!("second Solved");
    };
    assert_ne!(first_solved, second_solved);
    assert_eq!(cache.len(), 1);
}

#[test]
fn feature_cache_keeps_entries_per_feature() {
    let mut cache = FeatureCache::new();
    let a = feature_id(1);
    let b = feature_id(2);
    cache.evaluate(a, &horizontal_line_sketch(10.0));
    cache.evaluate(b, &horizontal_line_sketch(20.0));
    assert_eq!(cache.len(), 2);
    assert!(cache.lookup(a).is_some());
    assert!(cache.lookup(b).is_some());
}

#[test]
fn feature_cache_invalidate_drops_entry() {
    let mut cache = FeatureCache::new();
    let fid = feature_id(7);
    cache.evaluate(fid, &horizontal_line_sketch(10.0));
    assert!(cache.invalidate(fid));
    assert!(cache.lookup(fid).is_none());
    assert!(!cache.invalidate(fid));
}

#[test]
fn feature_cache_retain_drops_absent_features() {
    let mut cache = FeatureCache::new();
    let keep = feature_id(1);
    let drop = feature_id(2);
    cache.evaluate(keep, &horizontal_line_sketch(10.0));
    cache.evaluate(drop, &horizontal_line_sketch(20.0));
    cache.retain([keep]);
    assert!(cache.lookup(keep).is_some());
    assert!(cache.lookup(drop).is_none());
    assert_eq!(cache.len(), 1);
}

#[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.sketch(sid) else {
        panic!("sketch in map");
    };
    assert_eq!(sketch, direct);
}

#[test]
fn feature_cache_refreshes_when_feature_id_is_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 mut cache = FeatureCache::new();
    let Some(sketch_a) = doc.sketch(sid_a) else {
        panic!("A in map");
    };
    let EvaluatedSketch::Solved(solved_a) = cache.evaluate(fid_a, sketch_a).clone() 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_eq!(
        fid_a, fid_b,
        "precondition: FeatureTree reuses the id of a removed feature"
    );

    let Some(sketch_b) = doc.sketch(sid_b) else {
        panic!("B in map");
    };
    let EvaluatedSketch::Solved(solved_b) = cache.evaluate(fid_b, sketch_b).clone() else {
        panic!("B solves");
    };
    assert_ne!(solved_a, solved_b, "cache must refresh on reused FeatureId");
}
