#![allow(
    dead_code,
    reason = "dead_code checks each test binary alone and misses helpers used only by sibling binaries"
)]

use std::path::PathBuf;

use bone_render::{
    AdapterPolicy, OffscreenContext, PixelDiff, PixelDiffThreshold, RenderError, SnapshotFrame,
    ViewportExtent, ViewportPx, decode_png, encode_png,
};

const ADAPTER_RETRIES: u32 = 3;
const ADAPTER_RETRY_BACKOFF_MS: u64 = 100;
const GOLDEN_DIFF_TOLERANCE: f64 = 16.0 / 255.0;
const GOLDEN_MISMATCH_BUDGET_DIVISOR: u64 = 1000;

#[must_use]
pub fn extent_square(side: u32) -> ViewportExtent {
    ViewportExtent::square(ViewportPx::new(side))
}

#[must_use]
pub fn make_context(extent: ViewportExtent) -> OffscreenContext {
    let mut last_err: Option<RenderError> = None;
    for attempt in 0..ADAPTER_RETRIES {
        match pollster::block_on(OffscreenContext::new(extent, AdapterPolicy::Platform)) {
            Ok(ctx) => return ctx,
            Err(e @ (RenderError::NoAdapter(_) | RenderError::Device(_))) => {
                last_err = Some(e);
                let backoff_ms = ADAPTER_RETRY_BACKOFF_MS * u64::from(attempt + 1);
                std::thread::sleep(std::time::Duration::from_millis(backoff_ms));
            }
            Err(e) => panic!("offscreen context init failed: {e}"),
        }
    }
    match last_err {
        Some(RenderError::NoAdapter(e)) => panic!(
            "no wgpu adapter available after {ADAPTER_RETRIES} attempts, configure lavapipe or an iGPU: {e}"
        ),
        Some(RenderError::Device(e)) => {
            panic!("wgpu device request failed after {ADAPTER_RETRIES} attempts: {e}")
        }
        Some(other) => {
            panic!("offscreen context init failed after {ADAPTER_RETRIES} attempts: {other}")
        }
        None => unreachable!("retry loop always populates last_err on failure"),
    }
}

pub fn check_golden(frame: &SnapshotFrame, golden_rel: &str, update_env: &str) {
    let path = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join(golden_rel);
    if std::env::var(update_env).is_ok_and(|value| !matches!(value.as_str(), "" | "0")) {
        let Ok(bytes) = encode_png(frame) else {
            panic!("encode_png failed");
        };
        if let Some(parent) = path.parent() {
            assert!(
                std::fs::create_dir_all(parent).is_ok(),
                "failed to create goldens dir"
            );
        }
        assert!(
            std::fs::write(&path, &bytes).is_ok(),
            "failed to write golden {}",
            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, frame.extent(), "golden extent drift");
    let threshold = PixelDiffThreshold::new(GOLDEN_DIFF_TOLERANCE);
    let Ok(report) = PixelDiff::compare(frame, &golden_rgba, threshold) else {
        panic!("PixelDiff rejected inputs");
    };
    let budget = frame.extent().pixel_count() / GOLDEN_MISMATCH_BUDGET_DIVISOR;
    assert!(
        report.within_budget(budget),
        "{golden_rel} drifted from golden: {} of {} pixels over threshold (budget {budget}), worst {:?}, backend {}",
        report.over_threshold(),
        frame.extent().pixel_count(),
        report.worst(),
        frame.backend(),
    );
}
