//! Integration tests for mtp-rs.
//!
//! Requires a real MTP device (Android phone, Kindle, Garmin watch, etc.)
//! connected via USB. MTP only supports one operation at a time, so use
//! `--test-threads=1`.
//!
//! ```sh
//! # Read-only tests (safe):
//! cargo test --test integration readonly -- --ignored --nocapture --test-threads=1
//!
//! # Destructive tests (writes to device):
//! cargo test --test integration destructive -- --ignored --nocapture --test-threads=1
//!
//! # All tests (skip slow ones):
//! cargo test --test integration -- --ignored --nocapture --test-threads=1 --skip slow
//! ```
//!
//! ## Picking a writable folder
//!
//! Destructive tests need a folder they can write into. By default they walk a
//! priority list of common folder names (`Download`, `Downloads`, `Music`,
//! `Documents`, `documents`, `Pictures`, `Audiobooks`, `Podcasts`) and use the
//! first one that exists in the storage root. If your device exposes a
//! differently-named folder, override the list with `MTP_TEST_FOLDER`:
//!
//! ```sh
//! MTP_TEST_FOLDER=Internal cargo test --test integration destructive -- --ignored ...
//! ```
//!
//! When no match is found, destructive tests skip with a clear log line.

use mtp_rs::mtp::Storage;
use mtp_rs::ptp::ObjectHandle;
use serial_test::serial;
use std::time::Instant;

/// Global test start time - initialized lazily on first use
static TEST_START: std::sync::OnceLock<Instant> = std::sync::OnceLock::new();

/// Get the elapsed time since tests started, formatted as [HH:MM:SS.mmm]
fn elapsed_timestamp() -> String {
    let start = TEST_START.get_or_init(Instant::now);
    let elapsed = start.elapsed();
    let total_secs = elapsed.as_secs();
    let hours = total_secs / 3600;
    let minutes = (total_secs % 3600) / 60;
    let seconds = total_secs % 60;
    let millis = elapsed.subsec_millis();
    format!("[{:02}:{:02}:{:02}.{:03}]", hours, minutes, seconds, millis)
}

/// Timestamped logging macro
macro_rules! tlog {
    ($($arg:tt)*) => {{
        // Initialize start time on first log
        let _ = TEST_START.get_or_init(Instant::now);
        println!("{} {}", $crate::elapsed_timestamp(), format_args!($($arg)*));
    }};
}

/// Handle device errors gracefully - skip test on hardware issues, panic on others.
macro_rules! try_device {
    ($expr:expr, $context:expr) => {
        match $expr {
            Ok(v) => v,
            Err(e) => {
                if is_hardware_error(&e) {
                    tlog!("SKIPPING: {} - {:?}", $context, e);
                    print_device_help(&e);
                    return;
                } else {
                    panic!("{} failed: {:?}", $context, e);
                }
            }
        }
    };
}

fn is_hardware_error(e: &mtp_rs::Error) -> bool {
    use mtp_rs::Error;
    matches!(e, Error::Timeout | Error::NoDevice | Error::Disconnected) || e.is_exclusive_access()
}

fn print_device_help(e: &mtp_rs::Error) {
    use mtp_rs::Error;
    match e {
        Error::Timeout => {
            tlog!("  Check: phone unlocked? USB authorized? Cable connected?");
        }
        Error::NoDevice => {
            tlog!("  Check: phone connected? Set to MTP/File Transfer mode?");
        }
        Error::Disconnected => {
            tlog!("  Check: cable secure? Phone didn't sleep?");
        }
        _ if e.is_exclusive_access() => {
            tlog!("  Close other apps (file managers, Photos, Android File Transfer)");
        }
        _ => {}
    }
}

/// Search common Android folders for a file in the given size range.
/// Returns (handle, size, filename) if found.
async fn find_file_in_common_folders(
    storage: &Storage,
    min_size: u64,
    max_size: u64,
) -> Option<(ObjectHandle, u64, String)> {
    let root_objects = storage.list_objects(None).await.ok()?;

    let common_folders = [
        "Download",
        "Downloads",
        "DCIM",
        "Pictures",
        "Music",
        "Documents",
    ];

    for folder_name in &common_folders {
        let Some(folder) = root_objects
            .iter()
            .find(|o| o.is_folder() && o.filename == *folder_name)
        else {
            continue;
        };

        let objects = storage
            .list_objects(Some(folder.handle))
            .await
            .unwrap_or_default();

        // For DCIM, also check Camera subfolder
        let objects_to_check = if *folder_name == "DCIM" {
            if let Some(camera) = objects
                .iter()
                .find(|o| o.is_folder() && o.filename == "Camera")
            {
                storage
                    .list_objects(Some(camera.handle))
                    .await
                    .unwrap_or_default()
            } else {
                objects
            }
        } else {
            objects
        };

        if let Some(f) = objects_to_check
            .iter()
            .find(|o| o.is_file() && o.size > min_size && o.size < max_size)
        {
            return Some((f.handle, f.size, f.filename.clone()));
        }
    }
    None
}

/// Find a suitable file, falling back to recursive listing if needed.
async fn find_suitable_file(
    storage: &Storage,
    min_size: u64,
    max_size: u64,
) -> Option<(ObjectHandle, u64, String)> {
    // Try common folders first (fast)
    if let Some(result) = find_file_in_common_folders(storage, min_size, max_size).await {
        return Some(result);
    }

    // Fall back to recursive listing (slow)
    tlog!("No file in common folders, trying recursive listing...");
    let objects = storage.list_objects_recursive(None).await.ok()?;
    objects
        .iter()
        .find(|o| o.is_file() && o.size > min_size && o.size < max_size)
        .map(|f| (f.handle, f.size, f.filename.clone()))
}

/// Find a writable folder at storage root for destructive tests.
///
/// If `MTP_TEST_FOLDER` is set, look for that folder name only.
/// Otherwise walk a priority list covering Android, Kindle, and Garmin
/// devices and return the first match. Returns `(handle, name)`.
async fn find_writable_folder(storage: &Storage) -> Option<(ObjectHandle, String)> {
    let root_objects = storage.list_objects(None).await.ok()?;

    let override_name = std::env::var("MTP_TEST_FOLDER").ok();
    let candidates: Vec<String> = if let Some(name) = &override_name {
        vec![name.clone()]
    } else {
        [
            "Download",   // Android (most common)
            "Downloads",  // Android (alt)
            "Music",      // Android, Garmin music-capable watches
            "Documents",  // Android (capitalized)
            "documents",  // Kindle (lowercase)
            "Pictures",   // Android
            "Audiobooks", // Garmin
            "Podcasts",   // Garmin
        ]
        .iter()
        .map(|s| (*s).to_string())
        .collect()
    };

    for name in &candidates {
        if let Some(folder) = root_objects
            .iter()
            .find(|o| o.is_folder() && o.filename == *name)
        {
            return Some((folder.handle, folder.filename.clone()));
        }
    }

    if let Some(name) = &override_name {
        tlog!(
            "MTP_TEST_FOLDER='{}' is set, but no folder by that name at storage root",
            name
        );
    }
    None
}

/// Read-only tests that don't modify the device.
mod readonly {
    use super::*;
    use mtp_rs::mtp::MtpDevice;
    use mtp_rs::ptp::PtpDevice;
    use std::time::Duration;

    #[test]
    #[serial]
    fn test_list_devices() {
        let devices = MtpDevice::list_devices().expect("USB subsystem error");
        tlog!("Found {} MTP device(s)", devices.len());
        for dev in &devices {
            tlog!(
                "  {} {} ({:04x}:{:04x}) location={:08x}",
                dev.manufacturer.as_deref().unwrap_or("?"),
                dev.product.as_deref().unwrap_or("?"),
                dev.vendor_id,
                dev.product_id,
                dev.location_id
            );
        }
    }

    #[tokio::test]
    #[ignore]
    #[serial]
    async fn test_device_connection() {
        let device = try_device!(MtpDevice::open_first().await, "open device");
        let info = device.device_info();
        tlog!(
            "Connected: {} {} ({})",
            info.manufacturer,
            info.model,
            info.serial_number
        );
        assert!(!info.manufacturer.is_empty());
        assert!(!info.model.is_empty());
        device.close().await.expect("close failed");
    }

    #[tokio::test]
    #[ignore]
    #[serial]
    async fn test_list_storages() {
        let device = try_device!(MtpDevice::open_first().await, "open device");
        let storages = try_device!(device.storages().await, "get storages");
        tlog!("Found {} storage(s)", storages.len());
        assert!(!storages.is_empty());

        for storage in &storages {
            let info = storage.info();
            tlog!(
                "  {} - {:.2} GB free / {:.2} GB total",
                info.description,
                info.free_space_bytes as f64 / 1e9,
                info.max_capacity as f64 / 1e9
            );
        }
    }

    #[tokio::test]
    #[ignore]
    #[serial]
    async fn test_list_root_folder() {
        let device = try_device!(MtpDevice::open_first().await, "open device");
        let storages = try_device!(device.storages().await, "get storages");
        let storage = &storages[0];

        let objects = try_device!(storage.list_objects(None).await, "list root");
        tlog!("Root contains {} objects", objects.len());

        for obj in objects.iter().take(20) {
            let kind = if obj.is_folder() { "DIR " } else { "FILE" };
            let size = if obj.is_folder() {
                "-".to_string()
            } else {
                format!("{}", obj.size)
            };
            tlog!("  {} {:>12} {}", kind, size, obj.filename);
        }
        if objects.len() > 20 {
            tlog!("  ... and {} more", objects.len() - 20);
        }

        assert!(objects.iter().any(|o| o.is_folder()));
    }

    /// SLOW: Lists ALL objects recursively. Set MTP_RUN_SLOW_TESTS=1 to run.
    #[tokio::test]
    #[ignore]
    #[serial]
    async fn slow_test_list_recursive() {
        if std::env::var("MTP_RUN_SLOW_TESTS").is_err() {
            tlog!("SKIPPING slow_test_list_recursive (set MTP_RUN_SLOW_TESTS=1 to run)");
            return;
        }

        let device = try_device!(MtpDevice::open_first().await, "open device");
        let storages = try_device!(device.storages().await, "get storages");
        let storage = &storages[0];

        tlog!("Starting recursive listing (may take several minutes)...");
        let objects = try_device!(storage.list_objects_recursive(None).await, "recursive list");

        let folders = objects.iter().filter(|o| o.is_folder()).count();
        let files = objects.iter().filter(|o| o.is_file()).count();
        tlog!(
            "Total: {} objects ({} folders, {} files)",
            objects.len(),
            folders,
            files
        );
    }

    #[tokio::test]
    #[ignore]
    #[serial]
    async fn test_download_with_progress() {
        let device = try_device!(MtpDevice::open_first().await, "open device");
        let storages = try_device!(device.storages().await, "get storages");
        let storage = &storages[0];

        tlog!("Searching for file (100KB-10MB)...");
        let Some((handle, file_size, file_name)) =
            find_suitable_file(storage, 100_000, 10_000_000).await
        else {
            tlog!("No suitable file found, skipping");
            return;
        };
        tlog!("Downloading {} ({} bytes)", file_name, file_size);

        let mut download = try_device!(storage.download_stream(handle).await, "start download");
        let total = download.size();
        let mut last_percent = 0u64;

        while let Some(result) = download.next_chunk().await {
            result.expect("download error");
            let percent = download.bytes_received() * 100 / total;
            if percent >= last_percent + 10 {
                tlog!("  {}%", percent);
                last_percent = percent;
            }
        }
        tlog!("Download complete");
    }

    #[tokio::test]
    #[ignore]
    #[serial]
    async fn test_custom_timeout() {
        let device = try_device!(
            MtpDevice::builder()
                .timeout(Duration::from_secs(60))
                .open_first()
                .await,
            "open with timeout"
        );
        tlog!("Opened with 60s timeout: {}", device.device_info().model);
        device.close().await.expect("close failed");
    }

    #[tokio::test]
    #[ignore]
    #[serial]
    async fn test_ptp_device() {
        let device = try_device!(PtpDevice::open_first().await, "open PTP device");
        let info = try_device!(device.get_device_info().await, "get device info");
        tlog!("PTP Device: {} {}", info.manufacturer, info.model);

        let session = try_device!(device.open_session().await, "open session");
        let storage_ids = try_device!(session.get_storage_ids().await, "get storage IDs");
        tlog!("Storage IDs: {:?}", storage_ids);
        session.close().await.expect("close failed");
    }

    #[tokio::test]
    #[ignore]
    #[serial]
    async fn test_refresh_storage() {
        let device = try_device!(MtpDevice::open_first().await, "open device");
        let mut storages = try_device!(device.storages().await, "get storages");
        let storage = &mut storages[0];

        let before = storage.info().free_space_bytes;
        try_device!(storage.refresh().await, "refresh storage");
        let after = storage.info().free_space_bytes;
        tlog!("Free space: {} -> {} bytes", before, after);
    }

    #[tokio::test]
    #[ignore]
    #[serial]
    async fn test_cancel_download_then_reuse_session() {
        let device = try_device!(MtpDevice::open_first().await, "open device");
        let storages = try_device!(device.storages().await, "get storages");
        let storage = &storages[0];

        tlog!("Searching for file (100KB-10MB) to cancel...");
        let Some((handle, file_size, file_name)) =
            find_suitable_file(storage, 100_000, 10_000_000).await
        else {
            tlog!("No suitable file found, skipping");
            return;
        };
        tlog!("Starting download of {} ({} bytes)", file_name, file_size);

        let mut download = try_device!(storage.download_stream(handle).await, "start download");

        // Read just one chunk, then cancel
        let chunk = download.next_chunk().await.expect("expected a chunk");
        let bytes = chunk.expect("chunk error");
        tlog!(
            "Read {} bytes ({:.1}%), now cancelling...",
            bytes.len(),
            download.progress() * 100.0
        );

        download
            .cancel(std::time::Duration::from_millis(300))
            .await
            .expect("cancel failed");
        tlog!("Cancel succeeded");

        // Drop the download to release the session operation lock
        drop(download);

        // Prove the session is still healthy by doing another operation
        let objects = try_device!(storage.list_objects(None).await, "list root after cancel");
        tlog!(
            "Session healthy: listed {} root objects after cancel",
            objects.len()
        );
        assert!(!objects.is_empty());

        // Do a second download to prove streaming still works
        let mut download2 = try_device!(storage.download_stream(handle).await, "second download");
        let mut total = 0u64;
        while let Some(result) = download2.next_chunk().await {
            total += result.expect("download2 error").len() as u64;
        }
        assert_eq!(total, file_size);
        tlog!(
            "Second full download succeeded ({} bytes). Cancel test PASSED",
            total
        );
    }

    /// Test whether software reconnect recovers after dropping a stream mid-transfer.
    /// This intentionally corrupts the session by dropping without cancel/drain,
    /// then tries to close + reopen the device to see if it recovers.
    ///
    /// Must be run with `--release` to avoid the debug_assert panic in
    /// ReceiveStream::Drop (which is the exact scenario we're testing).
    /// ```sh
    /// cargo test --release --test integration test_drop_mid_stream -- --ignored --nocapture --test-threads=1
    /// ```
    #[tokio::test]
    #[ignore]
    #[serial]
    async fn test_drop_mid_stream_then_software_reconnect() {
        // This test intentionally drops mid-stream, which fires the debug_assert
        // in ReceiveStream::Drop. Skip in debug builds to avoid panicking.
        if cfg!(debug_assertions) {
            tlog!("SKIPPING: requires --release (debug_assert would panic)");
            return;
        }

        // Phase 1: Open device, start download, drop everything mid-stream.
        {
            let device = try_device!(MtpDevice::open_first().await, "open device");
            let storages = try_device!(device.storages().await, "get storages");
            let storage = &storages[0];

            tlog!("Searching for file (100KB-10MB)...");
            let Some((handle, _file_size, file_name)) =
                find_suitable_file(storage, 100_000, 10_000_000).await
            else {
                tlog!("No suitable file found, skipping");
                return;
            };
            tlog!("Starting download of {} ({} bytes)", file_name, _file_size);

            let mut download = try_device!(storage.download_stream(handle).await, "start download");

            // Read one chunk
            let chunk = download.next_chunk().await.expect("expected a chunk");
            let bytes = chunk.expect("chunk error");
            tlog!(
                "Read {} bytes ({:.1}%), now dropping without cancel...",
                bytes.len(),
                download.progress() * 100.0
            );

            // download drops here — in release mode the debug_assert is a no-op,
            // so Drop just releases the session lock and USB interface normally.
            // The USB pipe is left with stale data (intentionally).
        }
        tlog!("Dropped mid-stream (no cancel, no drain)");

        // Phase 2: Try software reconnect
        tlog!("Attempting software reconnect...");
        match MtpDevice::open_first().await {
            Ok(device2) => {
                tlog!("Reconnected: {}", device2.device_info().model);
                match device2.storages().await {
                    Ok(storages2) => {
                        let storage2 = &storages2[0];
                        match storage2.list_objects(None).await {
                            Ok(objects) => {
                                tlog!(
                                    "Software reconnect WORKS: listed {} root objects",
                                    objects.len()
                                );
                            }
                            Err(e) => {
                                tlog!("Software reconnect FAILED at list: {:?}", e);
                            }
                        }
                    }
                    Err(e) => {
                        tlog!("Software reconnect FAILED at storages: {:?}", e);
                    }
                }
            }
            Err(e) => {
                tlog!("Software reconnect FAILED at open: {:?}", e);
            }
        }
    }

    #[tokio::test]
    #[ignore]
    #[serial]
    async fn test_streaming_download() {
        let device = try_device!(MtpDevice::open_first().await, "open device");
        let storages = try_device!(device.storages().await, "get storages");
        let storage = &storages[0];

        tlog!("Searching for file (100KB-5MB)...");
        let Some((handle, file_size, file_name)) =
            find_suitable_file(storage, 100_000, 5_000_000).await
        else {
            tlog!("No suitable file found, skipping");
            return;
        };
        tlog!("Streaming {} ({} bytes)", file_name, file_size);

        let mut download = try_device!(storage.download_stream(handle).await, "start download");
        assert_eq!(download.size(), file_size);

        let mut total_received = 0u64;
        let mut chunk_count = 0u64;

        while let Some(result) = download.next_chunk().await {
            let chunk = result.expect("download error");
            total_received += chunk.len() as u64;
            chunk_count += 1;
        }

        tlog!(
            "Received {} bytes in {} chunks",
            total_received,
            chunk_count
        );
        assert_eq!(total_received, file_size);
    }
}

// Camera control tests disabled - need PtpSession device property methods.

/// Destructive tests - these write to the device.
mod destructive {
    use super::*;
    use bytes::Bytes;
    use mtp_rs::mtp::{MtpDevice, NewObjectInfo};
    use mtp_rs::Error;

    /// Helper to get device, storage, and a writable folder handle.
    ///
    /// Walks a priority list of common folder names (or the
    /// `MTP_TEST_FOLDER` override). Logs which folder was selected so test
    /// output is unambiguous on devices with non-standard layouts.
    async fn setup_with_writable_folder() -> Option<(MtpDevice, mtp_rs::mtp::Storage, ObjectHandle)>
    {
        let device = MtpDevice::open_first().await.ok()?;
        let storages = device.storages().await.ok()?;
        let storage = storages.into_iter().next()?;
        let (handle, name) = find_writable_folder(&storage).await?;
        tlog!("Using writable folder: {}", name);
        Some((device, storage, handle))
    }

    #[tokio::test]
    #[ignore]
    #[serial]
    async fn test_upload_download_delete() {
        let Some((_device, storage, folder_handle)) = setup_with_writable_folder().await else {
            tlog!(
                "Setup failed: no device or no writable folder (set MTP_TEST_FOLDER to override)"
            );
            return;
        };

        let content = format!("Test file at {:?}", std::time::SystemTime::now());
        let content_bytes = content.as_bytes();

        tlog!("Uploading {} bytes...", content_bytes.len());
        let info = NewObjectInfo::file("mtp-rs-test.txt", content_bytes.len() as u64);
        let stream = futures::stream::iter(vec![Ok::<_, std::io::Error>(Bytes::from(
            content_bytes.to_vec(),
        ))]);
        let handle = storage
            .upload(Some(folder_handle), info, Box::pin(stream))
            .await
            .expect("upload failed");

        // Verify
        let obj_info = storage
            .get_object_info(handle)
            .await
            .expect("get info failed");
        assert_eq!(obj_info.filename, "mtp-rs-test.txt");
        assert_eq!(obj_info.size, content_bytes.len() as u64);

        // Download and verify content
        let downloaded = storage.download(handle).await.expect("download failed");
        assert_eq!(downloaded, content_bytes);
        tlog!("Content verified");

        // Delete and verify
        storage.delete(handle).await.expect("delete failed");
        let result = storage.get_object_info(handle).await;
        assert!(matches!(
            result,
            Err(Error::Protocol {
                code: mtp_rs::ptp::ResponseCode::InvalidObjectHandle,
                ..
            })
        ));
        tlog!("Upload/download/delete PASSED");
    }

    #[tokio::test]
    #[ignore]
    #[serial]
    async fn test_create_delete_folder() {
        let Some((_device, storage, folder_handle)) = setup_with_writable_folder().await else {
            tlog!(
                "Setup failed: no device or no writable folder (set MTP_TEST_FOLDER to override)"
            );
            return;
        };

        let folder_name = format!("mtp-rs-test-{}", std::process::id());
        tlog!("Creating folder: {}", folder_name);

        let handle = storage
            .create_folder(Some(folder_handle), &folder_name)
            .await
            .expect("create failed");

        let info = storage
            .get_object_info(handle)
            .await
            .expect("get info failed");
        assert!(info.is_folder());
        assert_eq!(info.filename, folder_name);

        storage.delete(handle).await.expect("delete failed");
        tlog!("Create/delete folder PASSED");
    }

    #[tokio::test]
    #[ignore]
    #[serial]
    async fn test_rename_file() {
        let device = try_device!(MtpDevice::open_first().await, "open device");

        if !device.supports_rename() {
            tlog!("Device doesn't support rename, skipping");
            return;
        }

        let storages = try_device!(device.storages().await, "get storages");
        let storage = &storages[0];
        let Some((folder_handle, folder_name)) = find_writable_folder(storage).await else {
            tlog!("No writable folder found, skipping (set MTP_TEST_FOLDER to override)");
            return;
        };
        tlog!("Using writable folder: {}", folder_name);

        let original = format!("mtp-rs-rename-{}.txt", std::process::id());
        let renamed = format!("mtp-rs-renamed-{}.txt", std::process::id());
        let content = b"rename test";

        let info = NewObjectInfo::file(&original, content.len() as u64);
        let stream =
            futures::stream::iter(vec![Ok::<_, std::io::Error>(Bytes::from(content.to_vec()))]);
        let handle = storage
            .upload(Some(folder_handle), info, Box::pin(stream))
            .await
            .expect("upload failed");

        tlog!("Renaming {} -> {}", original, renamed);
        match storage.rename(handle, &renamed).await {
            Ok(()) => {
                let info = storage
                    .get_object_info(handle)
                    .await
                    .expect("get info failed");
                assert_eq!(info.filename, renamed);
                tlog!("Rename verified");
            }
            Err(Error::Protocol {
                code: mtp_rs::ptp::ResponseCode::OperationNotSupported,
                ..
            }) => {
                tlog!("Rename not actually supported (device lied)");
            }
            Err(e) => {
                storage.delete(handle).await.ok();
                panic!("Rename failed: {:?}", e);
            }
        }

        storage.delete(handle).await.expect("cleanup failed");
        tlog!("Rename test PASSED");
    }

    #[tokio::test]
    #[ignore]
    #[serial]
    async fn test_streaming_upload() {
        let Some((_device, storage, folder_handle)) = setup_with_writable_folder().await else {
            tlog!(
                "Setup failed: no device or no writable folder (set MTP_TEST_FOLDER to override)"
            );
            return;
        };

        let chunk_size = 64 * 1024;
        let num_chunks = 48; // 3 MB -- exercises multi-batch streaming
        let total_size = chunk_size * num_chunks;

        tlog!("Uploading {} bytes in {} chunks", total_size, num_chunks);

        let chunks: Vec<Result<Bytes, std::io::Error>> = (0..num_chunks)
            .map(|i| Ok(Bytes::from(vec![i as u8; chunk_size])))
            .collect();

        let filename = format!("mtp-rs-stream-{}.bin", std::process::id());
        let info = NewObjectInfo::file(&filename, total_size as u64);
        let handle = storage
            .upload(Some(folder_handle), info, futures::stream::iter(chunks))
            .await
            .expect("upload failed");

        // Verify
        let obj_info = storage
            .get_object_info(handle)
            .await
            .expect("get info failed");
        assert_eq!(obj_info.size, total_size as u64);

        let downloaded = storage.download(handle).await.expect("download failed");
        for i in 0..num_chunks {
            let start = i * chunk_size;
            assert!(downloaded[start..start + chunk_size]
                .iter()
                .all(|&b| b == i as u8));
        }

        storage.delete(handle).await.expect("cleanup failed");
        tlog!("Streaming upload PASSED");
    }

    #[tokio::test]
    #[ignore]
    #[serial]
    async fn test_streaming_copy() {
        let Some((_device, storage, folder_handle)) = setup_with_writable_folder().await else {
            tlog!(
                "Setup failed: no device or no writable folder (set MTP_TEST_FOLDER to override)"
            );
            return;
        };

        // Find a file to copy
        let objects = storage
            .list_objects(Some(folder_handle))
            .await
            .unwrap_or_default();
        let Some(source) = objects
            .iter()
            .find(|o| o.is_file() && o.size > 50_000 && o.size < 500_000)
        else {
            tlog!("No suitable source file (50KB-500KB), skipping");
            return;
        };

        let source_handle = source.handle;
        let source_size = source.size;
        tlog!("Copying {} ({} bytes)", source.filename, source_size);

        // Download
        let download = storage
            .download_stream(source_handle)
            .await
            .expect("download failed");
        let data = download.collect().await.expect("collect failed");

        // Upload copy
        let dest_name = format!("mtp-rs-copy-{}.bin", std::process::id());
        let info = NewObjectInfo::file(&dest_name, source_size);
        let stream =
            futures::stream::iter(vec![Ok::<_, std::io::Error>(Bytes::from(data.clone()))]);
        let dest_handle = storage
            .upload(Some(folder_handle), info, stream)
            .await
            .expect("upload failed");

        // Verify
        let copy_data = storage
            .download(dest_handle)
            .await
            .expect("download copy failed");
        assert_eq!(copy_data, data);

        storage.delete(dest_handle).await.expect("cleanup failed");
        tlog!("Streaming copy PASSED");
    }
}
