use std::process::Command;

use bone_document::{
    DimensionKind, Document, DocumentFolder, EditOutcome, Sketch, SketchDimension, SketchEdit,
    SketchEntity, load, save,
};
use bone_types::{
    DocumentId, Length, Point2, Point3, SketchId, SketchPlaneBasis, Tolerance, UnitVec3, millimeter,
};
use slotmap::KeyData;
use tempfile::{TempDir, tempdir};

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 ok_dir() -> TempDir {
    let Ok(dir) = tempdir() else {
        panic!("tempdir");
    };
    dir
}

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 jj_available() -> bool {
    let Ok(out) = Command::new("jj").arg("--version").output() else {
        return false;
    };
    out.status.success()
}

fn run(cmd: &mut Command) -> String {
    let Ok(out) = cmd.output() else {
        panic!("{cmd:?}");
    };
    assert!(
        out.status.success(),
        "{cmd:?} failed: {}\n{}",
        String::from_utf8_lossy(&out.stdout),
        String::from_utf8_lossy(&out.stderr),
    );
    String::from_utf8_lossy(&out.stdout).into_owned()
}

fn rectangle() -> Sketch {
    let script = [
        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))),
    ];
    let Ok((s, _)) = Sketch::new(plane()).apply_all(script) else {
        panic!("rectangle");
    };
    s
}

fn assert_save(doc: &Document, folder: &DocumentFolder) {
    let Ok(()) = save(doc, folder) else {
        panic!("save");
    };
}

fn assert_load(folder: &DocumentFolder) -> Document {
    let Ok(doc) = load(folder) else {
        panic!("load");
    };
    doc
}

#[test]
fn jj_diff_on_dimension_edit_is_text_only() {
    if !jj_available() {
        eprintln!("skip: jj not on PATH");
        return;
    }

    let dir = ok_dir();
    let folder = DocumentFolder::new(dir.path().join("demo.bone"));

    let mut doc = Document::new(document_id(1), "demo".to_owned());
    let sid = sketch_id(1);
    let rect = rectangle();
    let Some(&a) = rect.entity_order().first() else {
        panic!("rect has points");
    };
    let Some(&b) = rect.entity_order().get(1) else {
        panic!("rect has points");
    };
    let Ok((rect, EditOutcome::Dimension(dim_id))) =
        rect.apply(SketchEdit::AddDimension(SketchDimension::Linear {
            a,
            b,
            value: mm(10.0),
            kind: DimensionKind::Driving,
        }))
    else {
        panic!("dim");
    };
    doc.insert_sketch(sid, "Sketch1".to_owned(), rect.clone());
    assert_save(&doc, &folder);

    run(Command::new("jj")
        .arg("git")
        .arg("init")
        .arg("--colocate")
        .current_dir(folder.path()));
    run(Command::new("jj")
        .arg("config")
        .arg("set")
        .arg("--repo")
        .arg("user.name")
        .arg("test")
        .current_dir(folder.path()));
    run(Command::new("jj")
        .arg("config")
        .arg("set")
        .arg("--repo")
        .arg("user.email")
        .arg("test@example.com")
        .current_dir(folder.path()));
    run(Command::new("jj")
        .arg("describe")
        .arg("--message")
        .arg("initial")
        .current_dir(folder.path()));
    run(Command::new("jj").arg("new").current_dir(folder.path()));

    let reopened = assert_load(&folder);
    let Some(sketch) = reopened.sketch(sid) else {
        panic!("sketch missing");
    };
    let Ok((updated_sketch, _)) = sketch.clone().apply(SketchEdit::UpdateDimensionValue {
        id: dim_id,
        value: bone_document::DimensionValue::Length(mm(12.5)),
    }) else {
        panic!("update dim");
    };
    let mut next_doc = reopened.clone();
    next_doc.replace_sketch(sid, updated_sketch);
    assert_save(&next_doc, &folder);

    let diff = run(Command::new("jj")
        .arg("diff")
        .arg("--git")
        .current_dir(folder.path()));
    assert!(
        diff.contains("sketches/"),
        "expected sketch file in diff:\n{diff}"
    );
    assert!(
        diff.contains('+') && diff.contains('-'),
        "expected text diff markers:\n{diff}"
    );
    assert!(
        !diff.contains("Binary files"),
        "expected no binary diff:\n{diff}"
    );
}
