//! Integration tests for HEIC decoding

use heic::DecoderConfig;

fn heic_base_dir() -> String {
    std::env::var("HEIC_TEST_DIR").unwrap_or_else(|_| "/home/lilith/work/heic".into())
}

fn example_heic() -> String {
    format!("{}/libheif/examples/example.heic", heic_base_dir())
}

fn iphone_heic() -> String {
    format!(
        "{}/test-images/classic-car-iphone12pro.heic",
        heic_base_dir()
    )
}

#[test]
fn test_get_info() {
    let data = std::fs::read(example_heic()).expect("Failed to read test file");

    let info = heic::ImageInfo::from_bytes(&data).expect("Failed to get info");
    println!("Decoded info: {}x{}", info.width, info.height);

    // example.heic is 1280x854 (cropped from 1280x856 via conformance window)
    assert_eq!(info.width, 1280, "Expected width 1280");
    assert_eq!(info.height, 854, "Expected height 854 (cropped)");
}

#[test]
#[ignore] // Ignore until coefficient decoding is fully implemented
fn test_decode() {
    let data = std::fs::read(example_heic()).expect("Failed to read test file");
    let decoder = DecoderConfig::new();

    let image = decoder
        .decode(&data, heic::PixelLayout::Rgb8)
        .expect("Failed to decode");

    // example.heic is 1280x854 (cropped from 1280x856 via conformance window)
    assert_eq!(image.width, 1280, "Expected width 1280");
    assert_eq!(image.height, 854, "Expected height 854 (cropped)");

    // Check that we got RGB data (3 bytes per pixel)
    let expected_size = (image.width * image.height * 3) as usize;
    assert_eq!(image.data.len(), expected_size, "Unexpected data size");

    // Basic sanity check - data shouldn't be all zeros
    let non_zero = image.data.iter().any(|&b| b != 0);
    assert!(non_zero, "Image data is all zeros");

    // Print some stats
    let min_val = *image.data.iter().min().unwrap();
    let max_val = *image.data.iter().max().unwrap();
    let sum: u64 = image.data.iter().map(|&b| b as u64).sum();
    let avg = sum / image.data.len() as u64;
    println!("Pixel stats: min={}, max={}, avg={}", min_val, max_val, avg);

    // Print first 8x8 RGB block for comparison with reference
    println!("\n=== Our first 8x8 RGB block ===");
    for y in 0..8 {
        for x in 0..8 {
            let idx = (y * image.width as usize + x) * 3;
            let r = image.data[idx];
            let g = image.data[idx + 1];
            let b = image.data[idx + 2];
            print!("({:3},{:3},{:3}) ", r, g, b);
        }
        println!();
    }

    // Write to PPM for visual inspection
    let ppm_path = "/tmp/decoded_heic.ppm";
    let mut ppm = String::new();
    ppm.push_str(&format!("P6\n{} {}\n255\n", image.width, image.height));
    let mut file = std::fs::File::create(ppm_path).expect("Failed to create PPM");
    use std::io::Write;
    file.write_all(ppm.as_bytes())
        .expect("Failed to write PPM header");
    file.write_all(&image.data)
        .expect("Failed to write PPM data");
    println!("Wrote decoded image to: {}", ppm_path);
}

#[test]
#[ignore]
fn test_raw_yuv_values() {
    let data = std::fs::read(example_heic()).expect("Failed to read test file");
    let decoder = DecoderConfig::new();

    // Decode and examine raw YCbCr
    let frame = decoder.decode_to_frame(&data).expect("Failed to decode");

    // Analyze Y values in quadrants
    let mid_x = frame.width / 2;
    let mid_y = frame.height / 2;
    let mut quadrant_sums = [0u64; 4];
    let mut quadrant_counts = [0u64; 4];
    for y in 0..frame.height {
        for x in 0..frame.width {
            let idx = (y * frame.width + x) as usize;
            let val = frame.y_plane[idx] as u64;
            let q = if x < mid_x {
                if y < mid_y { 0 } else { 2 }
            } else if y < mid_y {
                1
            } else {
                3
            };
            quadrant_sums[q] += val;
            quadrant_counts[q] += 1;
        }
    }
    println!("\nY quadrant averages:");
    println!("  Top-Left: {}", quadrant_sums[0] / quadrant_counts[0]);
    println!("  Top-Right: {}", quadrant_sums[1] / quadrant_counts[1]);
    println!("  Bottom-Left: {}", quadrant_sums[2] / quadrant_counts[2]);
    println!("  Bottom-Right: {}", quadrant_sums[3] / quadrant_counts[3]);

    // Sample Y values along x=64 (CTU boundary) for different y
    println!("\nY values at x=64 for different rows:");
    for &y in &[0, 32, 64, 128, 256, 400] {
        if y < frame.height {
            let idx = (y * frame.width + 64) as usize;
            let vals: Vec<u16> = (0..8).map(|dx| frame.y_plane[idx + dx]).collect();
            println!("  y={:3}: {:?}", y, vals);
        }
    }

    // Sample Y values along y=64 for different x
    println!("\nY values at y=64 for different columns:");
    for &x in &[0, 64, 96, 120, 127, 128, 192, 256, 400, 640] {
        if x < frame.width {
            let idx = (64 * frame.width + x) as usize;
            let vals: Vec<u16> = (0..8).map(|dx| frame.y_plane[idx + dx]).collect();
            println!("  x={:3}: {:?}", x, vals);
        }
    }

    // Check the problematic row y=63 at different x
    println!("\nY values at y=63 (top border row for CTU row 1):");
    for &x in &[96, 104, 112, 120, 127] {
        if x < frame.width {
            let idx = (63 * frame.width + x) as usize;
            let vals: Vec<u16> = (0..8).map(|dx| frame.y_plane[idx + dx]).collect();
            println!("  x={:3}: {:?}", x, vals);
        }
    }

    println!(
        "Frame: {}x{}, bit_depth={}",
        frame.width, frame.height, frame.bit_depth
    );
    println!("Y plane: {} samples", frame.y_plane.len());
    println!("Cb plane: {} samples", frame.cb_plane.len());
    println!("Cr plane: {} samples", frame.cr_plane.len());

    // Y plane statistics with detailed histogram
    let y_min = frame.y_plane.iter().min().unwrap_or(&0);
    let y_max = frame.y_plane.iter().max().unwrap_or(&0);
    let y_sum: u64 = frame.y_plane.iter().map(|&v| v as u64).sum();
    let y_avg = y_sum / frame.y_plane.len().max(1) as u64;

    // Histogram in 32-value bins
    let mut hist = [0usize; 8];
    for &v in &frame.y_plane {
        hist[(v as usize / 32).min(7)] += 1;
    }
    println!("\nY plane: min={}, max={}, avg={}", y_min, y_max, y_avg);
    println!("  Histogram (32-bin):");
    for (i, count) in hist.iter().enumerate() {
        let pct = *count as f64 / frame.y_plane.len() as f64 * 100.0;
        println!(
            "    {:3}-{:3}: {:7} ({:5.1}%)",
            i * 32,
            (i + 1) * 32 - 1,
            count,
            pct
        );
    }

    // Cb plane statistics
    let cb_min = frame.cb_plane.iter().min().unwrap_or(&0);
    let cb_max = frame.cb_plane.iter().max().unwrap_or(&0);
    let cb_sum: u64 = frame.cb_plane.iter().map(|&v| v as u64).sum();
    let cb_avg = cb_sum / frame.cb_plane.len().max(1) as u64;
    println!("Cb plane: min={}, max={}, avg={}", cb_min, cb_max, cb_avg);

    // Cr plane statistics
    let cr_min = frame.cr_plane.iter().min().unwrap_or(&0);
    let cr_max = frame.cr_plane.iter().max().unwrap_or(&0);
    let cr_sum: u64 = frame.cr_plane.iter().map(|&v| v as u64).sum();
    let cr_avg = cr_sum / frame.cr_plane.len().max(1) as u64;
    println!("Cr plane: min={}, max={}, avg={}", cr_min, cr_max, cr_avg);

    println!("\n=== Raw YCbCr Values (first 8x8 Y block) ===");
    for y in 0..8 {
        let mut row = Vec::new();
        for x in 0..8 {
            let idx = (y * frame.width + x) as usize;
            row.push(format!("{:3}", frame.y_plane[idx]));
        }
        println!("  Y: {}", row.join(" "));
    }

    println!("\n=== Raw Cb/Cr (first 4x4 chroma block) ===");
    let c_stride = frame.width.div_ceil(2) as usize;
    for cy in 0..4 {
        let mut cb_row = Vec::new();
        let mut cr_row = Vec::new();
        for cx in 0..4 {
            let idx = cy * c_stride + cx;
            cb_row.push(format!("{:3}", frame.cb_plane[idx]));
            cr_row.push(format!("{:3}", frame.cr_plane[idx]));
        }
        println!("  Cb: {}  |  Cr: {}", cb_row.join(" "), cr_row.join(" "));
    }

    // Analyze chroma bias by CTU position
    // For 4:2:0, each CTU (64x64 luma) has 32x32 chroma
    println!("\n=== Chroma averages by CTU row ===");
    let c_height = frame.height.div_ceil(2) as usize;
    let ctu_chroma_size = 32usize;
    let num_ctu_rows = c_height.div_ceil(ctu_chroma_size);

    for ctu_row in 0..num_ctu_rows {
        let start_y = ctu_row * ctu_chroma_size;
        let end_y = ((ctu_row + 1) * ctu_chroma_size).min(c_height);

        let mut cb_sum = 0u64;
        let mut cr_sum = 0u64;
        let mut count = 0u64;

        for cy in start_y..end_y {
            for cx in 0..c_stride {
                let idx = cy * c_stride + cx;
                cb_sum += frame.cb_plane[idx] as u64;
                cr_sum += frame.cr_plane[idx] as u64;
                count += 1;
            }
        }

        if count > 0 {
            println!(
                "  CTU row {:2}: Cb avg={:3}, Cr avg={:3}",
                ctu_row,
                cb_sum / count,
                cr_sum / count
            );
        }
    }

    // Analyze chroma by CTU column for first row
    println!("\n=== Chroma averages by CTU column (first row) ===");
    let c_width = c_stride;
    let num_ctu_cols = c_width.div_ceil(ctu_chroma_size);

    for ctu_col in 0..num_ctu_cols {
        let start_x = ctu_col * ctu_chroma_size;
        let end_x = ((ctu_col + 1) * ctu_chroma_size).min(c_width);

        let mut cb_sum = 0u64;
        let mut cr_sum = 0u64;
        let mut count = 0u64;

        for cy in 0..ctu_chroma_size.min(c_height) {
            for cx in start_x..end_x {
                let idx = cy * c_stride + cx;
                cb_sum += frame.cb_plane[idx] as u64;
                cr_sum += frame.cr_plane[idx] as u64;
                count += 1;
            }
        }

        if count > 0 {
            println!(
                "  CTU col {:2}: Cb avg={:3}, Cr avg={:3}",
                ctu_col,
                cb_sum / count,
                cr_sum / count
            );
        }
    }

    // Analyze the boundary between CTU col 0 and 1
    println!("\n=== Chroma at CTU boundary (col 0 -> 1) ===");
    println!("Chroma values at x=28..35 (boundary at x=32), y=0..3:");
    for cy in 0..4 {
        let mut cb_row = Vec::new();
        let mut cr_row = Vec::new();
        for cx in 28..36 {
            let idx = cy * c_stride + cx;
            cb_row.push(format!("{:3}", frame.cb_plane[idx]));
            cr_row.push(format!("{:3}", frame.cr_plane[idx]));
        }
        println!(
            "  y={}: Cb=[{}]  Cr=[{}]",
            cy,
            cb_row.join(", "),
            cr_row.join(", ")
        );
    }
    println!("  (x=32 is start of CTU col 1)");

    // Also check what's at the very end of CTU col 0 (x=31)
    println!("\nChroma at right edge of CTU col 0 (x=31), all y:");
    let mut cb_at_31 = vec![];
    let mut cr_at_31 = vec![];
    for cy in 0..32.min(c_height) {
        let idx = cy * c_stride + 31;
        cb_at_31.push(frame.cb_plane[idx]);
        cr_at_31.push(frame.cr_plane[idx]);
    }
    let cb_avg: u64 = cb_at_31.iter().map(|&v| v as u64).sum::<u64>() / cb_at_31.len() as u64;
    let cr_avg: u64 = cr_at_31.iter().map(|&v| v as u64).sum::<u64>() / cr_at_31.len() as u64;
    println!("  x=31: Cb avg={}, Cr avg={}", cb_avg, cr_avg);
    println!("  first 8 Cb: {:?}", &cb_at_31[..8.min(cb_at_31.len())]);
    println!("  first 8 Cr: {:?}", &cr_at_31[..8.min(cr_at_31.len())]);
}

#[test]
fn test_extract_exif() {
    let data = std::fs::read(iphone_heic()).expect("read");
    let decoder = DecoderConfig::new();

    let exif = decoder.extract_exif(&data).expect("extract_exif");
    let exif = exif.expect("should have EXIF data");

    // EXIF TIFF data starts with byte-order mark: II (little-endian) or MM (big-endian)
    assert!(exif.len() > 8, "EXIF data too short: {} bytes", exif.len());
    assert!(
        &exif[..2] == b"II" || &exif[..2] == b"MM",
        "EXIF data should start with TIFF byte order mark, got {:02x?}",
        &exif[..2]
    );
    println!(
        "EXIF: {} bytes, byte order: {}",
        exif.len(),
        if exif[0] == b'I' {
            "little-endian"
        } else {
            "big-endian"
        }
    );
}

#[test]
fn test_extract_exif_none() {
    // example.heic has no EXIF
    let data = std::fs::read(example_heic()).expect("read");
    let decoder = DecoderConfig::new();
    let exif = decoder.extract_exif(&data).expect("extract_exif");
    assert!(exif.is_none(), "example.heic should not have EXIF");
}

#[test]
fn test_image_info_no_exif() {
    // example.heic: no EXIF, non-grid — probe should work
    let data = std::fs::read(example_heic()).expect("read");
    let info = heic::ImageInfo::from_bytes(&data).expect("probe");
    assert!(!info.has_exif, "example.heic should not have EXIF");
    assert!(!info.has_xmp, "example.heic should not have XMP");
    println!(
        "ImageInfo: {}x{}, has_exif={}, has_xmp={}",
        info.width, info.height, info.has_exif, info.has_xmp
    );
}

#[test]
fn test_image_info_grid_with_exif() {
    // iPhone HEIC: grid image with EXIF + XMP
    let data = std::fs::read(iphone_heic()).expect("read");
    let info = heic::ImageInfo::from_bytes(&data).expect("probe grid image");
    assert!(info.has_exif, "iPhone HEIC should have EXIF");
    assert!(info.has_xmp, "iPhone HEIC should have XMP");
    // Post-transform dimensions: iPhone photo is 4032x3024 raw but has irot 90°
    // rotation, so final decoded output is 3024x4032 (portrait)
    assert_eq!(info.width, 3024);
    assert_eq!(info.height, 4032);
    println!(
        "Grid ImageInfo: {}x{}, bit_depth={}, has_exif={}, has_xmp={}",
        info.width, info.height, info.bit_depth, info.has_exif, info.has_xmp
    );
}

#[test]
fn test_extract_xmp() {
    let data = std::fs::read(iphone_heic()).expect("read");
    let decoder = DecoderConfig::new();
    let xmp = decoder.extract_xmp(&data).expect("extract_xmp");
    // XMP may or may not be present; just ensure no crash
    if let Some(xmp_data) = xmp {
        // XMP should start with XML-like content
        let start =
            std::str::from_utf8(&xmp_data[..xmp_data.len().min(100)]).unwrap_or("(non-utf8)");
        println!("XMP: {} bytes, starts with: {:?}", xmp_data.len(), start);
    } else {
        println!("No XMP found (expected for some files)");
    }
}

#[test]
fn test_decode_thumbnail() {
    let data = std::fs::read(example_heic()).expect("read");
    let decoder = DecoderConfig::new();
    let thumb = decoder
        .decode_thumbnail(&data, heic::PixelLayout::Rgb8)
        .expect("decode_thumbnail");
    let thumb = thumb.expect("example.heic should have a thumbnail");
    // Thumbnail should be 320x212 per the container dump
    assert_eq!(thumb.width, 320);
    assert_eq!(thumb.height, 212);
    assert_eq!(thumb.data.len(), 320 * 212 * 3);
    println!(
        "Thumbnail: {}x{}, {} bytes",
        thumb.width,
        thumb.height,
        thumb.data.len()
    );
}

#[test]
fn test_image_info_has_thumbnail() {
    let data = std::fs::read(example_heic()).expect("read");
    let info = heic::ImageInfo::from_bytes(&data).expect("probe");
    assert!(
        info.has_thumbnail,
        "example.heic should report has_thumbnail=true"
    );
}

#[test]
fn test_decode_thumbnail_none() {
    // Nokia test files typically don't have thumbnails
    let nokia_path = format!("{}/test-images/nokia/C001.heic", heic_base_dir());
    if let Ok(data) = std::fs::read(nokia_path) {
        let decoder = DecoderConfig::new();
        let thumb = decoder
            .decode_thumbnail(&data, heic::PixelLayout::Rgb8)
            .expect("decode_thumbnail");
        if thumb.is_none() {
            println!("C001.heic has no thumbnail (expected)");
        } else {
            println!("C001.heic has a thumbnail (unexpected but OK)");
        }
    }
}

#[test]
fn test_image_info_matches_decoded_dimensions() {
    // Regression: ImageInfo returned raw (pre-transform) dimensions while decoder
    // applied irot/imir/clap transforms, causing dimension mismatch panics.
    // iPhone photos have irot 90°, making raw 4032x3024 → decoded 3024x4032.
    let data = std::fs::read(iphone_heic()).expect("read");
    let info = heic::ImageInfo::from_bytes(&data).expect("probe");

    let decoder = DecoderConfig::new();
    let decoded = decoder
        .decode(&data, heic::PixelLayout::Rgb8)
        .expect("decode");

    assert_eq!(
        info.width, decoded.width,
        "ImageInfo width {} != decoded width {}",
        info.width, decoded.width
    );
    assert_eq!(
        info.height, decoded.height,
        "ImageInfo height {} != decoded height {}",
        info.height, decoded.height
    );
}
