use std::path::PathBuf;

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

mod common;

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

const GOLDEN: &str = "tests/goldens/arc_circle_zoom100_256.png";
const UPDATE_ENV: &str = "BONE_UPDATE_ARC_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_circle(s: Sketch, center: SketchEntityId, radius_mm: f64) -> Sketch {
    let Ok((next, _)) = s.apply(SketchEdit::AddEntity(SketchEntity::circle(
        center,
        Length::new::<millimeter>(radius_mm),
        false,
    ))) else {
        panic!("add circle");
    };
    next
}

fn add_arc(
    s: Sketch,
    center: SketchEntityId,
    start: SketchEntityId,
    end: SketchEntityId,
) -> Sketch {
    let Ok((next, _)) = s.apply(SketchEdit::AddEntity(SketchEntity::arc(
        center, start, end, false,
    ))) else {
        panic!("add arc");
    };
    next
}

fn arc_circle_scene() -> SketchScene {
    let s = Sketch::new(plane());
    let (s, circle_c) = add_point(s, -0.7, 0.0);
    let s = add_circle(s, circle_c, 0.5);

    let (s, arc_c) = add_point(s, 0.7, 0.0);
    let (s, arc_start) = add_point(s, 1.2, 0.0);
    let (s, arc_end) = add_point(s, 0.7, 0.5);
    let s = add_arc(s, arc_c, arc_start, arc_end);

    let Ok(scene) = SketchScene::extract(&s) else {
        panic!("scene extract");
    };
    scene
}

#[test]
fn arc_circle_at_100x_zoom_matches_golden() {
    let size = extent(256);
    let ctx = make_context(size);
    let mut renderer = SketchRenderer::new(ctx.gpu(), ctx.color_format());
    let scene = arc_circle_scene();
    let camera = Camera2::new(size).with_zoom(PixelsPerMm::new(100.0));
    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(),
        "arc render drifted from golden: {} mismatches, worst {:?}, backend {}",
        report.over_threshold(),
        report.worst(),
        frame.backend(),
    );
}
