use std::path::PathBuf;

use bone_document::{EditOutcome, Sketch, SketchEdit, SketchEntity};
use bone_render::{
    Camera2, PixelDiff, PixelDiffThreshold, SketchRenderer, SketchScene, Style, decode_png,
    encode_png,
};
use bone_types::{Point2, Point3, SketchEntityId, SketchPlaneBasis, Tolerance, UnitVec3};

mod common;

use common::{extent_square as extent, make_context};

const GOLDEN: &str = "tests/goldens/rectangle_256.png";
const UPDATE_ENV: &str = "BONE_UPDATE_RECTANGLE_GOLDEN";
const DIFF_TOLERANCE: f64 = 16.0 / 255.0;

fn golden_path() -> PathBuf {
    PathBuf::from(env!("CARGO_MANIFEST_DIR")).join(GOLDEN)
}

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 is orthogonal");
    };
    basis
}

fn add_point(s: Sketch, x: f64, y: f64) -> (Sketch, SketchEntityId) {
    let Ok((next, EditOutcome::Entity(id))) = s.apply(SketchEdit::AddEntity(SketchEntity::point(
        Point2::from_mm(x, y),
    ))) else {
        panic!("add point");
    };
    (next, id)
}

fn add_line(s: Sketch, a: SketchEntityId, b: SketchEntityId) -> Sketch {
    let Ok((next, _)) = s.apply(SketchEdit::AddEntity(SketchEntity::line(a, b, false))) else {
        panic!("add line");
    };
    next
}

fn rectangle_scene() -> SketchScene {
    let s = Sketch::new(plane());
    let (s, p0) = add_point(s, -8.0, -5.0);
    let (s, p1) = add_point(s, 8.0, -5.0);
    let (s, p2) = add_point(s, 8.0, 5.0);
    let (s, p3) = add_point(s, -8.0, 5.0);
    let s = add_line(s, p0, p1);
    let s = add_line(s, p1, p2);
    let s = add_line(s, p2, p3);
    let s = add_line(s, p3, p0);
    let Ok(scene) = SketchScene::extract(&s) else {
        panic!("scene extract");
    };
    scene
}

#[test]
fn rectangle_sketch_matches_golden() {
    let size = extent(256);
    let ctx = make_context(size);
    let mut renderer = SketchRenderer::new(ctx.gpu(), ctx.color_format());
    let scene = rectangle_scene();
    let camera = Camera2::new(size);
    let style = Style::default();

    let Ok(frame) = renderer.render(&ctx, &scene, camera, &style) else {
        panic!("SketchRenderer::render failed");
    };

    let path = golden_path();

    if std::env::var(UPDATE_ENV).is_ok() {
        let Ok(bytes) = encode_png(&frame) else {
            panic!("encode_png failed");
        };
        if let Some(parent) = path.parent()
            && let Err(e) = std::fs::create_dir_all(parent)
        {
            panic!("create goldens dir {}: {e}", parent.display());
        }
        if let Err(e) = std::fs::write(&path, &bytes) {
            panic!("write golden {}: {e}", path.display());
        }
        return;
    }

    let Ok(bytes) = std::fs::read(&path) else {
        panic!(
            "golden missing at {}: rerun with {UPDATE_ENV}=1 to generate",
            path.display()
        );
    };
    let Ok((golden_extent, golden_rgba)) = decode_png(&bytes) else {
        panic!("failed to decode golden PNG");
    };
    assert_eq!(golden_extent, size, "golden extent drift");
    let threshold = PixelDiffThreshold::new(DIFF_TOLERANCE);
    let Ok(report) = PixelDiff::compare(&frame, &golden_rgba, threshold) else {
        panic!("PixelDiff rejected inputs");
    };
    assert!(
        report.is_clean(),
        "rectangle render drifted from golden: {} mismatches, worst {:?}, backend {}",
        report.over_threshold(),
        report.worst(),
        frame.backend(),
    );
}
