//! Visual regression tests for PDF rendering.
//!
//! Generates PDF from fixture template+data, converts to PNG via pdftoppm,
//! and compares against reference snapshots.
//!
//! Set UPDATE_SNAPSHOTS=1 to update reference images.

#![cfg(not(target_arch = "wasm32"))]

mod common;

mod visual {
    use std::fs;
    use std::path::Path;
    use std::process::Command;

    use dreport_core::models::Template;
    use dreport_layout::pdf_render::render_pdf;
    use dreport_layout::{ResolvedContent, compute_layout};

    use crate::common::load_test_fonts;

    fn fixtures_dir() -> std::path::PathBuf {
        Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures")
    }

    fn snapshots_dir() -> std::path::PathBuf {
        Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/snapshots")
    }

    fn generate_test_pdf(template_name: &str, data_name: &str) -> Vec<u8> {
        let template_json = fs::read_to_string(fixtures_dir().join(template_name)).unwrap();
        let data_json = fs::read_to_string(fixtures_dir().join(data_name)).unwrap();

        let template: Template = serde_json::from_str(&template_json).unwrap();
        let data: serde_json::Value = serde_json::from_str(&data_json).unwrap();
        let fonts = load_test_fonts();

        let layout = compute_layout(&template, &data, &fonts).unwrap();
        render_pdf(&layout, &fonts).expect("PDF render failed")
    }

    fn pdf_to_png(pdf_bytes: &[u8], output_path: &Path) -> bool {
        // Write PDF to temp file
        let temp_pdf = output_path.with_extension("pdf");
        fs::write(&temp_pdf, pdf_bytes).unwrap();

        // pdftoppm appends .png to the output prefix, so strip the extension
        let output_prefix = output_path.with_extension("");

        let result = Command::new("pdftoppm")
            .args(["-png", "-r", "150", "-singlefile"])
            .arg(&temp_pdf)
            .arg(&output_prefix)
            .output();

        // Clean up temp PDF
        let _ = fs::remove_file(&temp_pdf);

        match result {
            Ok(output) => {
                if !output.status.success() {
                    eprintln!(
                        "pdftoppm failed: {}",
                        String::from_utf8_lossy(&output.stderr)
                    );
                    return false;
                }
                true
            }
            Err(_) => {
                eprintln!("pdftoppm not available - skipping visual test");
                false
            }
        }
    }

    fn compare_images(
        actual_path: &Path,
        reference_path: &Path,
        max_diff_ratio: f64,
    ) -> Result<f64, String> {
        let actual =
            image::open(actual_path).map_err(|e| format!("Failed to open actual: {}", e))?;
        let reference =
            image::open(reference_path).map_err(|e| format!("Failed to open reference: {}", e))?;

        let actual_rgba = actual.to_rgba8();
        let reference_rgba = reference.to_rgba8();

        if actual_rgba.dimensions() != reference_rgba.dimensions() {
            return Err(format!(
                "Dimension mismatch: actual {:?} vs reference {:?}",
                actual_rgba.dimensions(),
                reference_rgba.dimensions()
            ));
        }

        let total_pixels = (actual_rgba.width() * actual_rgba.height()) as f64;
        let mut diff_pixels = 0u64;

        for (a, r) in actual_rgba.pixels().zip(reference_rgba.pixels()) {
            // Allow per-channel tolerance of 2 for font rendering differences
            let channel_diff =
                a.0.iter()
                    .zip(r.0.iter())
                    .any(|(ac, rc)| (*ac as i32 - *rc as i32).unsigned_abs() > 2);
            if channel_diff {
                diff_pixels += 1;
            }
        }

        let diff_ratio = diff_pixels as f64 / total_pixels;

        if diff_ratio > max_diff_ratio {
            Err(format!(
                "Visual diff too large: {:.4}% pixels differ (threshold: {:.4}%)",
                diff_ratio * 100.0,
                max_diff_ratio * 100.0
            ))
        } else {
            Ok(diff_ratio)
        }
    }

    fn run_visual_test(template_file: &str, data_file: &str, test_name: &str) {
        let pdf_bytes = generate_test_pdf(template_file, data_file);
        assert!(!pdf_bytes.is_empty(), "PDF should not be empty");

        let snap_dir = snapshots_dir();
        fs::create_dir_all(&snap_dir).unwrap();

        let actual_png = snap_dir.join(format!("{}_actual.png", test_name));
        let reference_png = snap_dir.join(format!("{}_reference.png", test_name));

        if !pdf_to_png(&pdf_bytes, &actual_png) {
            eprintln!("Skipping visual comparison - pdftoppm not available");
            return;
        }

        let update_snapshots = std::env::var("UPDATE_SNAPSHOTS").is_ok();

        if !reference_png.exists() || update_snapshots {
            // First run or explicit update: save as reference
            fs::copy(&actual_png, &reference_png).unwrap();
            println!("Reference snapshot saved to {:?}", reference_png);
            // Clean up actual
            let _ = fs::remove_file(&actual_png);
            return;
        }

        // Compare
        match compare_images(&actual_png, &reference_png, 0.01) {
            Ok(diff) => {
                println!(
                    "Visual test [{}] passed: {:.4}% pixels differ",
                    test_name,
                    diff * 100.0
                );
                let _ = fs::remove_file(&actual_png);
            }
            Err(err) => {
                // Keep actual for debugging
                panic!(
                    "Visual regression [{}]: {}. Actual saved at {:?}",
                    test_name, err, actual_png
                );
            }
        }
    }

    /// SVG'yi standalone HTML'e sar — chart'ın HTML render'ını görmek icin
    fn generate_chart_svg_html(template_file: &str, data_file: &str, output_path: &Path) {
        let template_json = fs::read_to_string(fixtures_dir().join(template_file)).unwrap();
        let data_json = fs::read_to_string(fixtures_dir().join(data_file)).unwrap();

        let template: Template = serde_json::from_str(&template_json).unwrap();
        let data: serde_json::Value = serde_json::from_str(&data_json).unwrap();
        let fonts = load_test_fonts();

        let layout = compute_layout(&template, &data, &fonts).unwrap();

        let mut html = String::from(
            "<!DOCTYPE html><html><head><style>body{margin:20px;font-family:sans-serif;background:#f5f5f5}.chart-box{margin:10px 0;background:white;box-shadow:0 1px 3px rgba(0,0,0,.1)}</style></head><body><h2>Chart SVG Preview (HTML render)</h2>",
        );

        for page in &layout.pages {
            for el in &page.elements {
                if let Some(ResolvedContent::Chart { svg, .. }) = &el.content {
                    html.push_str(&format!(
                        "<div class='chart-box' style='width:{}mm;height:{}mm'>{}</div>",
                        el.width_mm, el.height_mm, svg
                    ));
                }
            }
        }

        html.push_str("</body></html>");
        fs::write(output_path, html).unwrap();
    }

    /// Cross-renderer reference PNG output directory
    fn cross_renderer_dir() -> std::path::PathBuf {
        Path::new(env!("CARGO_MANIFEST_DIR"))
            .parent()
            .unwrap()
            .join("frontend/tests/visual/cross-renderer-refs")
    }

    /// Generates PDF→PNG references for cross-renderer comparison with HTML render.
    /// Run explicitly: cargo test -p dreport-layout --test visual_test -- generate_cross_renderer --ignored
    #[test]
    #[ignore]
    fn generate_cross_renderer_refs() {
        let fixtures = [
            (
                "visual_test_template.json",
                "visual_test_data.json",
                "visual_test",
            ),
            (
                "chart_test_template.json",
                "chart_test_data.json",
                "chart_test",
            ),
            (
                "comprehensive_test_template.json",
                "comprehensive_test_data.json",
                "comprehensive_test",
            ),
        ];

        let out_dir = cross_renderer_dir();
        fs::create_dir_all(&out_dir).unwrap();

        for (template_file, data_file, name) in &fixtures {
            let pdf_bytes = generate_test_pdf(template_file, data_file);
            assert!(
                !pdf_bytes.is_empty(),
                "PDF should not be empty for {}",
                name
            );

            let png_path = out_dir.join(format!("{}.png", name));
            if !pdf_to_png(&pdf_bytes, &png_path) {
                panic!("pdftoppm failed for {} — install poppler-utils", name);
            }
            println!("Cross-renderer reference: {:?}", png_path);
        }
    }

    #[test]
    fn test_visual_snapshot_basic() {
        run_visual_test(
            "visual_test_template.json",
            "visual_test_data.json",
            "visual_test",
        );
    }

    #[test]
    fn test_visual_snapshot_charts() {
        let pdf_bytes = generate_test_pdf("chart_test_template.json", "chart_test_data.json");
        assert!(!pdf_bytes.is_empty(), "Chart PDF should not be empty");

        let snap_dir = snapshots_dir();
        fs::create_dir_all(&snap_dir).unwrap();

        // PDF ciktisini kaydet (inceleme icin)
        let pdf_path = snap_dir.join("chart_test.pdf");
        fs::write(&pdf_path, &pdf_bytes).unwrap();
        println!("Chart PDF saved to {:?}", pdf_path);

        // SVG HTML ciktisini kaydet (karsilastirma icin)
        let html_path = snap_dir.join("chart_test_svg.html");
        generate_chart_svg_html(
            "chart_test_template.json",
            "chart_test_data.json",
            &html_path,
        );
        println!("Chart SVG HTML saved to {:?}", html_path);

        // Visual regression test
        run_visual_test(
            "chart_test_template.json",
            "chart_test_data.json",
            "chart_test",
        );
    }
}
