use std::path::PathBuf;

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

mod common;

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

const GOLDEN: &str = "tests/goldens/dimensions_256.png";
const UPDATE_ENV: &str = "BONE_UPDATE_DIMENSIONS_GOLDEN";
const DIFF_TOLERANCE: f64 = 20.0 / 255.0;

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, SketchEntityId) {
    let Ok((next, EditOutcome::Entity(id))) =
        s.apply(SketchEdit::AddEntity(SketchEntity::line(a, b, false)))
    else {
        panic!("add line");
    };
    (next, id)
}

fn add_circle(s: Sketch, center: SketchEntityId, radius_mm: f64) -> (Sketch, SketchEntityId) {
    let Ok((next, EditOutcome::Entity(id))) = s.apply(SketchEdit::AddEntity(SketchEntity::circle(
        center,
        Length::new::<millimeter>(radius_mm),
        false,
    ))) else {
        panic!("add circle");
    };
    (next, id)
}

fn add_dimension(s: Sketch, dim: SketchDimension) -> Sketch {
    let Ok((next, _)) = s.apply(SketchEdit::AddDimension(dim)) else {
        panic!("add dimension {dim:?}");
    };
    next
}

fn four_kind_scene() -> (Sketch, SketchScene) {
    let s = Sketch::new(plane());

    let (s, lp0) = add_point(s, -6.0, 3.0);
    let (s, lp1) = add_point(s, 0.0, 3.0);
    let s = add_dimension(
        s,
        SketchDimension::Linear {
            a: lp0,
            b: lp1,
            value: Length::new::<millimeter>(6.0),
            kind: DimensionKind::Driving,
        },
    );

    let (s, rc) = add_point(s, 4.5, 3.0);
    let (s, rcirc) = add_circle(s, rc, 1.5);
    let s = add_dimension(
        s,
        SketchDimension::Radius {
            target: rcirc,
            value: Length::new::<millimeter>(1.5),
            kind: DimensionKind::Driving,
        },
    );

    let (s, dc) = add_point(s, -3.0, -2.5);
    let (s, dcirc) = add_circle(s, dc, 2.0);
    let s = add_dimension(
        s,
        SketchDimension::Diameter {
            target: dcirc,
            value: Length::new::<millimeter>(4.0),
            kind: DimensionKind::Driving,
        },
    );

    let (s, ap0) = add_point(s, 2.5, -4.0);
    let (s, ap1) = add_point(s, 5.5, -4.0);
    let (s, aline_a) = add_line(s, ap0, ap1);
    let (s, ap2) = add_point(s, 4.0, -1.0);
    let (s, aline_b) = add_line(s, ap0, ap2);
    let s = add_dimension(
        s,
        SketchDimension::Angular {
            a: aline_a,
            b: aline_b,
            value: Angle::new::<degree>(45.0),
            kind: DimensionKind::Driving,
        },
    );

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

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

#[test]
fn four_dimension_kinds_match_golden() {
    let size = extent(256);
    let ctx = make_context(size);
    let (_sketch, scene) = four_kind_scene();
    assert_eq!(scene.dimensions().len(), 4);
    let mut renderer = SketchRenderer::new(ctx.gpu(), ctx.color_format());
    let camera = Camera2::new(ctx.extent()).with_zoom(PixelsPerMm::new(18.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);
    let threshold = PixelDiffThreshold::new(DIFF_TOLERANCE);
    let Ok(report) = PixelDiff::compare(&frame, &golden_rgba, threshold) else {
        panic!("PixelDiff rejected inputs");
    };
    assert!(
        report.is_clean(),
        "dimension render drifted: {} mismatches, worst {:?}, backend {}",
        report.over_threshold(),
        report.worst(),
        frame.backend(),
    );
}

#[test]
fn cache_reuses_across_repeated_renders_same_text() {
    let size = extent(128);
    let ctx = make_context(size);
    let (_sketch, scene) = four_kind_scene();
    let mut renderer = SketchRenderer::new(ctx.gpu(), ctx.color_format());
    renderer.prepare(&scene, &Style::default());
    let first_len = renderer.text_cache_len();
    renderer.prepare(&scene, &Style::default());
    let second_len = renderer.text_cache_len();
    assert_eq!(
        first_len, second_len,
        "cache size should be stable across repeated prepares on identical scene",
    );
}

#[test]
fn cache_refreshes_on_value_change() {
    use bone_document::SketchEdit;
    let size = extent(128);
    let ctx = make_context(size);
    let (sketch, scene) = four_kind_scene();
    let mut renderer = SketchRenderer::new(ctx.gpu(), ctx.color_format());
    renderer.prepare(&scene, &Style::default());
    let initial_len = renderer.text_cache_len();

    let Some(&target_id) = sketch.dimension_order().first() else {
        panic!("scene has at least one dimension");
    };
    let Ok((updated_sketch, _)) = sketch.apply(SketchEdit::UpdateDimensionValue {
        id: target_id,
        value: bone_document::DimensionValue::Length(Length::new::<millimeter>(999.0)),
    }) else {
        panic!("update dimension value");
    };
    let Ok(updated_scene) = SketchScene::extract(&updated_sketch) else {
        panic!("scene extract after update");
    };

    let Some(first) = updated_scene.dimensions().first() else {
        panic!("updated scene has dimensions");
    };
    let new_text = first.text().to_owned();
    assert!(
        new_text.contains("999"),
        "expected refreshed text to reflect updated value, got {new_text:?}",
    );

    renderer.prepare(&updated_scene, &Style::default());
    let refreshed_len = renderer.text_cache_len();
    assert_eq!(initial_len, refreshed_len);
}

#[test]
fn cache_refreshes_on_font_size_change() {
    let size = extent(128);
    let ctx = make_context(size);
    let (_sketch, scene) = four_kind_scene();
    let mut renderer = SketchRenderer::new(ctx.gpu(), ctx.color_format());
    let small = Style::default();
    renderer.prepare(&scene, &small);
    let baseline_len = renderer.text_cache_len();
    let big = Style::default().with_text(small.text().with_font_size_px(28.0));
    renderer.prepare(&scene, &big);
    assert_eq!(
        baseline_len,
        renderer.text_cache_len(),
        "font-size change must keep cache size equal to dimension count",
    );
}
