//! End-to-end integration tests for the Linux `io_uring` file-IO
//! backend (PLAN_v2.md §7).
//!
//! These tests exist to exercise the [`peel::io_backend::UringBackend`]
//! through the [`peel::download::SparseFile`] surface that workers see
//! in production. They are gated on `cfg(target_os = "linux")` so
//! non-Linux runners skip them at build time, mirroring the
//! "linux-uring feature flag" wording in `PLAN_v2.md` §7. Each test
//! also probes the kernel at runtime and short-circuits with a
//! diagnostic line if `io_uring` is unavailable (kernel < 5.6,
//! container without uring, seccomp policy).

#![cfg(target_os = "linux")]

use std::path::PathBuf;
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::Arc;
use std::thread;

use peel::download::SparseFile;
use peel::io_backend::{select_backend, IoBackend, IoBackendChoice, UringBackend, MIN_RING_DEPTH};
use peel::types::ByteOffset;

static UNIQ: AtomicU64 = AtomicU64::new(0);

fn unique_temp_path(label: &str) -> PathBuf {
    let pid = std::process::id();
    let n = UNIQ.fetch_add(1, Ordering::Relaxed);
    let nanos = std::time::SystemTime::now()
        .duration_since(std::time::UNIX_EPOCH)
        .map(|d| d.as_nanos())
        .unwrap_or(0);
    std::env::temp_dir().join(format!("peel_uring_it_{label}_{pid}_{nanos}_{n}.bin"))
}

struct CleanupOnDrop(PathBuf);
impl Drop for CleanupOnDrop {
    fn drop(&mut self) {
        let _ = std::fs::remove_file(&self.0);
    }
}

fn try_backend() -> Option<Arc<dyn IoBackend>> {
    UringBackend::try_new(MIN_RING_DEPTH)
        .ok()
        .map(|b| Arc::new(b) as Arc<dyn IoBackend>)
}

#[test]
fn sparse_file_round_trips_through_uring_backend() {
    let Some(backend) = try_backend() else {
        eprintln!("skipping: io_uring unavailable on this kernel");
        return;
    };
    let path = unique_temp_path("round_trip");
    let _cleanup = CleanupOnDrop(path.clone());

    let sparse = SparseFile::open_or_create_with_backend(&path, 64 * 1024, backend).expect("open");
    assert_eq!(sparse.backend_name(), "uring");

    let payload: Vec<u8> = (0u8..=255).cycle().take(8192).collect();
    sparse
        .pwrite_at(ByteOffset::new(1024), &payload)
        .expect("pwrite");
    sparse.sync_all().expect("sync");

    let mut got = vec![0u8; payload.len()];
    sparse
        .read_exact_at(ByteOffset::new(1024), &mut got)
        .expect("read_exact");
    assert_eq!(got, payload);
}

#[test]
fn parallel_workers_write_disjoint_regions_through_uring() {
    let Some(backend) = try_backend() else {
        return;
    };
    let path = unique_temp_path("parallel");
    let _cleanup = CleanupOnDrop(path.clone());

    const WORKERS: u64 = 16;
    const BYTES_PER_WORKER: u64 = 64 * 1024;
    const TOTAL: u64 = WORKERS * BYTES_PER_WORKER;

    let sparse =
        Arc::new(SparseFile::open_or_create_with_backend(&path, TOTAL, backend).expect("open"));

    thread::scope(|scope| {
        for w in 0..WORKERS {
            let s = Arc::clone(&sparse);
            scope.spawn(move || {
                let payload: Vec<u8> = (0..BYTES_PER_WORKER)
                    .map(|i| ((w * BYTES_PER_WORKER + i) & 0xff) as u8)
                    .collect();
                let off = ByteOffset::new(w * BYTES_PER_WORKER);
                s.pwrite_at(off, &payload).expect("worker pwrite");
            });
        }
    });
    sparse.sync_all().expect("sync");

    // Verify every region.
    for w in 0..WORKERS {
        let mut got = vec![0u8; BYTES_PER_WORKER as usize];
        sparse
            .read_exact_at(ByteOffset::new(w * BYTES_PER_WORKER), &mut got)
            .expect("read");
        let expected: Vec<u8> = (0..BYTES_PER_WORKER)
            .map(|i| ((w * BYTES_PER_WORKER + i) & 0xff) as u8)
            .collect();
        assert_eq!(got, expected, "region {w} mismatch");
    }
}

#[test]
fn select_backend_auto_yields_uring_when_kernel_supports() {
    // We cannot assert deterministically that auto picks uring on
    // every Linux runner — kernels in containers may lack support.
    // The test instead verifies that auto returns *some* working
    // backend whose name is one of the two we expect.
    let (backend, _label) = select_backend(IoBackendChoice::Auto, 4).expect("auto picks something");
    let name = backend.name();
    assert!(
        matches!(name, "blocking" | "uring"),
        "unexpected name {name}"
    );
}

#[test]
fn select_backend_uring_round_trips_through_sparse_file_when_available() {
    let (backend, _label) = match select_backend(IoBackendChoice::Uring, 4) {
        Ok(pair) => pair,
        Err(_) => {
            eprintln!("skipping: --io-backend uring unavailable on this kernel");
            return;
        }
    };
    let path = unique_temp_path("forced_uring");
    let _cleanup = CleanupOnDrop(path.clone());

    let sparse = SparseFile::open_or_create_with_backend(&path, 4096, backend).expect("open");
    let payload = b"the quick brown fox jumps over the lazy dog".to_vec();
    sparse
        .pwrite_at(ByteOffset::ZERO, &payload)
        .expect("pwrite");

    let mut got = vec![0u8; payload.len()];
    sparse
        .read_exact_at(ByteOffset::ZERO, &mut got)
        .expect("read");
    assert_eq!(got, payload);
}

#[test]
fn dropping_uring_backend_unblocks_quickly() {
    // Build, exercise once, drop. Drop tears down the IO thread and
    // joins it; this test guards against a regression where the join
    // would hang because the channel did not close. We bound the
    // overall test time at 5 s.
    let Some(backend) = try_backend() else {
        return;
    };
    let path = unique_temp_path("drop_join");
    let _cleanup = CleanupOnDrop(path.clone());

    {
        let sparse =
            SparseFile::open_or_create_with_backend(&path, 64, Arc::clone(&backend)).expect("open");
        sparse
            .pwrite_at(ByteOffset::ZERO, b"hello uring")
            .expect("pwrite");
    }
    drop(backend);
}

// Note: the previous `http_client_uses_uring_backend_for_socket_io`
// test exercised a now-removed plumbing path (`Client::with_backend`
// routing HTTP socket IO through the io_uring backend). With the
// migration to hyper, network IO no longer flows through `IoBackend`;
// the backend's role is confined to filesystem IO via SparseFile.
// The remaining tests in this file already cover that.
