use std::num::NonZeroUsize;

use bone_document::{Document, EditOutcome, FeatureEdge, Sketch, SketchEdit, SketchEntity, UndoStack};
use bone_kernel::{
    ExtrudeDirection, ExtrudeEndCondition, ExtrudeFeature, ExtrudeSense, MergeResult,
};
use bone_types::{
    DocumentId, ExtrudeId, Length, Point2, Point3, PositiveLength, SketchEntityId, 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 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:?}"), Sketch::new(plane()));
    doc
}

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

fn entity_of(outcome: EditOutcome) -> SketchEntityId {
    let EditOutcome::Entity(id) = outcome else {
        panic!("entity outcome");
    };
    id
}

fn rectangle() -> Sketch {
    let Ok((with_points, outcomes)) = Sketch::new(plane()).apply_all(vec![
        SketchEdit::AddEntity(SketchEntity::point(Point2::from_mm(0.0, 0.0))),
        SketchEdit::AddEntity(SketchEntity::point(Point2::from_mm(10.0, 0.0))),
        SketchEdit::AddEntity(SketchEntity::point(Point2::from_mm(10.0, 5.0))),
        SketchEdit::AddEntity(SketchEntity::point(Point2::from_mm(0.0, 5.0))),
    ]) else {
        panic!("rectangle corners");
    };
    let [c0, c1, c2, c3] = [0, 1, 2, 3].map(|i| entity_of(outcomes[i]));
    let Ok((closed, _)) = with_points.apply_all(vec![
        SketchEdit::AddEntity(SketchEntity::line(c0, c1, false)),
        SketchEdit::AddEntity(SketchEntity::line(c1, c2, false)),
        SketchEdit::AddEntity(SketchEntity::line(c2, c3, false)),
        SketchEdit::AddEntity(SketchEntity::line(c3, c0, false)),
    ]) else {
        panic!("rectangle edges");
    };
    closed
}

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

#[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}"), Sketch::new(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"
    );
}
