use std::collections::BTreeSet;
use std::num::NonZeroUsize;

use bone_document::{
    Document, EvaluatedModel, FeatureEdge, RecomputeScope, Sketch3, Sketch3Entity, UndoStack,
    evaluate_extrude, evaluate_sketch,
};
use bone_kernel::{BrepSolid, ExtrudeFeature};
use bone_types::{
    DocumentId, ExtrudeId, FeatureId, Length, Point3, PositiveLength, RollbackMarker,
    SketchEntityId, SketchId, SketchPlaneBasis, Tolerance, UnitVec3, millimeter,
};
use slotmap::{Key, 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 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 cap(n: usize) -> NonZeroUsize {
    let Some(nz) = NonZeroUsize::new(n) else {
        panic!("capacity");
    };
    nz
}

fn base_doc() -> Document {
    Document::new(document_id(1), "d".to_owned())
}

fn with_sketch(mut doc: Document, sid: SketchId) -> Document {
    doc.insert_sketch(sid, format!("S{sid:?}"), Sketch3::on_plane(plane()));
    doc
}

fn extrude_id(idx: u32) -> ExtrudeId {
    ExtrudeId::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 rectangle() -> Sketch3 {
    rectangle_width(10.0)
}

fn rectangle_width(width_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, 5.0);
    let (s, c3) = add_point(&s, 0.0, 5.0);
    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 sample_solid(feature: FeatureId) -> BrepSolid {
    let evaluated = evaluate_sketch(&rectangle());
    let extruded = evaluate_extrude(feature, &evaluated, &blind_extrude(SketchId::null()));
    let Some(solid) = extruded.solid() else {
        panic!("rectangle extrudes to a solid");
    };
    solid.clone()
}

fn blind_extrude(sketch: SketchId) -> ExtrudeFeature {
    let Ok(depth) = PositiveLength::new(Length::new::<millimeter>(10.0)) else {
        panic!("10 mm is positive");
    };
    ExtrudeFeature::blind(sketch, depth)
}

#[test]
fn fresh_stack_has_no_history() {
    let stack = UndoStack::with_capacity(cap(5));
    assert!(!stack.can_undo());
    assert!(!stack.can_redo());
    assert_eq!(stack.past_len(), 0);
    assert_eq!(stack.future_len(), 0);
}

#[test]
fn undo_swaps_current_with_previous() {
    let a = base_doc();
    let mut live = with_sketch(a.clone(), sketch_id(1));
    let mut stack = UndoStack::with_capacity(cap(5));
    stack.record(a.clone());
    assert!(stack.undo(&mut live));
    assert_eq!(live, a);
    assert!(!stack.can_undo());
    assert!(stack.can_redo());
}

#[test]
fn undo_on_empty_leaves_current_unchanged() {
    let mut live = base_doc();
    let snapshot = live.clone();
    let mut stack = UndoStack::with_capacity(cap(5));
    assert!(!stack.undo(&mut live));
    assert_eq!(live, snapshot);
    assert_eq!(stack.past_len(), 0);
    assert_eq!(stack.future_len(), 0);
}

#[test]
fn redo_on_empty_leaves_current_unchanged() {
    let mut live = base_doc();
    let snapshot = live.clone();
    let mut stack = UndoStack::with_capacity(cap(5));
    assert!(!stack.redo(&mut live));
    assert_eq!(live, snapshot);
    assert_eq!(stack.past_len(), 0);
    assert_eq!(stack.future_len(), 0);
}

#[test]
fn redo_replays_undone_state() {
    let a = base_doc();
    let b = with_sketch(a.clone(), sketch_id(1));
    let mut live = b.clone();
    let mut stack = UndoStack::with_capacity(cap(5));
    stack.record(a.clone());
    assert!(stack.undo(&mut live));
    assert!(stack.redo(&mut live));
    assert_eq!(live, b);
    assert!(stack.can_undo());
    assert!(!stack.can_redo());
}

#[test]
fn recording_after_undo_clears_redo() {
    let a = base_doc();
    let b = with_sketch(a.clone(), sketch_id(1));
    let c = with_sketch(a.clone(), sketch_id(2));
    let mut live = b;
    let mut stack = UndoStack::with_capacity(cap(5));
    stack.record(a.clone());
    assert!(stack.undo(&mut live));
    assert_eq!(live, a);
    stack.record(live.clone());
    let _ = c;
    assert!(stack.can_undo());
    assert!(!stack.can_redo());
    assert_eq!(stack.future_len(), 0);
}

#[test]
fn capacity_drops_oldest_entries() {
    let mut stack = UndoStack::with_capacity(cap(2));
    let snapshots: Vec<Document> = (1..=4)
        .map(|i| with_sketch(base_doc(), sketch_id(i)))
        .collect();
    snapshots.iter().cloned().for_each(|doc| stack.record(doc));
    assert_eq!(stack.past_len(), 2);
    let mut live = with_sketch(base_doc(), sketch_id(99));
    assert!(stack.undo(&mut live));
    assert_eq!(live, snapshots[3]);
    assert!(stack.undo(&mut live));
    assert_eq!(live, snapshots[2]);
    assert!(!stack.can_undo());
}

#[test]
fn redo_never_exceeds_capacity() {
    let mut stack = UndoStack::with_capacity(cap(2));
    let mut live = base_doc();
    [1u32, 2, 3, 4].iter().for_each(|i| {
        stack.record(live.clone());
        live.insert_sketch(sketch_id(*i), format!("S{i}"), Sketch3::on_plane(plane()));
        assert!(stack.past_len() <= 2, "record stays within capacity");
    });
    std::iter::from_fn(|| {
        stack
            .undo(&mut live)
            .then(|| assert!(stack.past_len() <= 2 && stack.future_len() <= 2))
    })
    .for_each(drop);
    std::iter::from_fn(|| {
        stack.redo(&mut live).then(|| {
            assert!(
                stack.past_len() <= 2,
                "redo must not grow past beyond capacity"
            );
        })
    })
    .for_each(drop);
    assert!(!stack.can_redo());
}

#[test]
fn undo_redo_cycle_preserves_determinism() {
    let a = base_doc();
    let b = with_sketch(a.clone(), sketch_id(1));
    let c = with_sketch(b.clone(), sketch_id(2));
    let mut live = c.clone();
    let mut stack = UndoStack::with_capacity(cap(5));
    stack.record(a.clone());
    stack.record(b.clone());
    assert!(stack.undo(&mut live));
    assert_eq!(live, b);
    assert!(stack.undo(&mut live));
    assert_eq!(live, a);
    assert!(stack.redo(&mut live));
    assert_eq!(live, b);
    assert!(stack.redo(&mut live));
    assert_eq!(live, c);
}

#[test]
fn undo_extrude_restores_sketch_only_document() {
    let sid = sketch_id(1);
    let xid = extrude_id(1);

    let mut live = base_doc();
    live.insert_sketch(sid, "Sketch1".to_owned(), rectangle());
    let pre_extrude = live.clone();

    let mut stack = UndoStack::with_capacity(cap(5));
    stack.record(pre_extrude.clone());

    live.insert_extrude(xid, blind_extrude(sid));
    assert_ne!(
        live, pre_extrude,
        "inserting an extrude must change the document"
    );
    let Some(sketch_feature) = live.feature_tree().feature_of_sketch(sid) else {
        panic!("sketch feature present");
    };
    let Some(extrude_feature) = live.feature_tree().feature_of_extrude(xid) else {
        panic!("extrude feature present");
    };
    let wired = [FeatureEdge::SketchToExtrude {
        sketch: sketch_feature,
        extrude: extrude_feature,
    }];
    assert_eq!(
        live.feature_tree().edges(),
        wired,
        "inserting an extrude wires the sketch-to-extrude edge"
    );

    assert!(stack.undo(&mut live));
    assert_eq!(
        live, pre_extrude,
        "undo restores the sketch-only document exactly"
    );
    assert!(
        live.feature_tree().feature_of_extrude(xid).is_none(),
        "no extrude node survives the undo"
    );
    assert!(
        live.header().extrudes.is_empty(),
        "no extrude payload survives the undo"
    );
    assert!(
        live.feature_tree().edges().is_empty(),
        "undo drops the derived sketch-to-extrude edge"
    );

    assert!(stack.redo(&mut live));
    let Some(feature) = live.feature_tree().feature_of_extrude(xid) else {
        panic!("redo restores the extrude node");
    };
    assert_eq!(
        live.extrude_of_feature(feature),
        Some(&blind_extrude(sid)),
        "redo restores the extrude payload"
    );
    assert_eq!(
        live.feature_tree().edges(),
        wired,
        "redo restores the derived edge even though it is never serialized"
    );
}

#[test]
fn undo_and_redo_cross_a_recompute() {
    let sid = sketch_id(1);
    let xid = extrude_id(1);
    let mut live = base_doc();
    live.insert_sketch(sid, "Sketch1".to_owned(), rectangle_width(10.0));
    live.insert_extrude(xid, blind_extrude(sid));

    let Some(extrude_feature) = live.feature_tree().feature_of_extrude(xid) else {
        panic!("extrude feature present");
    };
    let Some(sketch_feature) = live.feature_tree().feature_of_sketch(sid) else {
        panic!("sketch feature present");
    };

    let mut model = EvaluatedModel::new();
    let active = BTreeSet::new();
    model.recompute(&live, &active, RollbackMarker::AtEnd, RecomputeScope::Full);
    let Some(narrow) = model.body(extrude_feature).map(BrepSolid::content_key) else {
        panic!("the base extrude builds a body");
    };

    let mut stack = UndoStack::with_capacity(cap(5));
    stack.record(live.clone());
    live.replace_sketch(sid, rectangle_width(14.0));
    model.recompute(
        &live,
        &active,
        RollbackMarker::AtEnd,
        RecomputeScope::Edited(sketch_feature),
    );
    let Some(wide) = model.body(extrude_feature).map(BrepSolid::content_key) else {
        panic!("the widened extrude builds a body");
    };
    assert_ne!(narrow, wide, "widening the profile must change the body");

    assert!(stack.undo(&mut live));
    model.recompute(&live, &active, RollbackMarker::AtEnd, RecomputeScope::Full);
    assert_eq!(
        model.body(extrude_feature).map(BrepSolid::content_key),
        Some(narrow),
        "undo restores the recipe and the rebuild reproduces the pre-edit body",
    );

    assert!(stack.redo(&mut live));
    model.recompute(&live, &active, RollbackMarker::AtEnd, RecomputeScope::Full);
    assert_eq!(
        model.body(extrude_feature).map(BrepSolid::content_key),
        Some(wide),
        "redo restores the edited recipe and the body widens again",
    );
}

#[test]
fn rollback_and_suppression_survive_undo_and_redo() {
    let sid = sketch_id(1);
    let xid = extrude_id(1);
    let mut live = base_doc();
    live.insert_sketch(sid, "Sketch1".to_owned(), rectangle());
    live.insert_extrude(xid, blind_extrude(sid));
    let Some(feature) = live.feature_tree().feature_of_extrude(xid) else {
        panic!("extrude feature present");
    };
    let before = live.clone();

    let mut stack = UndoStack::with_capacity(cap(5));
    stack.record(live.clone());
    live.roll_to_here(feature);
    live.suppress(feature);
    assert_eq!(live.rollback(), RollbackMarker::Above(feature));
    assert!(live.suppressed().contains(&feature));

    assert!(stack.undo(&mut live));
    assert_eq!(live, before, "undo restores the pre-history-edit document");
    assert_eq!(
        live.rollback(),
        RollbackMarker::AtEnd,
        "undo clears the rollback marker set by the undone step",
    );
    assert!(
        live.suppressed().is_empty(),
        "undo clears the suppression set by the undone step",
    );

    assert!(stack.redo(&mut live));
    assert_eq!(
        live.rollback(),
        RollbackMarker::Above(feature),
        "redo restores the rollback marker",
    );
    assert!(
        live.suppressed().contains(&feature),
        "redo restores the suppression",
    );
}

#[test]
fn cloning_a_document_shares_imported_body_storage() {
    let mut live = base_doc();
    let Ok((_, body)) = live.import_body(|feature| Ok::<_, ()>(sample_solid(feature))) else {
        panic!("import a body");
    };

    let snapshot = live.clone();
    let Some(live_solid) = live.imported_body(body) else {
        panic!("live body present");
    };
    let Some(snapshot_solid) = snapshot.imported_body(body) else {
        panic!("snapshot body present");
    };
    assert!(
        std::ptr::eq(live_solid, snapshot_solid),
        "a clone shares the imported body payload, the snapshot is not a deep brep copy",
    );
}

#[test]
fn an_undo_needs_a_full_recompute_to_restore_an_out_of_scope_body() {
    let sid_a = sketch_id(1);
    let xid_a = extrude_id(1);
    let sid_b = sketch_id(2);
    let xid_b = extrude_id(2);

    let mut live = base_doc();
    live.insert_sketch(sid_a, "A".to_owned(), rectangle_width(10.0));
    live.insert_extrude(xid_a, blind_extrude(sid_a));
    live.insert_sketch(sid_b, "B".to_owned(), rectangle_width(10.0));
    live.insert_extrude(xid_b, blind_extrude(sid_b));

    let Some(a_sketch_feature) = live.feature_tree().feature_of_sketch(sid_a) else {
        panic!("sketch a feature present");
    };
    let Some(b_sketch_feature) = live.feature_tree().feature_of_sketch(sid_b) else {
        panic!("sketch b feature present");
    };
    let Some(b_extrude_feature) = live.feature_tree().feature_of_extrude(xid_b) else {
        panic!("extrude b feature present");
    };

    let mut model = EvaluatedModel::new();
    let active = BTreeSet::new();
    model.recompute(&live, &active, RollbackMarker::AtEnd, RecomputeScope::Full);
    let Some(b_original) = model.body(b_extrude_feature).map(BrepSolid::content_key) else {
        panic!("b builds a body");
    };

    let mut stack = UndoStack::with_capacity(cap(5));
    stack.record(live.clone());
    live.replace_sketch(sid_b, rectangle_width(18.0));
    model.recompute(
        &live,
        &active,
        RollbackMarker::AtEnd,
        RecomputeScope::Edited(b_sketch_feature),
    );
    let Some(b_wide) = model.body(b_extrude_feature).map(BrepSolid::content_key) else {
        panic!("b rebuilds wide");
    };
    assert_ne!(b_original, b_wide, "widening b must change its body");

    assert!(stack.undo(&mut live));
    model.recompute(
        &live,
        &active,
        RollbackMarker::AtEnd,
        RecomputeScope::Edited(a_sketch_feature),
    );
    assert_eq!(
        model.body(b_extrude_feature).map(BrepSolid::content_key),
        Some(b_wide),
        "an edit-scoped recompute after undo cannot see b reverted outside the scope",
    );

    model.recompute(&live, &active, RollbackMarker::AtEnd, RecomputeScope::Full);
    assert_eq!(
        model.body(b_extrude_feature).map(BrepSolid::content_key),
        Some(b_original),
        "a full recompute after undo restores every reverted body, the contract the app must use",
    );
}

#[test]
fn cloning_a_document_shares_sketch_storage() {
    let sid = sketch_id(1);
    let mut live = base_doc();
    live.insert_sketch(sid, "Sketch1".to_owned(), rectangle_width(10.0));

    let snapshot = live.clone();
    let Some(live_sketch) = live.sketch3(sid) else {
        panic!("live sketch present");
    };
    let Some(snapshot_sketch) = snapshot.sketch3(sid) else {
        panic!("snapshot sketch present");
    };
    assert!(
        std::ptr::eq(live_sketch.entities(), snapshot_sketch.entities()),
        "a clone shares the sketch entity payload, the snapshot is not a deep copy",
    );

    live.replace_sketch(sid, rectangle_width(14.0));
    let Some(edited_sketch) = live.sketch3(sid) else {
        panic!("edited sketch present");
    };
    let Some(snapshot_after) = snapshot.sketch3(sid) else {
        panic!("snapshot survives the edit");
    };
    assert!(
        !std::ptr::eq(edited_sketch.entities(), snapshot_after.entities()),
        "editing the live document leaves the snapshot's payload untouched",
    );
}
