use std::process::Command;

use bone_document::{
    BlobKind, Document, DocumentFolder, Sketch3, Sketch3Entity, Sketch3Relation, evaluate_extrude,
    evaluate_sketch, load, save, write_solid,
};
use bone_kernel::{BrepSolid, ExtrudeFeature};
use bone_types::{
    DocumentId, ExtrudeId, Length, Point3, PositiveLength, SketchEntityId, 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 add_point(s: &Sketch3, x: f64, y: f64) -> (Sketch3, SketchEntityId) {
    s.with_entity(Sketch3Entity::point(Point3::from_mm(x, y, 0.0)))
}

fn dimensioned_rectangle(width_mm: f64) -> Sketch3 {
    let (s, a) = add_point(&Sketch3::on_plane(plane()), 0.0, 0.0);
    let (s, b) = add_point(&s, 10.0, 0.0);
    let (s, _) = add_point(&s, 10.0, 5.0);
    let (s, _) = add_point(&s, 0.0, 5.0);
    s.with_relation(Sketch3Relation::Distance {
        a,
        b,
        value: mm(width_mm),
        reference: false,
    })
    .0
}

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);
    doc.insert_sketch(sid, "Sketch1".to_owned(), dimensioned_rectangle(10.0));
    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 mut next_doc = assert_load(&folder);
    next_doc.replace_sketch(sid, dimensioned_rectangle(12.5));
    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("document.ron"),
        "a dimension edit must leave the recipe stable, only the sketch file changes:\n{diff}"
    );
    assert!(
        diff.contains('+') && diff.contains('-'),
        "expected text diff markers:\n{diff}"
    );
    assert!(
        !diff.contains("Binary files"),
        "expected no binary diff:\n{diff}"
    );
}

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

fn closed_rectangle() -> Sketch3 {
    let (s, p0) = add_point(&Sketch3::on_plane(plane()), 0.0, 0.0);
    let (s, p1) = add_point(&s, 10.0, 0.0);
    let (s, p2) = add_point(&s, 10.0, 5.0);
    let (s, p3) = add_point(&s, 0.0, 5.0);
    [(p0, p1), (p1, p2), (p2, p3), (p3, p0)]
        .into_iter()
        .fold(s, |s, (a, b)| {
            s.with_entity(Sketch3Entity::line(a, b, false)).0
        })
}

fn blind_extrude(sketch: SketchId, depth_mm: f64) -> ExtrudeFeature {
    let Ok(depth) = PositiveLength::new(mm(depth_mm)) else {
        panic!("positive depth");
    };
    ExtrudeFeature::blind(sketch, depth)
}

fn evaluated_solid(doc: &Document, sketch: SketchId, extrude: ExtrudeId) -> BrepSolid {
    let Some(sketch_value) = doc.sketch3(sketch) else {
        panic!("sketch present");
    };
    let Some(feature_id) = doc.feature_tree().feature_of_extrude(extrude) else {
        panic!("extrude node present");
    };
    let Some(feature) = doc.extrude_of_feature(feature_id) else {
        panic!("extrude feature present");
    };
    let evaluated = evaluate_sketch(sketch_value);
    let extruded = evaluate_extrude(feature_id, &evaluated, feature);
    let Some(solid) = extruded.solid() else {
        panic!("rectangle extrudes to a solid");
    };
    solid.clone()
}

#[test]
fn jj_diff_on_extrude_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("slab.bone"));
    let sid = sketch_id(1);
    let eid = extrude_id(1);

    let mut doc = Document::new(document_id(1), "slab".to_owned());
    doc.insert_sketch(sid, "Sketch1".to_owned(), closed_rectangle());
    doc.insert_extrude(eid, blind_extrude(sid, 10.0));
    assert_save(&doc, &folder);
    let Ok(_baseline_blob) = write_solid(&folder, &evaluated_solid(&doc, sid, eid)) else {
        panic!("cache the depth-10 solid");
    };

    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 document_before = std::fs::read_to_string(folder.document_file()).unwrap_or_default();

    let mut next = assert_load(&folder);
    next.insert_extrude(eid, blind_extrude(sid, 14.0));
    assert_save(&next, &folder);
    let Ok(edited_blob) = write_solid(&folder, &evaluated_solid(&next, sid, eid)) else {
        panic!("cache the depth-14 solid");
    };

    let document_after = std::fs::read_to_string(folder.document_file()).unwrap_or_default();
    assert_eq!(
        document_before, document_after,
        "an extrude-depth edit must not churn document.ron; geometry is not in the recipe"
    );
    assert!(
        folder.blob_path(edited_blob, BlobKind::BREP).exists(),
        "the widened slab content-addresses a fresh brep blob"
    );

    let diff = run(Command::new("jj")
        .arg("diff")
        .arg("--git")
        .current_dir(folder.path()));
    assert!(
        diff.contains("extrudes/"),
        "expected the extrude file in the diff:\n{diff}"
    );
    assert!(
        diff.contains(".brep"),
        "expected a new geometry blob in the 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}"
    );
}
