use std::num::NonZeroUsize;

use bone_document::{Document, Sketch, UndoStack};
use bone_types::{DocumentId, Point3, SketchId, SketchPlaneBasis, Tolerance, UnitVec3};
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
}

#[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 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);
}
