// Copyright 2026 The Cloud Hypervisor Authors. All rights reserved.
//
// Copyright (c) Meta Platforms, Inc. and affiliates.
//
// SPDX-License-Identifier: Apache-2.0 AND BSD-3-Clause

//! Shared test helpers for [`AsyncIo`] backends.
//!
//! Each helper takes a `&mut dyn AsyncIo` together with the [`File`] handle
//! that backs the I/O object, so the same logic exercises every backend with
//! only the constructor differing.

use std::fs::File;
use std::io::{Read, Seek, SeekFrom, Write};

use crate::async_io::{AsyncIo, AsyncIoError};

fn next_completion(async_io: &mut dyn AsyncIo) -> (u64, i32) {
    let completion = async_io.next_completed_request().expect("No completion");
    (completion.user_data, completion.result)
}

/// Tests punching a hole in the middle of a 4 MB file and verifying data
/// integrity around the hole.
pub fn test_punch_hole(async_io: &mut dyn AsyncIo, file: &mut File) {
    // Write 4MB of data
    let data = vec![0xAA; 4 * 1024 * 1024];
    file.write_all(&data).unwrap();
    file.sync_all().unwrap();

    // Punch hole in the middle (1MB at offset 1MB)
    let offset = 1024 * 1024;
    let length = 1024 * 1024;
    async_io.punch_hole(offset, length, 1).unwrap();

    // Check completion
    let (user_data, result) = next_completion(async_io);
    assert_eq!(user_data, 1);
    assert_eq!(result, 0);

    // Verify the hole reads as zeros
    file.seek(SeekFrom::Start(offset)).unwrap();
    let mut read_buf = vec![0; length as usize];
    file.read_exact(&mut read_buf).unwrap();
    assert!(
        read_buf.iter().all(|&b| b == 0),
        "Punched hole should read as zeros"
    );

    // Verify data before hole is intact
    file.seek(SeekFrom::Start(0)).unwrap();
    let mut read_buf = vec![0; 1024];
    file.read_exact(&mut read_buf).unwrap();
    assert!(
        read_buf.iter().all(|&b| b == 0xAA),
        "Data before hole should be intact"
    );

    // Verify data after hole is intact
    file.seek(SeekFrom::Start(offset + length)).unwrap();
    let mut read_buf = vec![0; 1024];
    file.read_exact(&mut read_buf).unwrap();
    assert!(
        read_buf.iter().all(|&b| b == 0xAA),
        "Data after hole should be intact"
    );
}

/// Tests writing zeroes to a 512 KB region inside a 4 MB file and verifying
/// surrounding data is preserved.  Gracefully skips when the filesystem does
/// not support `FALLOC_FL_ZERO_RANGE`.
pub fn test_write_zeroes(async_io: &mut dyn AsyncIo, file: &mut File) {
    // Write 4MB of data
    let data = vec![0xBB; 4 * 1024 * 1024];
    file.write_all(&data).unwrap();
    file.sync_all().unwrap();

    // Write zeros in the middle (512KB at offset 2MB)
    let offset = 2 * 1024 * 1024;
    let length = 512 * 1024;
    let write_zeroes_result = async_io.write_zeroes(offset, length, 2);

    // FALLOC_FL_ZERO_RANGE might not be supported on all filesystems (e.g., tmpfs)
    // If it fails with ENOTSUP, skip the test
    if let Err(AsyncIoError::WriteZeroes(ref e)) = write_zeroes_result
        && (e.raw_os_error() == Some(libc::EOPNOTSUPP) || e.raw_os_error() == Some(libc::ENOTSUP))
    {
        eprintln!("Skipping test_write_zeroes: filesystem doesn't support FALLOC_FL_ZERO_RANGE");
        return;
    }
    write_zeroes_result.unwrap();

    // Check completion
    let (user_data, result) = next_completion(async_io);
    assert_eq!(user_data, 2);
    assert_eq!(result, 0);

    // Verify the zeroed region reads as zeros
    file.seek(SeekFrom::Start(offset)).unwrap();
    let mut read_buf = vec![0; length as usize];
    file.read_exact(&mut read_buf).unwrap();
    assert!(
        read_buf.iter().all(|&b| b == 0),
        "Zeroed region should read as zeros"
    );

    // Verify data before zeroed region is intact
    file.seek(SeekFrom::Start(offset - 1024)).unwrap();
    let mut read_buf = vec![0; 1024];
    file.read_exact(&mut read_buf).unwrap();
    assert!(
        read_buf.iter().all(|&b| b == 0xBB),
        "Data before zeroed region should be intact"
    );

    // Verify data after zeroed region is intact
    file.seek(SeekFrom::Start(offset + length)).unwrap();
    let mut read_buf = vec![0; 1024];
    file.read_exact(&mut read_buf).unwrap();
    assert!(
        read_buf.iter().all(|&b| b == 0xBB),
        "Data after zeroed region should be intact"
    );
}

/// Tests punching multiple holes in an 8 MB file and verifying each hole
/// independently reads as zeroes.
pub fn test_punch_hole_multiple_operations(async_io: &mut dyn AsyncIo, file: &mut File) {
    // Write 8MB of data
    let data = vec![0xCC; 8 * 1024 * 1024];
    file.write_all(&data).unwrap();
    file.sync_all().unwrap();

    // Punch multiple holes
    async_io.punch_hole(1024 * 1024, 512 * 1024, 10).unwrap();
    async_io
        .punch_hole(3 * 1024 * 1024, 512 * 1024, 11)
        .unwrap();
    async_io
        .punch_hole(5 * 1024 * 1024, 512 * 1024, 12)
        .unwrap();

    // Check all completions
    let (user_data, result) = next_completion(async_io);
    assert_eq!(user_data, 10);
    assert_eq!(result, 0);

    let (user_data, result) = next_completion(async_io);
    assert_eq!(user_data, 11);
    assert_eq!(result, 0);

    let (user_data, result) = next_completion(async_io);
    assert_eq!(user_data, 12);
    assert_eq!(result, 0);

    // Verify all holes read as zeros
    file.seek(SeekFrom::Start(1024 * 1024)).unwrap();
    let mut read_buf = vec![0; 512 * 1024];
    file.read_exact(&mut read_buf).unwrap();
    assert!(read_buf.iter().all(|&b| b == 0));

    file.seek(SeekFrom::Start(3 * 1024 * 1024)).unwrap();
    file.read_exact(&mut read_buf).unwrap();
    assert!(read_buf.iter().all(|&b| b == 0));

    file.seek(SeekFrom::Start(5 * 1024 * 1024)).unwrap();
    file.read_exact(&mut read_buf).unwrap();
    assert!(read_buf.iter().all(|&b| b == 0));
}
