pub mod backend;
pub mod builder;

pub use backend::{BufferBackend, ReadOnlyBackend, StreamBackend, WriteOnlyBackend};
pub use builder::{IntoStream, StreamBuilder};

use crate::{svn_result, with_tmp_pool, Error};
use std::ffi::OsStr;
use std::marker::PhantomData;
use subversion_sys::svn_io_dirent2_t;

#[allow(dead_code)]
/// Directory entry information.
pub struct Dirent(*const svn_io_dirent2_t);

impl From<*const svn_io_dirent2_t> for Dirent {
    fn from(ptr: *const svn_io_dirent2_t) -> Self {
        Self(ptr)
    }
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
/// File deletion behavior.
pub enum FileDel {
    /// Do not delete the file.
    None,
    /// Delete the file when closed.
    OnClose,
    /// Delete the file when the pool is cleaned up.
    OnPoolCleanup,
}

impl From<FileDel> for subversion_sys::svn_io_file_del_t {
    fn from(file_del: FileDel) -> Self {
        match file_del {
            FileDel::None => subversion_sys::svn_io_file_del_t_svn_io_file_del_none,
            FileDel::OnClose => subversion_sys::svn_io_file_del_t_svn_io_file_del_on_close,
            FileDel::OnPoolCleanup => {
                subversion_sys::svn_io_file_del_t_svn_io_file_del_on_pool_cleanup
            }
        }
    }
}

impl From<subversion_sys::svn_io_file_del_t> for FileDel {
    fn from(file_del: subversion_sys::svn_io_file_del_t) -> Self {
        match file_del {
            subversion_sys::svn_io_file_del_t_svn_io_file_del_none => FileDel::None,
            subversion_sys::svn_io_file_del_t_svn_io_file_del_on_close => FileDel::OnClose,
            subversion_sys::svn_io_file_del_t_svn_io_file_del_on_pool_cleanup => {
                FileDel::OnPoolCleanup
            }
            _ => unreachable!(),
        }
    }
}

/// Stream mark with RAII cleanup
pub struct Mark {
    ptr: *mut subversion_sys::svn_stream_mark_t,
    _pool: apr::Pool<'static>,
    _phantom: PhantomData<*mut ()>, // !Send + !Sync
}

impl Drop for Mark {
    fn drop(&mut self) {
        // Pool drop will clean up mark
    }
}

impl Mark {
    /// Returns a mutable pointer to the stream mark.
    pub fn as_mut_ptr(&mut self) -> *mut subversion_sys::svn_stream_mark_t {
        self.ptr
    }

    /// Returns a pointer to the stream mark.
    pub fn as_ptr(&self) -> *const subversion_sys::svn_stream_mark_t {
        self.ptr
    }

    pub(crate) unsafe fn from_ptr_and_pool(
        ptr: *mut subversion_sys::svn_stream_mark_t,
        pool: apr::Pool<'static>,
    ) -> Self {
        Self {
            ptr,
            _pool: pool,
            _phantom: PhantomData,
        }
    }
}

/// String buffer for stream operations
pub struct StringBuf {
    ptr: *mut subversion_sys::svn_stringbuf_t,
    _pool: apr::Pool<'static>,
    _phantom: PhantomData<*mut ()>, // !Send + !Sync
}

impl Drop for StringBuf {
    fn drop(&mut self) {
        // Pool drop will clean up stringbuf
    }
}

impl Default for StringBuf {
    fn default() -> Self {
        Self::new()
    }
}

impl TryFrom<&str> for StringBuf {
    type Error = std::ffi::NulError;

    fn try_from(s: &str) -> Result<Self, Self::Error> {
        let pool = apr::Pool::new();
        let cstr = std::ffi::CString::new(s)?;
        let ptr = unsafe { subversion_sys::svn_stringbuf_create(cstr.as_ptr(), pool.as_mut_ptr()) };
        Ok(Self {
            ptr,
            _pool: pool,
            _phantom: PhantomData,
        })
    }
}

impl StringBuf {
    /// Create an empty string buffer
    pub fn new() -> Self {
        let pool = apr::Pool::new();
        let ptr = unsafe { subversion_sys::svn_stringbuf_create_empty(pool.as_mut_ptr()) };
        Self {
            ptr,
            _pool: pool,
            _phantom: PhantomData,
        }
    }

    /// Get the contents as a byte slice
    pub fn as_bytes(&self) -> &[u8] {
        unsafe {
            let stringbuf = &*self.ptr;
            std::slice::from_raw_parts(stringbuf.data as *const u8, stringbuf.len)
        }
    }

    /// Get the length of the string buffer
    pub fn len(&self) -> usize {
        unsafe { (*self.ptr).len }
    }

    /// Check if the string buffer is empty
    pub fn is_empty(&self) -> bool {
        self.len() == 0
    }

    pub(crate) fn as_mut_ptr(&mut self) -> *mut subversion_sys::svn_stringbuf_t {
        self.ptr
    }
}

impl std::fmt::Display for StringBuf {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "{}", String::from_utf8_lossy(self.as_bytes()))
    }
}

/// Stream handle with RAII cleanup
pub struct Stream {
    ptr: *mut subversion_sys::svn_stream_t,
    _pool: apr::Pool<'static>,
    needs_close: bool,              // Whether to call svn_stream_close on drop
    _phantom: PhantomData<*mut ()>, // !Send + !Sync
}

impl Drop for Stream {
    fn drop(&mut self) {
        // Close the stream if needed (for streams with close callbacks that need to run).
        // Backends from from_backend() are freed by close_trampoline, not here.
        if self.needs_close {
            unsafe {
                // Ignore errors during drop
                let _ = subversion_sys::svn_stream_close(self.ptr);
            }
        }
        // Pool drop will clean up stream
    }
}

impl Stream {
    /// Creates an empty stream.
    pub fn empty() -> Self {
        let pool = apr::Pool::new();
        let stream = unsafe { subversion_sys::svn_stream_empty(pool.as_mut_ptr()) };
        Self {
            ptr: stream,
            _pool: pool,
            needs_close: false,
            _phantom: PhantomData,
        }
    }

    /// Create a buffered memory stream for reading and writing
    pub fn buffered() -> Self {
        let pool = apr::Pool::new();
        let stream = unsafe { subversion_sys::svn_stream_buffered(pool.as_mut_ptr()) };
        Self {
            ptr: stream,
            _pool: pool,
            needs_close: false,
            _phantom: PhantomData,
        }
    }

    /// Create a stream from a StringBuf for reading and writing
    pub fn from_stringbuf(stringbuf: &mut StringBuf) -> Self {
        let pool = apr::Pool::new();
        let stream = unsafe {
            subversion_sys::svn_stream_from_stringbuf(stringbuf.as_mut_ptr(), pool.as_mut_ptr())
        };
        Self {
            ptr: stream,
            _pool: pool,
            needs_close: false,
            _phantom: PhantomData,
        }
    }

    /// Create a Stream from a raw pointer and pool
    pub fn from_ptr(ptr: *mut subversion_sys::svn_stream_t, pool: apr::Pool<'static>) -> Self {
        Self {
            ptr,
            _pool: pool,
            needs_close: false,
            _phantom: PhantomData,
        }
    }

    /// Create a stream from a custom backend implementation
    pub fn from_backend<B: StreamBackend>(backend: B) -> Result<Self, Error<'static>> {
        let pool = apr::Pool::new();

        // Check if backend supports reset and mark before boxing
        let supports_reset = backend.supports_reset();
        let supports_mark = backend.supports_mark();

        // Box the backend and create a raw pointer for the baton
        let backend_ptr = Box::into_raw(Box::new(backend)) as *mut std::ffi::c_void;

        // Create the SVN stream with our backend as the baton
        let stream = unsafe { subversion_sys::svn_stream_create(backend_ptr, pool.as_mut_ptr()) };

        // Set up the callback trampolines
        unsafe {
            // Read callback
            extern "C" fn read_trampoline<B: StreamBackend>(
                baton: *mut std::ffi::c_void,
                buffer: *mut std::os::raw::c_char,
                len: *mut apr_sys::apr_size_t,
            ) -> *mut subversion_sys::svn_error_t {
                unsafe {
                    let backend = &mut *(baton as *mut B);
                    let requested_len = *len;
                    let buf = std::slice::from_raw_parts_mut(buffer as *mut u8, requested_len);
                    match backend.read(buf) {
                        Ok(bytes_read) => {
                            *len = bytes_read;
                            std::ptr::null_mut()
                        }
                        Err(mut e) => e.detach(),
                    }
                }
            }

            // Write callback
            extern "C" fn write_trampoline<B: StreamBackend>(
                baton: *mut std::ffi::c_void,
                data: *const std::os::raw::c_char,
                len: *mut apr_sys::apr_size_t,
            ) -> *mut subversion_sys::svn_error_t {
                unsafe {
                    let backend = &mut *(baton as *mut B);
                    let requested_len = *len;
                    let buf = std::slice::from_raw_parts(data as *const u8, requested_len);
                    match backend.write(buf) {
                        Ok(bytes_written) => {
                            *len = bytes_written;
                            std::ptr::null_mut()
                        }
                        Err(mut e) => e.detach(),
                    }
                }
            }

            // Close callback - handles cleanup
            extern "C" fn close_trampoline<B: StreamBackend>(
                baton: *mut std::ffi::c_void,
            ) -> *mut subversion_sys::svn_error_t {
                unsafe {
                    if !baton.is_null() {
                        let backend = &mut *(baton as *mut B);
                        // Call close on the backend
                        let result = match backend.close() {
                            Ok(()) => std::ptr::null_mut(),
                            Err(mut e) => e.detach(),
                        };
                        // Take ownership and drop the backend
                        let _ = Box::from_raw(baton as *mut B);
                        result
                    } else {
                        std::ptr::null_mut()
                    }
                }
            }

            // Skip callback
            extern "C" fn skip_trampoline<B: StreamBackend>(
                baton: *mut std::ffi::c_void,
                len: apr_sys::apr_size_t,
            ) -> *mut subversion_sys::svn_error_t {
                unsafe {
                    let backend = &mut *(baton as *mut B);
                    match backend.skip(len) {
                        Ok(_) => std::ptr::null_mut(),
                        Err(mut e) => e.detach(),
                    }
                }
            }

            // Data available callback
            extern "C" fn data_available_trampoline<B: StreamBackend>(
                baton: *mut std::ffi::c_void,
                data_available: *mut subversion_sys::svn_boolean_t,
            ) -> *mut subversion_sys::svn_error_t {
                unsafe {
                    let backend = &mut *(baton as *mut B);
                    match backend.data_available() {
                        Ok(available) => {
                            *data_available = if available { 1 } else { 0 };
                            std::ptr::null_mut()
                        }
                        Err(mut e) => e.detach(),
                    }
                }
            }

            // Mark callback
            extern "C" fn mark_trampoline<B: StreamBackend>(
                baton: *mut std::ffi::c_void,
                mark: *mut *mut subversion_sys::svn_stream_mark_t,
                pool: *mut apr_sys::apr_pool_t,
            ) -> *mut subversion_sys::svn_error_t {
                unsafe {
                    let backend = &mut *(baton as *mut B);
                    match backend.mark() {
                        Ok(stream_mark) => {
                            // Allocate mark structure in the pool
                            let mark_size =
                                std::mem::size_of::<subversion_sys::svn_stream_mark_t>();
                            let mark_ptr = apr_sys::apr_palloc(pool, mark_size)
                                as *mut subversion_sys::svn_stream_mark_t;
                            // Store our position in the mark
                            // Note: This is a simplified implementation
                            *(mark_ptr as *mut u64) = stream_mark.position;
                            *mark = mark_ptr;
                            std::ptr::null_mut()
                        }
                        Err(mut e) => e.detach(),
                    }
                }
            }

            // Seek callback
            extern "C" fn seek_trampoline<B: StreamBackend>(
                baton: *mut std::ffi::c_void,
                mark: *const subversion_sys::svn_stream_mark_t,
            ) -> *mut subversion_sys::svn_error_t {
                unsafe {
                    let backend = &mut *(baton as *mut B);

                    // If mark is null, reset to beginning
                    if mark.is_null() {
                        match backend.reset() {
                            Ok(()) => return std::ptr::null_mut(),
                            Err(mut e) => return e.detach(),
                        }
                    }

                    let position = *(mark as *const u64);
                    let stream_mark = crate::io::backend::StreamMark { position };
                    match backend.seek(&stream_mark) {
                        Ok(()) => std::ptr::null_mut(),
                        Err(mut e) => e.detach(),
                    }
                }
            }

            // Set the callbacks on the stream
            subversion_sys::svn_stream_set_read(stream, Some(read_trampoline::<B>));
            subversion_sys::svn_stream_set_write(stream, Some(write_trampoline::<B>));
            subversion_sys::svn_stream_set_close(stream, Some(close_trampoline::<B>));
            subversion_sys::svn_stream_set_skip(stream, Some(skip_trampoline::<B>));
            subversion_sys::svn_stream_set_data_available(
                stream,
                Some(data_available_trampoline::<B>),
            );

            // Set mark and seek if the backend supports them
            if supports_reset && supports_mark {
                subversion_sys::svn_stream_set_mark(stream, Some(mark_trampoline::<B>));
                subversion_sys::svn_stream_set_seek(stream, Some(seek_trampoline::<B>));
            }
        }

        Ok(Self {
            ptr: stream,
            _pool: pool,
            needs_close: false, // SVN C code closes the stream; avoid double-close/free
            _phantom: PhantomData,
        })
    }

    /// Create a disowned stream that doesn't close the underlying resource
    pub fn disown(&mut self) -> Self {
        let pool = apr::Pool::new();
        let stream = unsafe { subversion_sys::svn_stream_disown(self.ptr, pool.as_mut_ptr()) };
        Self {
            ptr: stream,
            _pool: pool,
            needs_close: false,
            _phantom: PhantomData,
        }
    }

    /// Create a stream from an APR file
    ///
    /// # Safety
    ///
    /// The caller must ensure that `file` is a valid APR file pointer.
    /// If `disown` is true, the caller retains responsibility for closing the file.
    pub unsafe fn from_apr_file(file: *mut apr_sys::apr_file_t, disown: bool) -> Self {
        let pool = apr::Pool::new();
        let stream = unsafe {
            subversion_sys::svn_stream_from_aprfile2(file, disown as i32, pool.as_mut_ptr())
        };
        Self {
            ptr: stream,
            _pool: pool,
            needs_close: false,
            _phantom: PhantomData,
        }
    }

    /// Returns a mutable pointer to the stream.
    pub fn as_mut_ptr(&mut self) -> *mut subversion_sys::svn_stream_t {
        self.ptr
    }

    /// Returns a pointer to the stream.
    pub fn as_ptr(&self) -> *const subversion_sys::svn_stream_t {
        self.ptr
    }

    /// Creates a Stream from a raw pointer and pool.
    pub(crate) unsafe fn from_ptr_and_pool(
        ptr: *mut subversion_sys::svn_stream_t,
        pool: apr::Pool<'static>,
    ) -> Self {
        Self {
            ptr,
            _pool: pool,
            needs_close: false,
            _phantom: PhantomData,
        }
    }

    /// Opens a stream for reading from a file.
    pub fn open_readonly(path: &std::path::Path) -> Result<Self, Error<'static>> {
        let pool = apr::Pool::new();
        let mut stream = std::ptr::null_mut();
        let path = std::ffi::CString::new(path.to_str().unwrap()).unwrap();

        with_tmp_pool(|scratch_pool| {
            let err = unsafe {
                subversion_sys::svn_stream_open_readonly(
                    &mut stream,
                    path.as_ptr(),
                    pool.as_mut_ptr(),
                    scratch_pool.as_mut_ptr(),
                )
            };
            svn_result(err)
        })?;

        Ok(Self {
            ptr: stream,
            _pool: pool,
            needs_close: false,
            _phantom: PhantomData,
        })
    }

    /// Opens a stream for writing to a file.
    pub fn open_writable(path: &std::path::Path) -> Result<Self, Error<'static>> {
        let pool = apr::Pool::new();
        let mut stream = std::ptr::null_mut();
        let path = std::ffi::CString::new(path.to_str().unwrap()).unwrap();

        with_tmp_pool(|scratch_pool| {
            let err = unsafe {
                subversion_sys::svn_stream_open_writable(
                    &mut stream,
                    path.as_ptr(),
                    pool.as_mut_ptr(),
                    scratch_pool.as_mut_ptr(),
                )
            };
            svn_result(err)
        })?;

        Ok(Self {
            ptr: stream,
            _pool: pool,
            needs_close: false,
            _phantom: PhantomData,
        })
    }

    /// Opens a unique temporary file stream.
    pub fn open_unique(
        dirpath: &std::path::Path,
        when: FileDel,
    ) -> Result<(std::path::PathBuf, Self), Error<'static>> {
        let dirpath = std::ffi::CString::new(dirpath.to_str().unwrap()).unwrap();
        let mut stream = std::ptr::null_mut();
        let mut path = std::ptr::null();
        let pool = apr::pool::Pool::new();
        let scratch_pool = apr::pool::Pool::new();
        let err = unsafe {
            subversion_sys::svn_stream_open_unique(
                &mut stream,
                &mut path,
                dirpath.as_ptr(),
                when.into(),
                pool.as_mut_ptr(),
                scratch_pool.as_mut_ptr(),
            )
        };
        Error::from_raw(err)?;
        Ok((
            std::path::PathBuf::from(unsafe { std::ffi::CStr::from_ptr(path).to_str().unwrap() }),
            unsafe { Self::from_ptr_and_pool(stream, pool) },
        ))
    }

    /// Creates a stream for standard input.
    pub fn stdin(buffered: bool) -> Result<Self, Error<'static>> {
        let pool = apr::Pool::new();
        let mut stream = std::ptr::null_mut();
        let err = unsafe {
            subversion_sys::svn_stream_for_stdin2(&mut stream, buffered as i32, pool.as_mut_ptr())
        };
        svn_result(err)?;
        Ok(Self {
            ptr: stream,
            _pool: pool,
            needs_close: false,
            _phantom: PhantomData,
        })
    }

    /// Creates a stream for standard error.
    pub fn stderr() -> Result<Self, Error<'static>> {
        let pool = apr::Pool::new();
        let mut stream = std::ptr::null_mut();
        let err = unsafe { subversion_sys::svn_stream_for_stderr(&mut stream, pool.as_mut_ptr()) };
        svn_result(err)?;
        Ok(Self {
            ptr: stream,
            _pool: pool,
            needs_close: false,
            _phantom: PhantomData,
        })
    }

    /// Creates a stream for standard output.
    pub fn stdout() -> Result<Self, Error<'static>> {
        let pool = apr::Pool::new();
        let mut stream = std::ptr::null_mut();
        let err = unsafe { subversion_sys::svn_stream_for_stdout(&mut stream, pool.as_mut_ptr()) };
        svn_result(err)?;
        Ok(Self {
            ptr: stream,
            _pool: pool,
            needs_close: false,
            _phantom: PhantomData,
        })
    }

    /// Creates a checksummed stream wrapper.
    pub fn checksummed(
        &mut self,
        checksum_kind: crate::ChecksumKind,
        read_all: bool,
    ) -> (crate::io::Stream, crate::Checksum<'_>, crate::Checksum<'_>) {
        let mut read_checksum = std::ptr::null_mut();
        let mut write_checksum = std::ptr::null_mut();
        // Create a pool that will live as long as the returned objects
        let pool = apr::pool::Pool::new();
        let stream = unsafe {
            subversion_sys::svn_stream_checksummed2(
                self.ptr,
                &mut read_checksum,
                &mut write_checksum,
                checksum_kind.into(),
                read_all as i32,
                pool.as_mut_ptr(),
            )
        };
        (
            unsafe { crate::io::Stream::from_ptr_and_pool(stream, pool) },
            crate::Checksum {
                ptr: read_checksum,
                _pool: std::marker::PhantomData,
            },
            crate::Checksum {
                ptr: write_checksum,
                _pool: std::marker::PhantomData,
            },
        )
    }

    /// Creates a compressed stream wrapper.
    pub fn compressed(&mut self) -> Self {
        let pool = apr::Pool::new();
        let stream = unsafe { subversion_sys::svn_stream_compressed(self.ptr, pool.as_mut_ptr()) };
        Self {
            ptr: stream,
            _pool: pool,
            needs_close: false,
            _phantom: PhantomData,
        }
    }

    /// Gets the checksum of the stream contents.
    pub fn contents_checksum(
        &mut self,
        checksum_kind: crate::ChecksumKind,
    ) -> Result<crate::Checksum<'_>, Error<'_>> {
        let mut checksum = std::ptr::null_mut();
        let pool = apr::pool::Pool::new();
        let scratch_pool = apr::pool::Pool::new();
        let err = unsafe {
            subversion_sys::svn_stream_contents_checksum(
                &mut checksum,
                self.ptr,
                checksum_kind.into(),
                pool.as_mut_ptr(),
                scratch_pool.as_mut_ptr(),
            )
        };
        Error::from_raw(err)?;
        Ok(crate::Checksum {
            ptr: checksum,
            _pool: std::marker::PhantomData,
        })
    }

    /// Reads data from the stream into a buffer.
    pub fn read_full(&mut self, buf: &mut [u8]) -> Result<usize, Error<'static>> {
        let mut len = buf.len();
        let err = unsafe {
            subversion_sys::svn_stream_read_full(self.ptr, buf.as_mut_ptr() as *mut i8, &mut len)
        };
        Error::from_raw(err)?;
        Ok(len)
    }

    /// Checks if the stream supports partial reads.
    pub fn supports_partial_read(&mut self) -> bool {
        unsafe { subversion_sys::svn_stream_supports_partial_read(self.ptr) != 0 }
    }

    /// Skips a number of bytes in the stream.
    pub fn skip(&mut self, len: usize) -> Result<(), Error<'static>> {
        let err = unsafe { subversion_sys::svn_stream_skip(self.ptr, len) };
        Error::from_raw(err)?;
        Ok(())
    }

    /// Reads data from the stream into a buffer.
    pub fn read(&mut self, buf: &mut [u8]) -> Result<usize, Error<'static>> {
        let mut len = buf.len();
        let err = unsafe {
            subversion_sys::svn_stream_read(self.ptr, buf.as_mut_ptr() as *mut i8, &mut len)
        };
        Error::from_raw(err)?;
        Ok(len)
    }

    /// Writes data from a buffer to the stream.
    pub fn write(&mut self, buf: &[u8]) -> Result<(), Error<'static>> {
        let mut len = buf.len();
        let err = unsafe {
            subversion_sys::svn_stream_write(self.ptr, buf.as_ptr() as *const i8, &mut len)
        };
        Error::from_raw(err)?;
        Ok(())
    }

    /// Closes the stream.
    pub fn close(&mut self) -> Result<(), Error<'static>> {
        self.needs_close = false; // Prevent double-close in Drop
        let err = unsafe { subversion_sys::svn_stream_close(self.ptr) };
        Error::from_raw(err)?;
        Ok(())
    }

    /// Checks if the stream supports marking.
    pub fn supports_mark(&mut self) -> bool {
        unsafe { subversion_sys::svn_stream_supports_mark(self.ptr) != 0 }
    }

    /// Creates a mark at the current position in the stream.
    pub fn mark(&mut self) -> Result<Mark, Error<'static>> {
        let mut mark = std::ptr::null_mut();
        let pool = apr::pool::Pool::new();
        let err =
            unsafe { subversion_sys::svn_stream_mark(self.ptr, &mut mark, pool.as_mut_ptr()) };
        svn_result(err)?;
        Ok(unsafe { Mark::from_ptr_and_pool(mark, pool) })
    }

    /// Seeks to a previously created mark in the stream.
    pub fn seek(&mut self, mark: &Mark) -> Result<(), Error<'static>> {
        let err = unsafe { subversion_sys::svn_stream_seek(self.ptr, mark.as_ptr()) };
        Error::from_raw(err)?;
        Ok(())
    }

    /// Reset the stream to its beginning (if supported)
    pub fn reset(&mut self) -> Result<(), Error<'static>> {
        let err = unsafe { subversion_sys::svn_stream_reset(self.ptr) };
        Error::from_raw(err)?;
        Ok(())
    }

    /// Check if the stream supports reset
    pub fn supports_reset(&self) -> bool {
        unsafe { subversion_sys::svn_stream_supports_reset(self.ptr) != 0 }
    }

    /// Check if data is available for reading without blocking
    pub fn data_available(&mut self) -> Result<bool, Error<'static>> {
        let mut available = 0;
        let err = unsafe { subversion_sys::svn_stream_data_available(self.ptr, &mut available) };
        Error::from_raw(err)?;
        Ok(available != 0)
    }

    /// Read a line from the stream
    pub fn readline(&mut self, eol: &str) -> Result<Option<String>, Error<'_>> {
        let pool = apr::Pool::new();
        let mut stringbuf = std::ptr::null_mut();
        let mut eof = 0;
        let eol_cstr = std::ffi::CString::new(eol).unwrap();

        let err = unsafe {
            subversion_sys::svn_stream_readline(
                self.ptr,
                &mut stringbuf,
                eol_cstr.as_ptr(),
                &mut eof,
                pool.as_mut_ptr(),
            )
        };
        Error::from_raw(err)?;

        if eof != 0 && stringbuf.is_null() {
            Ok(None)
        } else {
            unsafe {
                let svn_stringbuf = &*stringbuf;
                let data_slice = std::slice::from_raw_parts(
                    svn_stringbuf.data as *const u8,
                    svn_stringbuf.len as usize,
                );
                Ok(Some(String::from_utf8_lossy(data_slice).into_owned()))
            }
        }
    }

    /// Writes a string to the stream.
    pub fn puts(&mut self, s: &str) -> Result<(), Error<'static>> {
        let s = std::ffi::CString::new(s).unwrap();
        let err = unsafe { subversion_sys::svn_stream_puts(self.ptr, s.as_ptr()) };

        Error::from_raw(err)?;
        Ok(())
    }

    /// Write a formatted string to the stream
    pub fn printf(
        &mut self,
        format: &str,
        args: &[&dyn std::fmt::Display],
    ) -> Result<(), Error<'static>> {
        let mut formatted = String::new();
        let mut arg_iter = args.iter();
        let mut chars = format.chars().peekable();

        while let Some(c) = chars.next() {
            if c == '%' {
                if chars.peek() == Some(&'%') {
                    chars.next();
                    formatted.push('%');
                } else {
                    // Skip format specifier characters
                    for spec_char in chars.by_ref() {
                        if spec_char.is_alphabetic() {
                            if let Some(arg) = arg_iter.next() {
                                formatted.push_str(&arg.to_string());
                            }
                            break;
                        }
                    }
                }
            } else {
                formatted.push(c);
            }
        }

        self.puts(&formatted)
    }

    /// Write a formatted UTF-8 string to the stream
    pub fn printf_from_utf8(
        &mut self,
        format: &str,
        args: &[&dyn std::fmt::Display],
    ) -> Result<(), Error<'static>> {
        // For now, this is the same as printf since we're already working with UTF-8
        self.printf(format, args)
    }

    /// Create a stream from a Rust reader
    /// This is a simplified implementation that works with the existing SVN stream infrastructure
    pub fn from_reader<R: std::io::Read + 'static>(mut reader: R) -> Result<Self, Error<'static>> {
        // Read all data from the reader into memory
        let mut buffer = Vec::new();
        reader.read_to_end(&mut buffer).map_err(Error::from)?;

        // Create a stream from the buffer
        Ok(Stream::from(buffer))
    }
}

impl std::io::Write for Stream {
    fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
        match self.write(buf) {
            Ok(_) => Ok(buf.len()),
            Err(e) => Err(std::io::Error::other(e.to_string())),
        }
    }

    fn flush(&mut self) -> std::io::Result<()> {
        Ok(())
    }
}

impl std::io::Read for Stream {
    fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
        match self.read(buf) {
            Ok(bytes_read) => Ok(bytes_read),
            Err(e) => Err(std::io::Error::other(e.to_string())),
        }
    }
}

/// Creates a tee stream that writes to two output streams.
pub fn tee(out1: &mut Stream, out2: &mut Stream) -> Result<Stream, Error<'static>> {
    let pool = apr::Pool::new();
    let stream = unsafe { subversion_sys::svn_stream_tee(out1.ptr, out2.ptr, pool.as_mut_ptr()) };
    Ok(Stream {
        ptr: stream,
        _pool: pool,
        needs_close: false,
        _phantom: PhantomData,
    })
}

/// Check if two streams have the same contents
pub fn streams_contents_same(
    stream1: &mut Stream,
    stream2: &mut Stream,
) -> Result<bool, Error<'static>> {
    let mut same = 0;
    let pool = apr::Pool::new();
    let err = unsafe {
        subversion_sys::svn_stream_contents_same2(
            &mut same,
            stream1.ptr,
            stream2.ptr,
            pool.as_mut_ptr(),
        )
    };
    Error::from_raw(err)?;
    Ok(same != 0)
}

/// Copy contents from one stream to another with optional cancel function
pub fn copy_stream(
    from: &mut Stream,
    to: &mut Stream,
    cancel_func: Option<&impl Fn() -> Result<(), Error<'static>>>,
) -> Result<(), Error<'static>> {
    stream_copy(from, to, cancel_func)
}

/// Read the entire contents of a stream into a string
pub fn read_stream_to_string(stream: &mut Stream) -> Result<String, Error<'static>> {
    string_from_stream(stream)
}

impl From<&[u8]> for Stream {
    fn from(bytes: &[u8]) -> Self {
        let pool = apr::Pool::new();
        // Create a proper svn_string in the pool
        let svn_str = unsafe {
            subversion_sys::svn_string_ncreate(
                bytes.as_ptr() as *const i8,
                bytes.len(),
                pool.as_mut_ptr(),
            )
        };
        let stream = unsafe { subversion_sys::svn_stream_from_string(svn_str, pool.as_mut_ptr()) };
        Self {
            ptr: stream,
            _pool: pool,
            needs_close: false,
            _phantom: PhantomData,
        }
    }
}

impl From<String> for Stream {
    fn from(s: String) -> Self {
        Stream::from(s.as_bytes())
    }
}

impl From<&str> for Stream {
    fn from(s: &str) -> Self {
        Stream::from(s.as_bytes())
    }
}

impl From<Vec<u8>> for Stream {
    fn from(bytes: Vec<u8>) -> Self {
        Stream::from(bytes.as_slice())
    }
}

/// Removes a file from the filesystem.
pub fn remove_file(path: &std::path::Path, ignore_enoent: bool) -> Result<(), Error<'static>> {
    let pool = apr::pool::Pool::new();
    let path = std::ffi::CString::new(path.to_str().unwrap()).unwrap();
    let err = unsafe {
        subversion_sys::svn_io_remove_file2(path.as_ptr(), ignore_enoent as i32, pool.as_mut_ptr())
    };
    Error::from_raw(err)?;
    Ok(())
}

/// Create a stream that wraps a std::io::Write trait object
pub fn wrap_write(write: &mut dyn std::io::Write) -> Result<Stream, Error<'static>> {
    let write = Box::into_raw(Box::new(write));
    let pool = apr::Pool::new();
    let stream = unsafe {
        subversion_sys::svn_stream_create(write as *mut std::ffi::c_void, pool.as_mut_ptr())
    };

    extern "C" fn write_fn(
        baton: *mut std::ffi::c_void,
        buffer: *const i8,
        len: *mut usize,
    ) -> *mut subversion_sys::svn_error_t {
        let write = unsafe { Box::from_raw(baton as *mut &mut dyn std::io::Write) };
        let write = Box::leak(write);
        let buffer = unsafe { std::slice::from_raw_parts(buffer as *const u8, *len) };
        match write.write(buffer) {
            Ok(bytes_written) => {
                unsafe { *len = bytes_written };
                std::ptr::null_mut()
            }
            Err(e) => {
                let mut e: crate::Error = e.into();
                unsafe { e.detach() }
            }
        }
    }

    extern "C" fn close_fn(baton: *mut std::ffi::c_void) -> *mut subversion_sys::svn_error_t {
        let write = unsafe { Box::from_raw(baton as *mut &mut dyn std::io::Write) };
        match write.flush() {
            Ok(_) => std::ptr::null_mut(),
            Err(e) => {
                let mut e: crate::Error = e.into();
                unsafe { e.detach() }
            }
        }
    }

    unsafe {
        subversion_sys::svn_stream_set_write(stream, Some(write_fn));
        subversion_sys::svn_stream_set_close(stream, Some(close_fn));
    }

    Ok(Stream {
        ptr: stream,
        _pool: pool,
        needs_close: true, // Ensure close_fn runs to free the boxed write reference
        _phantom: PhantomData,
    })
}

/// Creates a unique symbolic link.
pub fn create_uniqe_link(
    path: &std::path::Path,
    dest: &std::path::Path,
    suffix: &OsStr,
) -> Result<std::path::PathBuf, Error<'static>> {
    let path = std::ffi::CString::new(path.to_str().unwrap()).unwrap();
    let dest = std::ffi::CString::new(dest.to_str().unwrap()).unwrap();
    let suffix = std::ffi::CString::new(suffix.to_str().unwrap()).unwrap();
    let pool = apr::pool::Pool::new();
    let mut result = std::ptr::null();
    let err = unsafe {
        subversion_sys::svn_io_create_unique_link(
            &mut result,
            path.as_ptr(),
            dest.as_ptr(),
            suffix.as_ptr(),
            pool.as_mut_ptr(),
        )
    };
    Error::from_raw(err)?;
    Ok(std::path::PathBuf::from(unsafe {
        std::ffi::CStr::from_ptr(result).to_str().unwrap()
    }))
}

/// Reads the target of a symbolic link.
pub fn read_link(path: &std::path::Path) -> Result<std::path::PathBuf, Error<'static>> {
    let scratch_pool = apr::pool::Pool::new();
    let path = std::ffi::CString::new(path.to_str().unwrap()).unwrap();
    let mut target = std::ptr::null_mut();
    let err = unsafe {
        subversion_sys::svn_io_read_link(&mut target, path.as_ptr(), scratch_pool.as_mut_ptr())
    };
    Error::from_raw(err)?;

    let ptr = unsafe { (*target).data as *const u8 };
    let len = unsafe { (*target).len };

    let target = unsafe { std::slice::from_raw_parts(ptr, len) };
    Ok(std::path::PathBuf::from(
        std::str::from_utf8(target).unwrap(),
    ))
}

/// Gets the system temporary directory path.
pub fn temp_dir() -> Result<std::path::PathBuf, Error<'static>> {
    let pool = apr::pool::Pool::new();
    let mut path = std::ptr::null();
    let err = unsafe { subversion_sys::svn_io_temp_dir(&mut path, pool.as_mut_ptr()) };
    Error::from_raw(err)?;
    Ok(std::path::PathBuf::from(unsafe {
        std::ffi::CStr::from_ptr(path).to_str().unwrap()
    }))
}

/// Copies a file from source to destination.
pub fn copy_file(
    src: &std::path::Path,
    dest: &std::path::Path,
    copy_perms: bool,
) -> Result<(), Error<'static>> {
    let scratch_pool = apr::pool::Pool::new();
    let src = std::ffi::CString::new(src.to_str().unwrap()).unwrap();
    let dest = std::ffi::CString::new(dest.to_str().unwrap()).unwrap();
    let err = unsafe {
        subversion_sys::svn_io_copy_file(
            src.as_ptr(),
            dest.as_ptr(),
            copy_perms as i32,
            scratch_pool.as_mut_ptr(),
        )
    };
    Error::from_raw(err)?;
    Ok(())
}

/// Copies file permissions from source to destination.
pub fn copy_perms(src: &std::path::Path, dest: &std::path::Path) -> Result<(), Error<'static>> {
    let scratch_pool = apr::pool::Pool::new();
    let src = std::ffi::CString::new(src.to_str().unwrap()).unwrap();
    let dest = std::ffi::CString::new(dest.to_str().unwrap()).unwrap();
    let err = unsafe {
        subversion_sys::svn_io_copy_perms(src.as_ptr(), dest.as_ptr(), scratch_pool.as_mut_ptr())
    };
    Error::from_raw(err)?;
    Ok(())
}

/// Copies a symbolic link from source to destination.
pub fn copy_link(src: &std::path::Path, dest: &std::path::Path) -> Result<(), Error<'static>> {
    let scratch_pool = apr::pool::Pool::new();
    let src = std::ffi::CString::new(src.to_str().unwrap()).unwrap();
    let dest = std::ffi::CString::new(dest.to_str().unwrap()).unwrap();
    let err = unsafe {
        subversion_sys::svn_io_copy_link(src.as_ptr(), dest.as_ptr(), scratch_pool.as_mut_ptr())
    };
    Error::from_raw(err)?;
    Ok(())
}

/// Recursively copies a directory and its contents.
pub fn copy_dir_recursively(
    src: &std::path::Path,
    dst_path: &std::path::Path,
    dst_basename: &std::ffi::OsStr,
    copy_perms: bool,
    cancel_func: Option<&impl Fn() -> Result<(), Error<'static>>>,
) -> Result<(), Error<'static>> {
    let scratch_pool = apr::pool::Pool::new();
    let src = std::ffi::CString::new(src.to_str().unwrap()).unwrap();
    let dst_path = std::ffi::CString::new(dst_path.to_str().unwrap()).unwrap();
    let dst_basename = std::ffi::CString::new(dst_basename.to_str().unwrap()).unwrap();
    let err = unsafe {
        subversion_sys::svn_io_copy_dir_recursively(
            src.as_ptr(),
            dst_path.as_ptr(),
            dst_basename.as_ptr(),
            copy_perms as i32,
            if cancel_func.is_some() {
                Some(crate::wrap_cancel_func)
            } else {
                None
            },
            if let Some(cancel_func) = cancel_func {
                &cancel_func as *const _ as *mut std::ffi::c_void
            } else {
                std::ptr::null_mut()
            },
            scratch_pool.as_mut_ptr(),
        )
    };
    Error::from_raw(err)?;
    Ok(())
}

/// Creates a directory and all necessary parent directories.
pub fn make_dir_recursively(path: &std::path::Path) -> Result<(), Error<'static>> {
    let scratch_pool = apr::pool::Pool::new();
    let path = std::ffi::CString::new(path.to_str().unwrap()).unwrap();
    let err = unsafe {
        subversion_sys::svn_io_make_dir_recursively(path.as_ptr(), scratch_pool.as_mut_ptr())
    };
    Error::from_raw(err)?;
    Ok(())
}

/// Checks if a directory is empty.
pub fn dir_empty(path: &std::path::Path) -> Result<bool, Error<'static>> {
    let scratch_pool = apr::pool::Pool::new();
    let path = std::ffi::CString::new(path.to_str().unwrap()).unwrap();
    let mut empty = 0;
    let err = unsafe {
        subversion_sys::svn_io_dir_empty(&mut empty, path.as_ptr(), scratch_pool.as_mut_ptr())
    };
    Error::from_raw(err)?;
    Ok(empty != 0)
}

/// Appends the contents of one file to another.
pub fn append_file(src: &std::path::Path, dest: &std::path::Path) -> Result<(), Error<'static>> {
    let scratch_pool = apr::pool::Pool::new();
    let src = std::ffi::CString::new(src.to_str().unwrap()).unwrap();
    let dest = std::ffi::CString::new(dest.to_str().unwrap()).unwrap();
    let err = unsafe {
        subversion_sys::svn_io_append_file(src.as_ptr(), dest.as_ptr(), scratch_pool.as_mut_ptr())
    };
    Error::from_raw(err)?;
    Ok(())
}

/// Sets a file to read-only mode.
pub fn set_file_read_only(
    path: &std::path::Path,
    ignore_enoent: bool,
) -> Result<(), Error<'static>> {
    let scratch_pool = apr::pool::Pool::new();
    let path = std::ffi::CString::new(path.to_str().unwrap()).unwrap();

    let err = unsafe {
        subversion_sys::svn_io_set_file_read_only(
            path.as_ptr(),
            ignore_enoent as i32,
            scratch_pool.as_mut_ptr(),
        )
    };
    Error::from_raw(err)?;
    Ok(())
}

/// Sets a file to read-write mode.
pub fn set_file_read_write(
    path: &std::path::Path,
    ignore_enoent: bool,
) -> Result<(), Error<'static>> {
    let scratch_pool = apr::pool::Pool::new();
    let path = std::ffi::CString::new(path.to_str().unwrap()).unwrap();

    let err = unsafe {
        subversion_sys::svn_io_set_file_read_write(
            path.as_ptr(),
            ignore_enoent as i32,
            scratch_pool.as_mut_ptr(),
        )
    };
    Error::from_raw(err)?;
    Ok(())
}

/// Checks if a file is executable.
pub fn is_file_executable(path: &std::path::Path) -> Result<bool, Error<'static>> {
    let scratch_pool = apr::pool::Pool::new();
    let path = std::ffi::CString::new(path.to_str().unwrap()).unwrap();
    let mut executable = 0;
    let err = unsafe {
        subversion_sys::svn_io_is_file_executable(
            &mut executable,
            path.as_ptr(),
            scratch_pool.as_mut_ptr(),
        )
    };
    Error::from_raw(err)?;
    Ok(executable != 0)
}

/// Gets the last affected time of a file.
pub fn file_affected_time(path: &std::path::Path) -> Result<apr::time::Time, Error<'static>> {
    let scratch_pool = apr::pool::Pool::new();
    let path = std::ffi::CString::new(path.to_str().unwrap()).unwrap();
    let mut affected_time = 0;
    let err = unsafe {
        subversion_sys::svn_io_file_affected_time(
            &mut affected_time,
            path.as_ptr(),
            scratch_pool.as_mut_ptr(),
        )
    };
    Error::from_raw(err)?;
    Ok(apr::time::Time::from(affected_time))
}

/// Sets the last affected time of a file.
pub fn set_file_affected_time(
    path: &std::path::Path,
    affected_time: apr::time::Time,
) -> Result<(), Error<'static>> {
    let scratch_pool = apr::pool::Pool::new();
    let path = std::ffi::CString::new(path.to_str().unwrap()).unwrap();
    let affected_time = affected_time.into();
    let err = unsafe {
        subversion_sys::svn_io_set_file_affected_time(
            affected_time,
            path.as_ptr(),
            scratch_pool.as_mut_ptr(),
        )
    };
    Error::from_raw(err)?;
    Ok(())
}

/// Sleeps until the next timestamp change for the given path.
pub fn sleep_for_timestamps(path: &std::path::Path) {
    let scratch_pool = apr::pool::Pool::new();
    let path = std::ffi::CString::new(path.to_str().unwrap()).unwrap();
    unsafe { subversion_sys::svn_io_sleep_for_timestamps(path.as_ptr(), scratch_pool.as_mut_ptr()) }
}

/// Checks if two files have different sizes.
pub fn filesizes_different_p(
    file1: &std::path::Path,
    file2: &std::path::Path,
) -> Result<bool, Error<'static>> {
    let scratch_pool = apr::pool::Pool::new();
    let file1 = std::ffi::CString::new(file1.to_str().unwrap()).unwrap();
    let file2 = std::ffi::CString::new(file2.to_str().unwrap()).unwrap();
    let mut different = 0;
    let err = unsafe {
        subversion_sys::svn_io_filesizes_different_p(
            &mut different,
            file1.as_ptr(),
            file2.as_ptr(),
            scratch_pool.as_mut_ptr(),
        )
    };
    Error::from_raw(err)?;
    Ok(different != 0)
}

/// Checks if three files have different sizes from each other.
pub fn filesizes_three_different_p(
    file1: &std::path::Path,
    file2: &std::path::Path,
    file3: &std::path::Path,
) -> Result<(bool, bool, bool), Error<'static>> {
    let scratch_pool = apr::pool::Pool::new();
    let file1 = std::ffi::CString::new(file1.to_str().unwrap()).unwrap();
    let file2 = std::ffi::CString::new(file2.to_str().unwrap()).unwrap();
    let file3 = std::ffi::CString::new(file3.to_str().unwrap()).unwrap();
    let mut different1 = 0;
    let mut different2 = 0;
    let mut different3 = 0;
    let err = unsafe {
        subversion_sys::svn_io_filesizes_three_different_p(
            &mut different1,
            &mut different2,
            &mut different3,
            file1.as_ptr(),
            file2.as_ptr(),
            file3.as_ptr(),
            scratch_pool.as_mut_ptr(),
        )
    };
    Error::from_raw(err)?;
    Ok((different1 != 0, different2 != 0, different3 != 0))
}

/// Computes a checksum for a file.
pub fn file_checksum(
    file: &std::path::Path,
    checksum_kind: crate::ChecksumKind,
) -> Result<crate::Checksum<'_>, Error<'_>> {
    let file = std::ffi::CString::new(file.to_str().unwrap()).unwrap();
    let mut checksum = std::ptr::null_mut();
    let pool = apr::pool::Pool::new();
    let err = unsafe {
        subversion_sys::svn_io_file_checksum2(
            &mut checksum,
            file.as_ptr(),
            checksum_kind.into(),
            pool.as_mut_ptr(),
        )
    };
    Error::from_raw(err)?;
    Ok(crate::Checksum {
        ptr: checksum,
        _pool: std::marker::PhantomData,
    })
}

/// Checks if two files have the same contents.
pub fn files_contents_same_p(
    file1: &std::path::Path,
    file2: &std::path::Path,
) -> Result<bool, Error<'static>> {
    let scratch_pool = apr::pool::Pool::new();
    let file1 = std::ffi::CString::new(file1.to_str().unwrap()).unwrap();
    let file2 = std::ffi::CString::new(file2.to_str().unwrap()).unwrap();
    let mut same = 0;
    let err = unsafe {
        subversion_sys::svn_io_files_contents_same_p(
            &mut same,
            file1.as_ptr(),
            file2.as_ptr(),
            scratch_pool.as_mut_ptr(),
        )
    };
    Error::from_raw(err)?;
    Ok(same != 0)
}

/// Checks if three files have the same contents.
pub fn files_contents_three_same_p(
    file1: &std::path::Path,
    file2: &std::path::Path,
    file3: &std::path::Path,
) -> Result<(bool, bool, bool), Error<'static>> {
    let scratch_pool = apr::pool::Pool::new();
    let file1 = std::ffi::CString::new(file1.to_str().unwrap()).unwrap();
    let file2 = std::ffi::CString::new(file2.to_str().unwrap()).unwrap();
    let file3 = std::ffi::CString::new(file3.to_str().unwrap()).unwrap();
    let mut same1 = 0;
    let mut same2 = 0;
    let mut same3 = 0;
    let err = unsafe {
        subversion_sys::svn_io_files_contents_three_same_p(
            &mut same1,
            &mut same2,
            &mut same3,
            file1.as_ptr(),
            file2.as_ptr(),
            file3.as_ptr(),
            scratch_pool.as_mut_ptr(),
        )
    };
    Error::from_raw(err)?;
    Ok((same1 != 0, same2 != 0, same3 != 0))
}

/// Creates a file with the specified string contents.
pub fn file_create(path: &std::path::Path, contents: &str) -> Result<(), Error<'static>> {
    let scratch_pool = apr::pool::Pool::new();
    let path = std::ffi::CString::new(path.to_str().unwrap()).unwrap();
    let contents = std::ffi::CString::new(contents).unwrap();
    let err = unsafe {
        subversion_sys::svn_io_file_create(
            path.as_ptr(),
            contents.as_ptr(),
            scratch_pool.as_mut_ptr(),
        )
    };
    Error::from_raw(err)?;
    Ok(())
}

/// Creates a file with the specified byte contents.
pub fn file_create_bytes(path: &std::path::Path, contents: &[u8]) -> Result<(), Error<'static>> {
    let scratch_pool = apr::pool::Pool::new();
    let path = std::ffi::CString::new(path.to_str().unwrap()).unwrap();
    let err = unsafe {
        subversion_sys::svn_io_file_create_bytes(
            path.as_ptr(),
            contents.as_ptr() as *const std::ffi::c_void,
            contents.len(),
            scratch_pool.as_mut_ptr(),
        )
    };
    Error::from_raw(err)?;
    Ok(())
}

/// Creates an empty file.
pub fn file_create_empty(path: &std::path::Path) -> Result<(), Error<'static>> {
    let scratch_pool = apr::pool::Pool::new();
    let path = std::ffi::CString::new(path.to_str().unwrap()).unwrap();
    let err = unsafe {
        subversion_sys::svn_io_file_create_empty(path.as_ptr(), scratch_pool.as_mut_ptr())
    };
    Error::from_raw(err)?;
    Ok(())
}

/// Locks a file.
pub fn file_lock(
    path: &std::path::Path,
    exclusive: bool,
    nonblocking: bool,
) -> Result<(), Error<'static>> {
    let scratch_pool = apr::pool::Pool::new();
    let path = std::ffi::CString::new(path.to_str().unwrap()).unwrap();
    let err = unsafe {
        subversion_sys::svn_io_file_lock2(
            path.as_ptr(),
            exclusive as i32,
            nonblocking as i32,
            scratch_pool.as_mut_ptr(),
        )
    };
    Error::from_raw(err)?;
    Ok(())
}

/// Copies a file from one directory to another.
pub fn dir_file_copy(
    src_path: &std::path::Path,
    dest_path: &std::path::Path,
    file: &std::ffi::OsStr,
) -> Result<(), Error<'static>> {
    let scratch_pool = apr::pool::Pool::new();
    let src_path = std::ffi::CString::new(src_path.to_str().unwrap()).unwrap();
    let dest_path = std::ffi::CString::new(dest_path.to_str().unwrap()).unwrap();
    let file = std::ffi::CString::new(file.to_str().unwrap()).unwrap();
    let err = unsafe {
        subversion_sys::svn_io_dir_file_copy(
            src_path.as_ptr(),
            dest_path.as_ptr(),
            file.as_ptr(),
            scratch_pool.as_mut_ptr(),
        )
    };
    Error::from_raw(err)?;
    Ok(())
}

/// Copies data from one stream to another.
pub fn stream_copy(
    from: &mut Stream,
    to: &mut Stream,
    cancel_func: Option<&impl Fn() -> Result<(), Error<'static>>>,
) -> Result<(), Error<'static>> {
    let scratch_pool = apr::pool::Pool::new();
    let err = unsafe {
        subversion_sys::svn_stream_copy3(
            from.ptr,
            to.ptr,
            if cancel_func.is_some() {
                Some(crate::wrap_cancel_func)
            } else {
                None
            },
            if let Some(cancel_func) = cancel_func {
                &cancel_func as *const _ as *mut std::ffi::c_void
            } else {
                std::ptr::null_mut()
            },
            scratch_pool.as_mut_ptr(),
        )
    };
    Error::from_raw(err)?;
    Ok(())
}

/// Checks if two streams have the same contents.
pub fn stream_contents_same(
    stream1: &mut Stream,
    stream2: &mut Stream,
) -> Result<bool, Error<'static>> {
    let scratch_pool = apr::pool::Pool::new();
    let mut same = 0;
    let err = unsafe {
        subversion_sys::svn_stream_contents_same(
            &mut same,
            stream1.ptr,
            stream2.ptr,
            scratch_pool.as_mut_ptr(),
        )
    };
    Error::from_raw(err)?;
    Ok(same != 0)
}

/// Reads a string from a stream.
pub fn string_from_stream(stream: &mut Stream) -> Result<String, Error<'static>> {
    let mut str = std::ptr::null_mut();
    let pool = apr::pool::Pool::new();
    let err = unsafe {
        subversion_sys::svn_string_from_stream(
            &mut str,
            stream.ptr,
            pool.as_mut_ptr(),
            pool.as_mut_ptr(),
        )
    };
    Error::from_raw(err)?;
    let data = unsafe { (*str).data };
    let len = unsafe { (*str).len };
    Ok(unsafe {
        std::str::from_utf8(std::slice::from_raw_parts(data as *const u8, len))
            .unwrap()
            .to_string()
    })
}

/// Removes a directory and its contents.
pub fn remove_dir(
    path: &std::path::Path,
    ignore_enoent: bool,
    cancel_func: Option<&impl Fn() -> Result<(), Error<'static>>>,
) -> Result<(), Error<'static>> {
    let scratch_pool = apr::pool::Pool::new();
    let path = std::ffi::CString::new(path.to_str().unwrap()).unwrap();
    let err = unsafe {
        subversion_sys::svn_io_remove_dir2(
            path.as_ptr(),
            ignore_enoent as i32,
            if cancel_func.is_some() {
                Some(crate::wrap_cancel_func)
            } else {
                None
            },
            if let Some(cancel_func) = cancel_func {
                &cancel_func as *const _ as *mut std::ffi::c_void
            } else {
                std::ptr::null_mut()
            },
            scratch_pool.as_mut_ptr(),
        )
    };
    Error::from_raw(err)?;
    Ok(())
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_stream_empty() {
        let mut stream = Stream::empty();
        let mut buf = [0u8; 10];
        let bytes_read = stream.read(&mut buf).unwrap();
        assert_eq!(bytes_read, 0);
    }

    #[test]
    fn test_stream_from_string() {
        let data = "Hello, world!";
        let mut stream = Stream::from(data);
        let mut buf = vec![0u8; data.len()];
        let bytes_read = stream.read_full(&mut buf).unwrap();
        assert_eq!(bytes_read, data.len());
        assert_eq!(&buf[..bytes_read], data.as_bytes());
    }

    #[test]
    fn test_stream_from_bytes() {
        let data = b"Binary \x00 data";
        let mut stream = Stream::from(&data[..]);
        let mut buf = vec![0u8; data.len()];
        let bytes_read = stream.read_full(&mut buf).unwrap();
        assert_eq!(bytes_read, data.len());
        assert_eq!(&buf[..bytes_read], data);
    }

    #[test]
    fn test_stringbuf_operations() {
        let stringbuf = StringBuf::try_from("Initial content").unwrap();
        assert_eq!(stringbuf.len(), 15);
        assert!(!stringbuf.is_empty());
        assert_eq!(stringbuf.to_string(), "Initial content");
        assert_eq!(stringbuf.as_bytes(), b"Initial content");
    }

    #[test]
    fn test_stream_buffered() {
        let mut stream = Stream::buffered();
        let data = b"Test data";
        stream.write(data).unwrap();

        // Note: buffered streams may not allow immediate read after write
        // without reset/seek, so this test just verifies creation
    }

    #[test]
    fn test_stream_write_read_with_stringbuf() {
        let mut stringbuf = StringBuf::new();

        // Write data
        {
            let mut write_stream = Stream::from_stringbuf(&mut stringbuf);
            write_stream.write(b"Hello").unwrap();
            write_stream.write(b", ").unwrap();
            write_stream.write(b"world!").unwrap();
        }

        // Verify stringbuf contains the data
        assert_eq!(stringbuf.to_string(), "Hello, world!");
    }

    #[test]
    fn test_stream_puts_printf() {
        let mut stringbuf = StringBuf::new();
        let mut stream = Stream::from_stringbuf(&mut stringbuf);

        stream.puts("Line 1").unwrap();
        stream.puts("Line 2").unwrap();
        stream.printf("Number: %d", &[&42]).unwrap();

        let content = stringbuf.to_string();
        assert!(content.contains("Line 1"));
        assert!(content.contains("Line 2"));
        assert!(content.contains("42"));
    }

    #[test]
    fn test_stream_reset() {
        // Create a stream that supports reset (string-based streams typically do)
        let data = "Test data for reset";
        let mut stream = Stream::from(data);

        if stream.supports_reset() {
            let mut buf1 = vec![0u8; 4];
            stream.read_full(&mut buf1).unwrap();
            assert_eq!(&buf1, b"Test");

            // Reset and read again
            stream.reset().unwrap();
            let mut buf2 = vec![0u8; 4];
            stream.read_full(&mut buf2).unwrap();
            assert_eq!(&buf2, b"Test");
        }
    }

    #[test]
    fn test_stream_mark_seek() {
        let data = "0123456789";
        let mut stream = Stream::from(data);

        if stream.supports_mark() {
            // Read first 3 bytes
            let mut buf = vec![0u8; 3];
            stream.read_full(&mut buf).unwrap();
            assert_eq!(&buf, b"012");

            // Mark current position
            let mark = stream.mark().unwrap();

            // Read next 3 bytes
            stream.read_full(&mut buf).unwrap();
            assert_eq!(&buf, b"345");

            // Seek back to mark
            stream.seek(&mark).unwrap();

            // Read should give us "345" again
            stream.read_full(&mut buf).unwrap();
            assert_eq!(&buf, b"345");
        }
    }

    #[test]
    fn test_stream_readline() {
        let data = "Line 1\nLine 2\rLine 3\r\nLine 4";
        let mut stream = Stream::from(data);

        // Read lines with different EOL markers
        let result = stream.readline("\n");
        if let Ok(Some(line)) = result {
            assert_eq!(line, "Line 1");
        };
    }

    #[test]
    fn test_stream_skip() {
        let data = "0123456789";
        let mut stream = Stream::from(data);

        // Skip first 5 bytes
        stream.skip(5).unwrap();

        // Read remaining
        let mut buf = vec![0u8; 5];
        let bytes_read = stream.read_full(&mut buf).unwrap();
        assert_eq!(bytes_read, 5);
        assert_eq!(&buf, b"56789");
    }

    #[test]
    fn test_io_read_trait() {
        use std::io::Read;

        let data = "Hello from Read trait";
        let mut stream = Stream::from(data);
        let mut buf = String::new();
        stream.read_to_string(&mut buf).unwrap();
        assert_eq!(buf, data);
    }

    #[test]
    fn test_io_write_trait() {
        use std::io::Write;

        let mut stringbuf = StringBuf::new();
        let mut stream = Stream::from_stringbuf(&mut stringbuf);

        write!(stream, "Hello world").unwrap();
        stream.flush().unwrap();

        assert!(stringbuf.to_string().contains("Hello world"));
    }

    #[test]
    fn test_stream_std_io_write() {
        use std::io::Write;

        let mut stringbuf = StringBuf::new();
        let mut stream = Stream::from_stringbuf(&mut stringbuf);

        // Test std::io::Write trait - explicitly use the trait method
        let data = b"Hello, World!";
        let bytes_written = Write::write(&mut stream, data).unwrap();
        assert_eq!(bytes_written, data.len());

        // Verify the data was written
        let result = stringbuf.to_string();
        assert_eq!(result, "Hello, World!");
    }

    #[test]
    fn test_stream_std_io_read() {
        use std::io::Read;

        // Create a stream from some data
        let data = b"Test data for reading";
        let mut stream = Stream::from(data.as_slice());

        // Test std::io::Read trait - explicitly use the trait method
        let mut buffer = vec![0u8; 9];
        let bytes_read = Read::read(&mut stream, &mut buffer).unwrap();
        assert_eq!(bytes_read, 9);
        assert_eq!(&buffer, b"Test data");

        // Read more
        let mut buffer2 = vec![0u8; 12];
        let bytes_read = Read::read(&mut stream, &mut buffer2).unwrap();
        assert_eq!(bytes_read, 12);
        assert_eq!(&buffer2, b" for reading");
    }

    #[test]
    fn test_stream_read_write_roundtrip() {
        use std::io::{Read, Write};

        // Write data to a stream
        let mut stringbuf = StringBuf::new();
        let mut write_stream = Stream::from_stringbuf(&mut stringbuf);

        write_stream.write_all(b"First line\n").unwrap();
        write_stream.write_all(b"Second line\n").unwrap();
        write_stream.write_all(b"Third line").unwrap();

        // Create a new stream from the written data to read it back
        let written_data = stringbuf.to_string();
        let mut read_stream = Stream::from(written_data.as_str());

        let mut read_buffer = String::new();
        read_stream.read_to_string(&mut read_buffer).unwrap();

        assert_eq!(read_buffer, "First line\nSecond line\nThird line");
    }

    #[test]
    fn test_custom_backend_buffer() {
        use crate::io::backend::BufferBackend;

        // Create a stream with a buffer backend
        let backend = BufferBackend::from_vec(b"Hello from buffer backend!".to_vec());
        let mut stream = Stream::from_backend(backend).unwrap();

        // Read from the stream
        let mut buf = vec![0u8; 5];
        let n = stream.read(&mut buf).unwrap();
        assert_eq!(n, 5);
        assert_eq!(&buf, b"Hello");

        // Read more
        let mut buf2 = vec![0u8; 21];
        let n = stream.read(&mut buf2).unwrap();
        assert_eq!(n, 21);
        assert_eq!(&buf2, b" from buffer backend!");
    }

    #[test]
    fn test_custom_backend_read_write() {
        use crate::io::backend::BufferBackend;

        // Create a stream with an empty buffer backend
        let backend = BufferBackend::new();
        let mut stream = Stream::from_backend(backend).unwrap();

        // Write to the stream
        stream.write(b"Test data").unwrap();
        stream.write(b" for custom backend").unwrap();

        // Reset to read from beginning
        stream.reset().unwrap();

        // Read back
        let mut buf = vec![0u8; 28];
        let n = stream.read(&mut buf).unwrap();
        assert_eq!(n, 28);
        assert_eq!(&buf[..n], b"Test data for custom backend");
    }

    #[test]
    fn test_stream_builder() {
        use crate::io::backend::BufferBackend;
        use crate::io::builder::StreamBuilder;

        // Create a stream using the builder
        let backend = BufferBackend::from_vec(b"Builder test".to_vec());
        let mut stream = StreamBuilder::new(backend)
            .buffer_size(4096)
            .build()
            .unwrap();

        // Read from the stream
        let mut buf = vec![0u8; 12];
        let n = stream.read(&mut buf).unwrap();
        assert_eq!(n, 12);
        assert_eq!(&buf, b"Builder test");
    }

    #[test]
    fn test_read_only_backend() {
        use crate::io::backend::ReadOnlyBackend;
        use std::io::Cursor;

        // Create a read-only stream from a cursor
        let data = b"Read-only data";
        let cursor = Cursor::new(data);
        let backend = ReadOnlyBackend::new(cursor);
        let mut stream = Stream::from_backend(backend).unwrap();

        // Read from the stream
        let mut buf = vec![0u8; 14];
        let n = stream.read(&mut buf).unwrap();
        assert_eq!(n, 14);
        assert_eq!(&buf, b"Read-only data");

        // Writing should fail (backend doesn't support write)
        let result = stream.write(b"Should fail");
        assert!(result.is_err());
    }

    #[test]
    fn test_write_only_backend() {
        use crate::io::backend::BufferBackend;

        // Use BufferBackend for write testing since it owns its data
        let backend = BufferBackend::new();
        let mut stream = Stream::from_backend(backend).unwrap();

        // Write to the stream
        stream.write(b"Write test data").unwrap();

        // Reset and read back to verify
        stream.reset().unwrap();
        let mut buf = vec![0u8; 15];
        let n = stream.read(&mut buf).unwrap();
        assert_eq!(n, 15);
        assert_eq!(&buf, b"Write test data");
    }

    #[test]
    fn test_from_backend_mark_requires_both_reset_and_mark() {
        // A backend that supports reset but NOT mark. from_backend only wires
        // up the mark/seek callbacks when BOTH are supported (reset && mark),
        // so the resulting stream must report no mark support.
        struct ResetOnlyBackend;
        impl crate::io::backend::StreamBackend for ResetOnlyBackend {
            fn read(&mut self, _buf: &mut [u8]) -> Result<usize, Error<'static>> {
                Ok(0)
            }
            fn supports_reset(&self) -> bool {
                true
            }
            fn reset(&mut self) -> Result<(), Error<'static>> {
                Ok(())
            }
            // supports_mark stays false (trait default).
        }

        let mut stream = Stream::from_backend(ResetOnlyBackend).unwrap();
        assert!(
            !stream.supports_mark(),
            "mark must not be enabled unless the backend supports both reset and mark"
        );
    }

    #[test]
    fn test_from_backend_skip() {
        use crate::io::backend::BufferBackend;
        let backend = BufferBackend::from_vec(b"0123456789".to_vec());
        let mut stream = Stream::from_backend(backend).unwrap();

        // skip routes through skip_trampoline -> backend.skip().
        stream.skip(4).unwrap();
        let mut buf = vec![0u8; 6];
        let n = stream.read(&mut buf).unwrap();
        assert_eq!(n, 6);
        assert_eq!(&buf, b"456789");
    }

    #[test]
    fn test_from_backend_mark_seek() {
        use crate::io::backend::BufferBackend;
        let backend = BufferBackend::from_vec(b"0123456789".to_vec());
        let mut stream = Stream::from_backend(backend).unwrap();

        assert!(stream.supports_mark());

        let mut buf = vec![0u8; 3];
        stream.read_full(&mut buf).unwrap();
        assert_eq!(&buf, b"012");

        // mark routes through mark_trampoline -> backend.mark().
        let mark = stream.mark().unwrap();
        stream.read_full(&mut buf).unwrap();
        assert_eq!(&buf, b"345");

        // seek routes through seek_trampoline -> backend.seek().
        stream.seek(&mark).unwrap();
        stream.read_full(&mut buf).unwrap();
        assert_eq!(&buf, b"345");
    }

    #[test]
    fn test_from_backend_close_propagates_error() {
        // A backend whose close() errors; close_trampoline must surface it,
        // and must still drop the backend (no leak / no double free).
        struct ClosesWithError;
        impl crate::io::backend::StreamBackend for ClosesWithError {
            fn read(&mut self, _buf: &mut [u8]) -> Result<usize, Error<'static>> {
                Ok(0)
            }
            fn close(&mut self) -> Result<(), Error<'static>> {
                Err(Error::from_message("close boom"))
            }
        }

        let mut stream = Stream::from_backend(ClosesWithError).unwrap();
        let result = stream.close();
        assert!(
            result.is_err(),
            "close() should propagate the backend close error"
        );
    }

    #[test]
    fn test_from_backend_no_double_close() {
        use crate::io::backend::BufferBackend;

        // Verify that a stream created via from_backend can be read from
        // and then dropped without double-close issues.
        // The close_trampoline frees the backend; Drop must not close again.
        let backend = BufferBackend::from_vec(b"no double close".to_vec());
        let mut stream = Stream::from_backend(backend).unwrap();

        // needs_close should be false for backend-created streams
        assert!(!stream.needs_close);

        let mut buf = vec![0u8; 15];
        let n = stream.read(&mut buf).unwrap();
        assert_eq!(n, 15);
        assert_eq!(&buf, b"no double close");

        // Explicitly close the stream (simulating what SVN C code would do)
        stream.close().unwrap();

        // Drop should be safe now (no double-close)
        drop(stream);
    }

    #[test]
    fn test_from_backend_drop_without_close() {
        // Stream is dropped without anyone calling close().
        // Backend should still be freed (no leak).
        use crate::io::backend::ReadOnlyBackend;
        use std::io::Cursor;

        let data = b"drop without close";
        let backend = ReadOnlyBackend::new(Cursor::new(data.to_vec()));
        let mut stream = Stream::from_backend(backend).unwrap();

        let mut buf = vec![0u8; 18];
        let n = stream.read(&mut buf).unwrap();
        assert_eq!(n, 18);
        assert_eq!(&buf, &b"drop without close"[..]);

        // Just drop — no explicit close
        drop(stream);
    }

    #[test]
    fn test_from_backend_svn_close_then_drop() {
        // Simulates SVN C code closing the stream, then Rust dropping it.
        // Must not double-free.
        use crate::io::backend::BufferBackend;

        let backend = BufferBackend::from_vec(b"svn close then drop".to_vec());
        let mut stream = Stream::from_backend(backend).unwrap();

        let mut buf = vec![0u8; 19];
        let n = stream.read(&mut buf).unwrap();
        assert_eq!(n, 19);

        // SVN calls svn_stream_close
        stream.close().unwrap();

        // Rust drops — must be safe
        drop(stream);
    }

    #[test]
    fn test_from_backend_read_eof() {
        // Read past EOF returns 0 bytes.
        use crate::io::backend::ReadOnlyBackend;
        use std::io::Cursor;

        let backend = ReadOnlyBackend::new(Cursor::new(b"short".to_vec()));
        let mut stream = Stream::from_backend(backend).unwrap();

        let mut buf = vec![0u8; 100];
        let n = stream.read(&mut buf).unwrap();
        assert_eq!(n, 5);
        assert_eq!(&buf[..5], b"short");

        // Read again — should get 0 (EOF)
        let n = stream.read(&mut buf).unwrap();
        assert_eq!(n, 0);
    }

    #[test]
    fn test_from_backend_empty_stream() {
        use crate::io::backend::ReadOnlyBackend;
        use std::io::Cursor;

        let backend = ReadOnlyBackend::new(Cursor::new(Vec::new()));
        let mut stream = Stream::from_backend(backend).unwrap();

        let mut buf = vec![0u8; 10];
        let n = stream.read(&mut buf).unwrap();
        assert_eq!(n, 0);

        drop(stream);
    }

    #[test]
    fn test_from_backend_write_then_close() {
        use crate::io::backend::BufferBackend;

        let backend = BufferBackend::new();
        let mut stream = Stream::from_backend(backend).unwrap();

        stream.write(b"hello ").unwrap();
        stream.write(b"world").unwrap();

        stream.reset().unwrap();
        let mut buf = vec![0u8; 11];
        let n = stream.read(&mut buf).unwrap();
        assert_eq!(n, 11);
        assert_eq!(&buf, b"hello world");

        stream.close().unwrap();
        drop(stream);
    }

    #[test]
    fn test_from_backend_multiple_reads() {
        use crate::io::backend::ReadOnlyBackend;
        use std::io::Cursor;

        let backend = ReadOnlyBackend::new(Cursor::new(b"abcdefghij".to_vec()));
        let mut stream = Stream::from_backend(backend).unwrap();

        let mut buf = vec![0u8; 3];
        assert_eq!(stream.read(&mut buf).unwrap(), 3);
        assert_eq!(&buf, b"abc");

        assert_eq!(stream.read(&mut buf).unwrap(), 3);
        assert_eq!(&buf, b"def");

        assert_eq!(stream.read(&mut buf).unwrap(), 3);
        assert_eq!(&buf, b"ghi");

        assert_eq!(stream.read(&mut buf).unwrap(), 1);
        assert_eq!(&buf[..1], b"j");

        assert_eq!(stream.read(&mut buf).unwrap(), 0);
    }

    #[test]
    fn test_copy_perms_actually_copies() {
        // Test that copy_perms() actually copies file permissions
        let td = tempfile::tempdir().unwrap();
        let src = td.path().join("source.txt");
        let dest = td.path().join("dest.txt");

        // Create source file with specific permissions
        std::fs::write(&src, "source content").unwrap();
        #[cfg(unix)]
        {
            use std::os::unix::fs::PermissionsExt;
            let perms = std::fs::Permissions::from_mode(0o644);
            std::fs::set_permissions(&src, perms).unwrap();
        }

        // Create dest file with different permissions
        std::fs::write(&dest, "dest content").unwrap();
        #[cfg(unix)]
        {
            use std::os::unix::fs::PermissionsExt;
            let perms = std::fs::Permissions::from_mode(0o600);
            std::fs::set_permissions(&dest, perms).unwrap();
        }

        // Copy permissions
        copy_perms(&src, &dest).unwrap();

        // Verify permissions were actually copied
        #[cfg(unix)]
        {
            use std::os::unix::fs::PermissionsExt;
            let src_perms = std::fs::metadata(&src).unwrap().permissions().mode();
            let dest_perms = std::fs::metadata(&dest).unwrap().permissions().mode();
            assert_eq!(
                src_perms & 0o777,
                dest_perms & 0o777,
                "Permissions should be copied from source to dest"
            );
        }
    }

    #[test]
    #[cfg(unix)]
    fn test_set_file_read_write_actually_changes_perms() {
        // Test that set_file_read_write() actually changes file permissions
        let td = tempfile::tempdir().unwrap();
        let path = td.path().join("test.txt");

        // Create file with read-only permissions
        std::fs::write(&path, "test content").unwrap();
        use std::os::unix::fs::PermissionsExt;
        let readonly_perms = std::fs::Permissions::from_mode(0o444);
        std::fs::set_permissions(&path, readonly_perms).unwrap();

        // Verify it's read-only
        let before_perms = std::fs::metadata(&path).unwrap().permissions().mode();
        assert_eq!(before_perms & 0o200, 0, "File should be read-only before");

        // Make it read-write
        set_file_read_write(&path, false).unwrap();

        // Verify it's now read-write
        let after_perms = std::fs::metadata(&path).unwrap().permissions().mode();
        assert_ne!(
            after_perms & 0o200,
            0,
            "File should be writable after set_file_read_write"
        );
    }

    #[test]
    fn test_remove_file() {
        let td = tempfile::tempdir().unwrap();
        let path = td.path().join("doomed.txt");
        std::fs::write(&path, "bye").unwrap();
        assert!(path.exists());
        remove_file(&path, false).unwrap();
        assert!(!path.exists());
    }

    #[test]
    fn test_remove_file_enoent() {
        let td = tempfile::tempdir().unwrap();
        let missing = td.path().join("nope.txt");
        // ignore_enoent = true succeeds, false errors
        remove_file(&missing, true).unwrap();
        assert!(remove_file(&missing, false).is_err());
    }

    #[test]
    fn test_temp_dir() {
        let dir = temp_dir().unwrap();
        assert!(dir.is_dir(), "temp_dir {:?} should exist", dir);
    }

    #[test]
    fn test_copy_file() {
        let td = tempfile::tempdir().unwrap();
        let src = td.path().join("src.txt");
        let dest = td.path().join("dest.txt");
        std::fs::write(&src, "payload").unwrap();
        copy_file(&src, &dest, false).unwrap();
        assert_eq!(std::fs::read_to_string(&dest).unwrap(), "payload");
    }

    #[test]
    fn test_make_dir_recursively() {
        let td = tempfile::tempdir().unwrap();
        let nested = td.path().join("a/b/c");
        make_dir_recursively(&nested).unwrap();
        assert!(nested.is_dir());
    }

    #[test]
    fn test_dir_empty() {
        let td = tempfile::tempdir().unwrap();
        let empty = td.path().join("empty");
        std::fs::create_dir(&empty).unwrap();
        assert!(dir_empty(&empty).unwrap());

        std::fs::write(empty.join("file.txt"), "x").unwrap();
        assert!(!dir_empty(&empty).unwrap());
    }

    #[test]
    fn test_append_file() {
        let td = tempfile::tempdir().unwrap();
        let src = td.path().join("src.txt");
        let dest = td.path().join("dest.txt");
        std::fs::write(&src, "world").unwrap();
        std::fs::write(&dest, "hello ").unwrap();
        append_file(&src, &dest).unwrap();
        assert_eq!(std::fs::read_to_string(&dest).unwrap(), "hello world");
    }

    #[test]
    #[cfg(unix)]
    fn test_is_file_executable() {
        use std::os::unix::fs::PermissionsExt;
        let td = tempfile::tempdir().unwrap();
        let path = td.path().join("script");
        std::fs::write(&path, "#!/bin/sh\n").unwrap();

        std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o644)).unwrap();
        assert!(!is_file_executable(&path).unwrap());

        std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o755)).unwrap();
        assert!(is_file_executable(&path).unwrap());
    }

    #[test]
    fn test_set_file_affected_time() {
        let td = tempfile::tempdir().unwrap();
        let path = td.path().join("timed.txt");
        std::fs::write(&path, "data").unwrap();

        // 2001-09-09T01:46:40Z in microseconds since the epoch.
        let when = apr::time::Time::from(1_000_000_000_000_000i64);
        set_file_affected_time(&path, when).unwrap();
        let got: i64 = file_affected_time(&path).unwrap().into();
        assert_eq!(got, 1_000_000_000_000_000i64);
    }

    #[test]
    fn test_filesizes_different_p() {
        let td = tempfile::tempdir().unwrap();
        let a = td.path().join("a.txt");
        let b = td.path().join("b.txt");
        let c = td.path().join("c.txt");
        std::fs::write(&a, "1234").unwrap();
        std::fs::write(&b, "1234").unwrap();
        std::fs::write(&c, "123456").unwrap();
        assert!(!filesizes_different_p(&a, &b).unwrap());
        assert!(filesizes_different_p(&a, &c).unwrap());
    }

    #[test]
    fn test_filesizes_three_different_p() {
        let td = tempfile::tempdir().unwrap();
        let a = td.path().join("a.txt");
        let b = td.path().join("b.txt");
        let c = td.path().join("c.txt");
        std::fs::write(&a, "1234").unwrap();
        std::fs::write(&b, "1234").unwrap();
        std::fs::write(&c, "123456").unwrap();
        // (1 vs 2, 1 vs 3, 2 vs 3): a==b, a!=c, b!=c
        assert_eq!(
            filesizes_three_different_p(&a, &b, &c).unwrap(),
            (false, true, true)
        );

        // A second arrangement so no single constant tuple satisfies both
        // assertions. Tuple is (f1!=f2, f2!=f3, f1!=f3); here a!=b, b==c, a!=c.
        std::fs::write(&b, "123456").unwrap();
        assert_eq!(
            filesizes_three_different_p(&a, &b, &c).unwrap(),
            (true, false, true)
        );
    }

    #[test]
    fn test_files_contents_same_p() {
        let td = tempfile::tempdir().unwrap();
        let a = td.path().join("a.txt");
        let b = td.path().join("b.txt");
        let c = td.path().join("c.txt");
        std::fs::write(&a, "same").unwrap();
        std::fs::write(&b, "same").unwrap();
        std::fs::write(&c, "diff").unwrap();
        assert!(files_contents_same_p(&a, &b).unwrap());
        assert!(!files_contents_same_p(&a, &c).unwrap());
    }

    #[test]
    fn test_files_contents_three_same_p() {
        let td = tempfile::tempdir().unwrap();
        let a = td.path().join("a.txt");
        let b = td.path().join("b.txt");
        let c = td.path().join("c.txt");
        std::fs::write(&a, "same").unwrap();
        std::fs::write(&b, "same").unwrap();
        std::fs::write(&c, "diff").unwrap();
        // (1 vs 2, 1 vs 3, 2 vs 3): a==b, a!=c, b!=c
        assert_eq!(
            files_contents_three_same_p(&a, &b, &c).unwrap(),
            (true, false, false)
        );

        // A second arrangement so no single constant tuple satisfies both
        // assertions. Tuple is (f1==f2, f2==f3, f1==f3); here a!=b, b==c, a!=c.
        std::fs::write(&b, "diff").unwrap();
        assert_eq!(
            files_contents_three_same_p(&a, &b, &c).unwrap(),
            (false, true, false)
        );
    }

    #[test]
    fn test_file_create() {
        let td = tempfile::tempdir().unwrap();
        let path = td.path().join("created.txt");
        file_create(&path, "contents here").unwrap();
        assert_eq!(std::fs::read_to_string(&path).unwrap(), "contents here");
    }

    #[test]
    fn test_file_create_bytes() {
        let td = tempfile::tempdir().unwrap();
        let path = td.path().join("created.bin");
        file_create_bytes(&path, b"\x00\x01\x02binary").unwrap();
        assert_eq!(std::fs::read(&path).unwrap(), b"\x00\x01\x02binary");
    }

    #[test]
    fn test_file_create_empty() {
        let td = tempfile::tempdir().unwrap();
        let path = td.path().join("empty.txt");
        file_create_empty(&path).unwrap();
        assert!(path.exists());
        assert_eq!(std::fs::metadata(&path).unwrap().len(), 0);
    }

    #[test]
    fn test_file_lock() {
        let td = tempfile::tempdir().unwrap();
        let path = td.path().join("lock.txt");
        std::fs::write(&path, "data").unwrap();
        file_lock(&path, true, false).unwrap();
    }

    #[test]
    fn test_dir_file_copy() {
        let td = tempfile::tempdir().unwrap();
        let src_dir = td.path().join("src");
        let dest_dir = td.path().join("dest");
        std::fs::create_dir(&src_dir).unwrap();
        std::fs::create_dir(&dest_dir).unwrap();
        std::fs::write(src_dir.join("file.txt"), "moved").unwrap();

        dir_file_copy(&src_dir, &dest_dir, std::ffi::OsStr::new("file.txt")).unwrap();
        assert_eq!(
            std::fs::read_to_string(dest_dir.join("file.txt")).unwrap(),
            "moved"
        );
    }

    #[test]
    fn test_remove_dir() {
        let td = tempfile::tempdir().unwrap();
        let dir = td.path().join("to_remove");
        std::fs::create_dir(&dir).unwrap();
        std::fs::write(dir.join("inner.txt"), "x").unwrap();
        let no_cancel: Option<&fn() -> Result<(), Error<'static>>> = None;
        remove_dir(&dir, false, no_cancel).unwrap();
        assert!(!dir.exists());
    }

    #[test]
    fn test_remove_dir_enoent() {
        let td = tempfile::tempdir().unwrap();
        let missing = td.path().join("never");
        let no_cancel: Option<&fn() -> Result<(), Error<'static>>> = None;
        remove_dir(&missing, true, no_cancel).unwrap();
        assert!(remove_dir(&missing, false, no_cancel).is_err());
    }

    #[test]
    fn test_copy_dir_recursively() {
        let td = tempfile::tempdir().unwrap();
        let src = td.path().join("src");
        std::fs::create_dir(&src).unwrap();
        std::fs::write(src.join("file.txt"), "nested content").unwrap();
        let dst_parent = td.path().join("dstparent");
        std::fs::create_dir(&dst_parent).unwrap();

        let no_cancel: Option<&fn() -> Result<(), Error<'static>>> = None;
        copy_dir_recursively(
            &src,
            &dst_parent,
            std::ffi::OsStr::new("copied"),
            false,
            no_cancel,
        )
        .unwrap();
        assert_eq!(
            std::fs::read_to_string(dst_parent.join("copied/file.txt")).unwrap(),
            "nested content"
        );
    }

    #[test]
    fn test_stream_copy() {
        let mut from = Stream::from("stream payload");
        let mut buf = StringBuf::new();
        let mut to = Stream::from_stringbuf(&mut buf);
        let no_cancel: Option<&fn() -> Result<(), Error<'static>>> = None;
        stream_copy(&mut from, &mut to, no_cancel).unwrap();
        drop(to);
        assert_eq!(buf.to_string(), "stream payload");
    }

    #[test]
    fn test_stream_contents_same() {
        let mut a = Stream::from("identical");
        let mut b = Stream::from("identical");
        assert!(stream_contents_same(&mut a, &mut b).unwrap());

        let mut c = Stream::from("identical");
        let mut d = Stream::from("different");
        assert!(!stream_contents_same(&mut c, &mut d).unwrap());
    }

    #[test]
    fn test_string_from_stream() {
        let mut stream = Stream::from("read me back");
        assert_eq!(string_from_stream(&mut stream).unwrap(), "read me back");
    }

    #[test]
    fn test_read_stream_to_string() {
        let mut stream = Stream::from("via read_stream_to_string");
        assert_eq!(
            read_stream_to_string(&mut stream).unwrap(),
            "via read_stream_to_string"
        );
    }

    #[test]
    #[cfg(unix)]
    fn test_create_uniqe_link_and_read_link() {
        let td = tempfile::tempdir().unwrap();
        let target = td.path().join("target.txt");
        std::fs::write(&target, "linked").unwrap();
        let link_base = td.path().join("mylink");

        let link = create_uniqe_link(&link_base, &target, std::ffi::OsStr::new(".tmp")).unwrap();
        let read_target = read_link(&link).unwrap();
        assert_eq!(read_target, target);
    }

    #[test]
    #[cfg(unix)]
    fn test_copy_link() {
        let td = tempfile::tempdir().unwrap();
        let target = td.path().join("target.txt");
        std::fs::write(&target, "data").unwrap();
        let src_link = td.path().join("src_link");
        std::os::unix::fs::symlink(&target, &src_link).unwrap();
        let dest_link = td.path().join("dest_link");

        copy_link(&src_link, &dest_link).unwrap();
        assert_eq!(std::fs::read_link(&dest_link).unwrap(), target);
    }

    #[test]
    fn test_file_del_conversions() {
        // Each variant maps to a distinct svn constant and round-trips back.
        for variant in [FileDel::None, FileDel::OnClose, FileDel::OnPoolCleanup] {
            let raw: subversion_sys::svn_io_file_del_t = variant.into();
            assert_eq!(FileDel::from(raw), variant);
        }

        // Pin the mapping to the actual svn constants so the forward arms can't
        // be swapped without detection.
        assert_eq!(
            subversion_sys::svn_io_file_del_t::from(FileDel::None),
            subversion_sys::svn_io_file_del_t_svn_io_file_del_none
        );
        assert_eq!(
            subversion_sys::svn_io_file_del_t::from(FileDel::OnClose),
            subversion_sys::svn_io_file_del_t_svn_io_file_del_on_close
        );
        assert_eq!(
            subversion_sys::svn_io_file_del_t::from(FileDel::OnPoolCleanup),
            subversion_sys::svn_io_file_del_t_svn_io_file_del_on_pool_cleanup
        );
    }

    #[test]
    fn test_stringbuf_is_empty() {
        assert!(StringBuf::new().is_empty());
        assert!(!StringBuf::try_from("x").unwrap().is_empty());
    }

    #[test]
    fn test_stream_data_available() {
        let mut stream = Stream::from("payload");
        assert!(stream.data_available().unwrap());

        // Drain the stream, then no data is available.
        let mut buf = vec![0u8; 7];
        stream.read_full(&mut buf).unwrap();
        assert!(!stream.data_available().unwrap());
    }

    #[test]
    fn test_stream_readline_multiple() {
        let mut stream = Stream::from("first line\nsecond line\n");
        assert_eq!(
            stream.readline("\n").unwrap(),
            Some("first line".to_string())
        );
        assert_eq!(
            stream.readline("\n").unwrap(),
            Some("second line".to_string())
        );
        // String-backed streams keep returning Some("") at EOF rather than None,
        // so assert the empty-string tail rather than None here.
        assert_eq!(stream.readline("\n").unwrap(), Some(String::new()));
    }

    #[test]
    fn test_streams_contents_same() {
        let mut a = Stream::from("identical bytes");
        let mut b = Stream::from("identical bytes");
        assert!(streams_contents_same(&mut a, &mut b).unwrap());

        let mut c = Stream::from("identical bytes");
        let mut d = Stream::from("different bytes");
        assert!(!streams_contents_same(&mut c, &mut d).unwrap());
    }

    #[test]
    fn test_copy_stream() {
        let mut from = Stream::from("copy_stream payload");
        let mut buf = StringBuf::new();
        let mut to = Stream::from_stringbuf(&mut buf);
        let no_cancel: Option<&fn() -> Result<(), Error<'static>>> = None;
        copy_stream(&mut from, &mut to, no_cancel).unwrap();
        drop(to);
        assert_eq!(buf.to_string(), "copy_stream payload");
    }

    #[test]
    fn test_stream_printf_from_utf8() {
        let mut buf = StringBuf::new();
        let mut stream = Stream::from_stringbuf(&mut buf);
        stream.printf_from_utf8("value=%d", &[&7]).unwrap();
        drop(stream);
        assert_eq!(buf.to_string(), "value=7");
    }

    #[test]
    #[cfg(unix)]
    fn test_set_file_read_only() {
        use std::os::unix::fs::PermissionsExt;
        let td = tempfile::tempdir().unwrap();
        let path = td.path().join("ro.txt");
        std::fs::write(&path, "data").unwrap();
        std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o644)).unwrap();

        set_file_read_only(&path, false).unwrap();
        let mode = std::fs::metadata(&path).unwrap().permissions().mode();
        assert_eq!(mode & 0o200, 0, "write bit should be cleared");
    }

    #[test]
    fn test_stream_supports_mark() {
        // String-backed streams support marking; buffered streams do not.
        assert!(Stream::from("data").supports_mark());
        assert!(!Stream::buffered().supports_mark());
    }

    #[test]
    fn test_stream_supports_reset() {
        assert!(Stream::from("data").supports_reset());
        assert!(!Stream::buffered().supports_reset());
    }

    #[test]
    fn test_stream_supports_partial_read() {
        assert!(Stream::from("data").supports_partial_read());
        assert!(!Stream::buffered().supports_partial_read());
    }

    #[test]
    fn test_wrap_write() {
        let mut sink: Vec<u8> = Vec::new();
        {
            let mut stream = wrap_write(&mut sink).unwrap();
            stream.write(b"wrapped ").unwrap();
            stream.write(b"output").unwrap();
            stream.close().unwrap();
        }
        assert_eq!(sink, b"wrapped output");
    }

    #[test]
    fn test_wrap_write_close_propagates_flush_error() {
        // A writer whose flush() always fails; close_fn must surface that error.
        struct FailFlush;
        impl std::io::Write for FailFlush {
            fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
                Ok(buf.len())
            }
            fn flush(&mut self) -> std::io::Result<()> {
                Err(std::io::Error::other("flush boom"))
            }
        }

        let mut writer = FailFlush;
        let mut stream = wrap_write(&mut writer).unwrap();
        let result = stream.close();
        assert!(
            result.is_err(),
            "close() should propagate the flush error from close_fn"
        );
    }

    #[test]
    fn test_sleep_for_timestamps() {
        let td = tempfile::tempdir().unwrap();
        let path = td.path().join("file.txt");
        std::fs::write(&path, "data").unwrap();
        // Just verify it runs without panicking; the sleep is short for files
        // on a filesystem with sub-second timestamp resolution.
        sleep_for_timestamps(&path);
    }
}
