//! Repository Access (RA) layer for network operations.
//!
//! This module provides the [`Session`](crate::ra::Session) type for accessing Subversion repositories over
//! various network protocols (http, https, svn, svn+ssh, file).
//!
//! # Overview
//!
//! The RA layer is the network abstraction in Subversion. It handles communication with
//! remote repositories and provides operations for fetching repository data, querying
//! metadata, and retrieving history.
//!
//! ## Key Operations
//!
//! - **Repository querying**: Get latest revision, check paths, read directories
//! - **History access**: Retrieve log entries and file contents at specific revisions
//! - **Property access**: Get revision properties and file properties
//! - **Location tracking**: Find where paths existed across revisions
//! - **Lock management**: Query and manipulate repository locks
//! - **Mergeinfo**: Query merge tracking information
//!
//! # Example
//!
//! ```no_run
//! use subversion::ra::Session;
//!
//! let mut session = Session::open("https://svn.example.com/repo").unwrap();
//! let latest = session.get_latest_revnum().unwrap();
//! println!("Latest revision: {}", latest);
//! ```

use crate::{svn_result, with_tmp_pool, Depth, Error, OwnedLogEntry, Revnum};
use apr::pool::Pool;
use std::collections::HashMap;
use std::marker::PhantomData;
use subversion_sys::svn_ra_session_t;

/// Callback for receiving txdelta windows from [`Session::get_file_revs`].
///
/// Called with `Some(window)` for each delta window, and `None` to signal
/// end-of-delta. The handler is freed automatically after the final `None` call.
pub type TxDeltaHandler =
    Box<dyn FnMut(Option<&crate::delta::TxDeltaWindowRef<'_>>) -> Result<(), Error<'static>>>;

/// A canonical relative path for use with RA (Repository Access) functions.
///
/// This type ensures that paths are properly formatted for SVN's RA layer,
/// which expects relative paths without leading slashes.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RelPath(String);

impl RelPath {
    /// Create a new RelPath from a string, canonicalizing it
    pub fn canonicalize(path: &str) -> Result<Self, Error<'static>> {
        // Remove leading slash if present
        let path = path.strip_prefix('/').unwrap_or(path);

        with_tmp_pool(|pool| unsafe {
            let path_cstr = std::ffi::CString::new(path)?;
            let canonical =
                subversion_sys::svn_relpath_canonicalize(path_cstr.as_ptr(), pool.as_mut_ptr());
            let canonical_cstr = std::ffi::CStr::from_ptr(canonical);
            Ok(RelPath(canonical_cstr.to_str()?.to_owned()))
        })
    }

    /// Create a RelPath from an already-canonical string (unchecked)
    pub fn from_canonical(path: String) -> Result<Self, Error<'static>> {
        // Verify it's actually canonical
        let is_canonical = with_tmp_pool(|_pool| unsafe {
            let path_cstr = std::ffi::CString::new(path.as_str())?;
            Ok::<bool, Error>(subversion_sys::svn_relpath_is_canonical(path_cstr.as_ptr()) != 0)
        })?;

        if !is_canonical {
            return Err(Error::from_message(&format!(
                "Relative path is not canonical: {}",
                path
            )));
        }

        Ok(RelPath(path))
    }

    /// Get the path as a string slice
    pub fn as_str(&self) -> &str {
        &self.0
    }
}

impl TryFrom<&str> for RelPath {
    type Error = Error<'static>;

    fn try_from(path: &str) -> Result<Self, Self::Error> {
        Self::from_canonical(path.to_string())
    }
}

impl TryFrom<String> for RelPath {
    type Error = Error<'static>;

    fn try_from(path: String) -> Result<Self, Self::Error> {
        Self::from_canonical(path)
    }
}

impl TryFrom<&String> for RelPath {
    type Error = Error<'static>;

    fn try_from(path: &String) -> Result<Self, Self::Error> {
        Self::from_canonical(path.clone())
    }
}

impl<'a> TryFrom<&'a &str> for RelPath {
    type Error = Error<'static>;

    fn try_from(path: &'a &str) -> Result<Self, Self::Error> {
        Self::from_canonical((*path).to_string())
    }
}

impl std::fmt::Display for RelPath {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        self.0.fmt(f)
    }
}

/// Repository information struct
#[derive(Debug, Clone)]
pub struct RepositoryInfo {
    /// Repository UUID.
    pub uuid: String,
    /// Repository root URL.
    pub root_url: String,
    /// Latest revision in the repository.
    pub latest_revision: Revnum,
    /// Current session URL.
    pub session_url: String,
}

// Removed global vtable storage - using simpler approach

/// Directory entry information from the repository access layer.
///
/// Re-exported from [`crate::DirEntry`].
pub use crate::DirEntry as Dirent;

/// Repository access session handle with automatic cleanup.
pub struct Session<'a> {
    ptr: *mut svn_ra_session_t,
    _pool: apr::Pool<'static>,
    // Keep a reference to callbacks to ensure they outlive the session
    _callbacks: Option<&'a mut Callbacks>,
    // Own default callbacks when user doesn't provide them
    _owned_callbacks: Option<Box<Callbacks>>,
    _phantom: PhantomData<*mut ()>, // !Send + !Sync
}

impl<'a> Drop for Session<'a> {
    fn drop(&mut self) {
        // Pool drop will clean up session
    }
}

pub(crate) extern "C" fn wrap_dirent_receiver(
    rel_path: *const std::os::raw::c_char,
    dirent: *mut subversion_sys::svn_dirent_t,
    baton: *mut std::os::raw::c_void,
    _pool: *mut apr_sys::apr_pool_t,
) -> *mut subversion_sys::svn_error_t {
    let rel_path = unsafe { std::ffi::CStr::from_ptr(rel_path) };
    // baton is a pointer to a reference to the trait object
    let callback = unsafe {
        &*(baton as *const _ as *const &dyn Fn(&str, &Dirent) -> Result<(), crate::Error<'static>>)
    };
    match callback(rel_path.to_str().unwrap(), &Dirent::from_raw(dirent)) {
        Ok(()) => std::ptr::null_mut(),
        Err(mut e) => e.as_mut_ptr(),
    }
}

extern "C" fn wrap_location_segment_receiver(
    svn_location_segment: *mut subversion_sys::svn_location_segment_t,
    baton: *mut std::os::raw::c_void,
    _pool: *mut apr_sys::apr_pool_t,
) -> *mut subversion_sys::svn_error_t {
    let baton = unsafe {
        &*(baton as *const _
            as *const &dyn Fn(&crate::LocationSegment) -> Result<(), crate::Error<'static>>)
    };
    match baton(&crate::LocationSegment {
        ptr: svn_location_segment,
        _pool: std::marker::PhantomData,
    }) {
        Ok(()) => std::ptr::null_mut(),
        Err(mut e) => e.as_mut_ptr(),
    }
}

extern "C" fn wrap_lock_func(
    lock_baton: *mut std::os::raw::c_void,
    path: *const std::os::raw::c_char,
    do_lock: i32,
    lock: *const subversion_sys::svn_lock_t,
    error: *mut subversion_sys::svn_error_t,
    pool: *mut apr_sys::apr_pool_t,
) -> *mut subversion_sys::svn_error_t {
    let lock_baton = unsafe {
        // Unbox the reference like get_log does
        &mut **(lock_baton
            as *mut &mut dyn FnMut(
                &str,
                bool,
                Option<&crate::Lock>,
                Option<&Error>,
            ) -> Result<(), Error<'static>>)
    };
    let path = unsafe { std::ffi::CStr::from_ptr(path) };

    let error = Error::from_raw(error).err();

    let lock_obj = if lock.is_null() {
        None
    } else {
        let pool_handle = unsafe { apr::PoolHandle::from_borrowed_raw(pool) };
        Some(crate::Lock::from_raw(lock as *mut _, pool_handle))
    };

    match lock_baton(
        path.to_str().unwrap(),
        do_lock != 0,
        lock_obj.as_ref(),
        error.as_ref(),
    ) {
        Ok(()) => std::ptr::null_mut(),
        Err(e) => unsafe { e.into_raw() },
    }
}

/// Options for the do_diff operation
pub struct DoDiffOptions<'a, 'pool> {
    /// Depth of the diff operation.
    pub depth: crate::Depth,
    /// Whether to ignore ancestry.
    pub ignore_ancestry: bool,
    /// Whether to include text deltas.
    pub text_deltas: bool,
    /// URL to compare against.
    pub versus_url: &'a str,
    /// Editor to receive diff callbacks.
    ///
    /// The editor's own lifetime (`'pool`) is independent of the borrow
    /// (`'a`), so holding these options does not pin the editor for its whole
    /// lifetime.
    pub diff_editor: &'a mut crate::delta::WrapEditor<'pool>,
}

impl<'a, 'pool> DoDiffOptions<'a, 'pool> {
    /// Creates new diff options with default settings.
    pub fn new(versus_url: &'a str, diff_editor: &'a mut crate::delta::WrapEditor<'pool>) -> Self {
        Self {
            depth: crate::Depth::Infinity,
            ignore_ancestry: false,
            text_deltas: true,
            versus_url,
            diff_editor,
        }
    }

    /// Sets the depth for the diff operation.
    pub fn with_depth(mut self, depth: crate::Depth) -> Self {
        self.depth = depth;
        self
    }

    /// Sets whether to ignore ancestry when diffing.
    pub fn with_ignore_ancestry(mut self, ignore_ancestry: bool) -> Self {
        self.ignore_ancestry = ignore_ancestry;
        self
    }

    /// Sets whether to include text deltas.
    pub fn with_text_deltas(mut self, text_deltas: bool) -> Self {
        self.text_deltas = text_deltas;
        self
    }
}

/// Options for the get_log operation.
#[derive(Default)]
pub struct GetLogOptions<'a> {
    /// Maximum number of log entries to return (0 for unlimited).
    pub limit: usize,
    /// Whether to discover changed paths.
    pub discover_changed_paths: bool,
    /// Whether to use strict node history.
    pub strict_node_history: bool,
    /// Whether to include merged revisions.
    pub include_merged_revisions: bool,
    /// The revision properties to retrieve.
    /// `None` means return all revprops, `Some(&[])` means return none.
    pub revprops: Option<&'a [&'a str]>,
}

impl<'a> GetLogOptions<'a> {
    /// Sets the maximum number of log entries to return.
    pub fn with_limit(mut self, limit: usize) -> Self {
        self.limit = limit;
        self
    }

    /// Sets whether to discover changed paths.
    pub fn with_discover_changed_paths(mut self, discover: bool) -> Self {
        self.discover_changed_paths = discover;
        self
    }

    /// Sets whether to use strict node history.
    pub fn with_strict_node_history(mut self, strict: bool) -> Self {
        self.strict_node_history = strict;
        self
    }

    /// Sets whether to include merged revisions.
    pub fn with_include_merged_revisions(mut self, include: bool) -> Self {
        self.include_merged_revisions = include;
        self
    }

    /// Sets the revision properties to retrieve.
    /// `None` means return all revprops, `Some(&[])` means return none.
    pub fn with_revprops(mut self, revprops: Option<&'a [&'a str]>) -> Self {
        self.revprops = revprops;
        self
    }
}

/// A streaming iterator over log entries from a repository access session.
///
/// Receives entries lazily from a worker thread that runs `get_log()` internally.
/// Dropping the iterator cancels any remaining log retrieval and joins the
/// worker thread.
pub struct RaLogIterator<'a> {
    rx: std::sync::mpsc::Receiver<Result<OwnedLogEntry, Error<'static>>>,
    handle: Option<std::thread::JoinHandle<()>>,
    _phantom: PhantomData<&'a mut Session<'a>>,
}

impl Iterator for RaLogIterator<'_> {
    type Item = Result<OwnedLogEntry, Error<'static>>;

    fn next(&mut self) -> Option<Self::Item> {
        self.rx.recv().ok()
    }
}

impl Drop for RaLogIterator<'_> {
    fn drop(&mut self) {
        // Drop the receiver to signal cancellation to the worker thread
        drop(std::mem::replace(
            &mut self.rx,
            std::sync::mpsc::sync_channel(0).1,
        ));
        // Wait for the worker thread to finish, ensuring the &mut Session
        // borrow is released before this iterator's lifetime ends
        if let Some(handle) = self.handle.take() {
            let _ = handle.join();
        }
    }
}

impl<'a> Session<'a> {
    /// Creates a Session from a raw pointer and pool.
    #[cfg(feature = "client")]
    pub(crate) unsafe fn from_ptr_and_pool(
        ptr: *mut svn_ra_session_t,
        pool: apr::Pool<'static>,
    ) -> Self {
        Self {
            ptr,
            _pool: pool,
            _callbacks: None,
            _owned_callbacks: None,
            _phantom: PhantomData,
        }
    }

    /// Returns a raw pointer to the session.
    pub fn as_ptr(&self) -> *const svn_ra_session_t {
        self.ptr
    }

    /// Returns a mutable raw pointer to the session.
    pub fn as_mut_ptr(&mut self) -> *mut svn_ra_session_t {
        self.ptr
    }

    /// Opens a repository access session to a URL.
    pub fn open(
        url: &str,
        uuid: Option<&str>,
        mut callbacks: Option<&'a mut Callbacks>,
        mut config: Option<&mut crate::config::ConfigHash>,
    ) -> Result<(Self, Option<String>, Option<String>), Error<'static>> {
        // Ensure SVN libraries are initialized
        crate::init::initialize()?;

        let url = crate::uri::canonicalize_uri(url)?;
        let url = std::ffi::CString::new(url.as_str()).unwrap();
        let mut corrected_url = std::ptr::null();
        let mut redirect_url = std::ptr::null();
        let pool = Pool::new();
        let mut session = std::ptr::null_mut();
        let uuid = uuid.map(|uuid| std::ffi::CString::new(uuid).unwrap());

        // Create default callbacks if none provided - SVN requires valid callbacks
        let mut owned_callbacks = None;
        let (callbacks_ptr, callback_baton) = if let Some(callbacks) = callbacks.as_mut() {
            (callbacks.as_mut_ptr(), callbacks.get_callback_baton())
        } else {
            let mut default_callbacks = Box::new(Callbacks::new()?);
            let ptr = default_callbacks.as_mut_ptr();
            let baton = default_callbacks.get_callback_baton();
            owned_callbacks = Some(default_callbacks);
            (ptr, baton)
        };

        let err = unsafe {
            subversion_sys::svn_ra_open5(
                &mut session,
                &mut corrected_url,
                &mut redirect_url,
                url.as_ptr(),
                if let Some(uuid) = uuid {
                    uuid.as_ptr()
                } else {
                    std::ptr::null()
                },
                callbacks_ptr,
                callback_baton,
                if let Some(config) = config.as_mut() {
                    config.as_mut_ptr()
                } else {
                    std::ptr::null_mut()
                },
                pool.as_mut_ptr(),
            )
        };
        Error::from_raw(err)?;
        Ok((
            Session {
                ptr: session,
                _pool: pool,
                _callbacks: callbacks,
                _owned_callbacks: owned_callbacks,
                _phantom: PhantomData,
            },
            if corrected_url.is_null() {
                None
            } else {
                Some(
                    unsafe { std::ffi::CStr::from_ptr(corrected_url) }
                        .to_str()
                        .unwrap()
                        .to_string(),
                )
            },
            if redirect_url.is_null() {
                None
            } else {
                Some(
                    unsafe { std::ffi::CStr::from_ptr(redirect_url) }
                        .to_str()
                        .unwrap()
                        .to_string(),
                )
            },
        ))
    }

    /// Changes the session to point to a different URL.
    pub fn reparent(&mut self, url: &str) -> Result<(), Error<'static>> {
        let url = crate::uri::canonicalize_uri(url)?;
        let url = std::ffi::CString::new(url.as_str()).unwrap();
        with_tmp_pool(|pool| {
            let err = unsafe {
                subversion_sys::svn_ra_reparent(self.ptr, url.as_ptr(), pool.as_mut_ptr())
            };
            Error::from_raw(err)
        })
    }

    /// Gets the current session URL.
    pub fn get_session_url(&mut self) -> Result<String, Error<'static>> {
        with_tmp_pool(|pool| {
            let mut url = std::ptr::null();
            let err = unsafe {
                subversion_sys::svn_ra_get_session_url(self.ptr, &mut url, pool.as_mut_ptr())
            };
            Error::from_raw(err)?;
            let url = unsafe { std::ffi::CStr::from_ptr(url) };
            Ok(url.to_string_lossy().into_owned())
        })
    }

    /// Gets the path relative to the session URL.
    pub fn get_path_relative_to_session(&mut self, url: &str) -> Result<String, Error<'static>> {
        let url = std::ffi::CString::new(url).unwrap();
        let pool = Pool::new();
        let mut path = std::ptr::null();
        let err = unsafe {
            subversion_sys::svn_ra_get_path_relative_to_session(
                self.ptr,
                &mut path,
                url.as_ptr(),
                pool.as_mut_ptr(),
            )
        };
        Error::from_raw(err)?;
        let path = unsafe { std::ffi::CStr::from_ptr(path) };
        Ok(path.to_string_lossy().into_owned())
    }

    /// Gets the path relative to the repository root.
    pub fn get_path_relative_to_root(&mut self, url: &str) -> String {
        let url = std::ffi::CString::new(url).unwrap();
        let pool = Pool::new();
        let mut path = std::ptr::null();
        unsafe {
            subversion_sys::svn_ra_get_path_relative_to_root(
                self.ptr,
                &mut path,
                url.as_ptr(),
                pool.as_mut_ptr(),
            );
        }
        let path = unsafe { std::ffi::CStr::from_ptr(path) };
        path.to_string_lossy().into_owned()
    }

    /// Gets the latest revision number.
    pub fn get_latest_revnum(&mut self) -> Result<Revnum, Error<'static>> {
        with_tmp_pool(|pool| {
            let mut revnum = 0;
            let err = unsafe {
                subversion_sys::svn_ra_get_latest_revnum(self.ptr, &mut revnum, pool.as_mut_ptr())
            };
            Error::from_raw(err)?;
            Ok(Revnum::from_raw(revnum).unwrap())
        })
    }

    /// Gets the revision at a specific time.
    pub fn get_dated_revision(&mut self, tm: apr::apr_time_t) -> Result<Revnum, Error<'static>> {
        with_tmp_pool(|pool| {
            let mut revnum = 0;
            let err = unsafe {
                subversion_sys::svn_ra_get_dated_revision(
                    self.ptr,
                    &mut revnum,
                    tm,
                    pool.as_mut_ptr(),
                )
            };
            Error::from_raw(err)?;
            Ok(Revnum::from_raw(revnum).unwrap())
        })
    }

    /// Changes a revision property.
    pub fn change_revprop(
        &mut self,
        rev: Revnum,
        name: &str,
        old_value: Option<&[u8]>,
        new_value: &[u8],
    ) -> Result<(), Error<'static>> {
        let name = std::ffi::CString::new(name).unwrap();
        let pool = Pool::new();
        let new_value = subversion_sys::svn_string_t {
            data: new_value.as_ptr() as *mut _,
            len: new_value.len(),
        };
        // old_value_p is `const svn_string_t **`: NULL means "no atomicity
        // check", while a non-NULL pointer to a NULL inner pointer means "assert
        // the property currently has no value". Keep both the svn_string_t and
        // the inner pointer in locals so they outlive the call.
        let old_value = old_value.map(|v| subversion_sys::svn_string_t {
            data: v.as_ptr() as *mut _,
            len: v.len(),
        });
        let old_value_inner: *const subversion_sys::svn_string_t = old_value
            .as_ref()
            .map_or(std::ptr::null(), |v| v as *const _);
        let old_value_p = if old_value.is_some() {
            &old_value_inner as *const *const subversion_sys::svn_string_t
        } else {
            std::ptr::null()
        };
        let err = unsafe {
            subversion_sys::svn_ra_change_rev_prop2(
                self.ptr,
                rev.into(),
                name.as_ptr(),
                old_value_p,
                &new_value,
                pool.as_mut_ptr(),
            )
        };
        Error::from_raw(err)?;
        Ok(())
    }

    /// Gets the property list for a revision.
    pub fn rev_proplist(&mut self, rev: Revnum) -> Result<HashMap<String, Vec<u8>>, Error<'_>> {
        if rev.0 < 0 {
            return Err(Error::with_raw_status(
                subversion_sys::svn_errno_t_SVN_ERR_CLIENT_BAD_REVISION as i32,
                None,
                "Invalid revision number",
            ));
        }
        let pool = Pool::new();
        let mut props = std::ptr::null_mut();
        let err = unsafe {
            subversion_sys::svn_ra_rev_proplist(self.ptr, rev.into(), &mut props, pool.as_mut_ptr())
        };
        Error::from_raw(err)?;
        let prop_hash = unsafe { crate::props::PropHash::from_ptr(props) };
        Ok(prop_hash.to_hashmap())
    }

    /// Gets a specific property for a revision.
    pub fn rev_prop(&mut self, rev: Revnum, name: &str) -> Result<Option<Vec<u8>>, Error<'_>> {
        let name = std::ffi::CString::new(name).unwrap();
        let pool = Pool::new();
        let mut value = std::ptr::null_mut();
        let err = unsafe {
            subversion_sys::svn_ra_rev_prop(
                self.ptr,
                rev.into(),
                name.as_ptr(),
                &mut value,
                pool.as_mut_ptr(),
            )
        };
        Error::from_raw(err)?;
        if value.is_null() {
            Ok(None)
        } else {
            Ok(Some(Vec::from(unsafe {
                std::slice::from_raw_parts((*value).data as *const u8, (*value).len)
            })))
        }
    }

    /// Gets a commit editor for making changes to the repository.
    ///
    /// The returned editor borrows from the session and must not outlive it.
    pub fn get_commit_editor<'s>(
        &'s mut self,
        revprop_table: HashMap<String, Vec<u8>>,
        commit_callback: &dyn FnMut(&crate::CommitInfo) -> Result<(), Error<'static>>,
        lock_tokens: HashMap<String, String>,
        keep_locks: bool,
    ) -> Result<crate::delta::WrapEditor<'s>, Error<'s>> {
        // Create the result pool first - this will live as long as the editor
        let result_pool = Pool::new();
        let commit_callback = Box::into_raw(Box::new(commit_callback));
        let mut editor = std::ptr::null();
        let mut edit_baton = std::ptr::null_mut();
        // Create svn_string_t values and copy keys into result_pool so they live as long as the editor
        let svn_strings_and_keys: Vec<_> = revprop_table
            .iter()
            .map(|(k, v)| {
                let key_cstr = std::ffi::CString::new(k.as_str()).unwrap();
                // Copy key into result_pool so it lives as long as the editor
                let key_in_pool =
                    unsafe { apr_sys::apr_pstrdup(result_pool.as_mut_ptr(), key_cstr.as_ptr()) };
                let key_slice = unsafe { std::ffi::CStr::from_ptr(key_in_pool).to_bytes() };
                (
                    key_slice,
                    crate::string::BStr::from_bytes(v.as_slice(), &result_pool),
                )
            })
            .collect();

        let mut hash_revprop_table = apr::hash::Hash::new(&result_pool);
        for (key_slice, v) in svn_strings_and_keys.iter() {
            unsafe {
                hash_revprop_table.insert(key_slice, v.as_ptr() as *mut std::ffi::c_void);
            }
        }

        // Create C strings for values and copy keys into result_pool
        let c_strings: Vec<_> = lock_tokens
            .iter()
            .map(|(k, v)| {
                let key_cstr = std::ffi::CString::new(k.as_str()).unwrap();
                let value_cstr = std::ffi::CString::new(v.as_str()).unwrap();
                // Copy key into result_pool so it lives as long as the editor
                let key_in_pool =
                    unsafe { apr_sys::apr_pstrdup(result_pool.as_mut_ptr(), key_cstr.as_ptr()) };
                let key_slice = unsafe { std::ffi::CStr::from_ptr(key_in_pool).to_bytes() };
                // Copy value into result_pool too
                let value_in_pool =
                    unsafe { apr_sys::apr_pstrdup(result_pool.as_mut_ptr(), value_cstr.as_ptr()) };
                (key_slice, value_in_pool)
            })
            .collect();

        let mut hash_lock_tokens = apr::hash::Hash::new(&result_pool);
        for (k, v) in c_strings.iter() {
            unsafe {
                hash_lock_tokens.insert(k, *v as *mut std::ffi::c_void);
            }
        }
        let err = unsafe {
            subversion_sys::svn_ra_get_commit_editor3(
                self.ptr,
                &mut editor,
                &mut edit_baton,
                hash_revprop_table.as_mut_ptr(),
                Some(crate::wrap_commit_callback2),
                commit_callback as *mut _ as *mut _,
                hash_lock_tokens.as_mut_ptr(),
                keep_locks.into(),
                result_pool.as_mut_ptr(),
            )
        };
        unsafe fn drop_commit_callback_baton(baton: *mut std::ffi::c_void) {
            drop(Box::from_raw(
                baton as *mut &dyn FnMut(&crate::CommitInfo) -> Result<(), Error<'static>>,
            ));
        }

        Error::from_raw(err)?;
        Ok(crate::delta::WrapEditor {
            editor,
            baton: edit_baton,
            _pool: apr::PoolHandle::owned(result_pool),
            callback_batons: vec![(
                commit_callback as *mut std::ffi::c_void,
                drop_commit_callback_baton,
            )],
        })
    }

    /// Gets a file from the repository.
    pub fn get_file(
        &mut self,
        path: impl TryInto<RelPath, Error = Error<'static>>,
        rev: Revnum,
        stream: &mut crate::io::Stream,
    ) -> Result<(Option<Revnum>, HashMap<String, Vec<u8>>), Error<'static>> {
        let relpath = path.try_into()?;
        let path = std::ffi::CString::new(relpath.as_str()).unwrap();
        let pool = Pool::new();
        let mut props = std::ptr::null_mut();
        let mut fetched_rev = 0;
        let err = unsafe {
            subversion_sys::svn_ra_get_file(
                self.ptr,
                path.as_ptr(),
                rev.into(),
                stream.as_mut_ptr(),
                &mut fetched_rev,
                &mut props,
                pool.as_mut_ptr(),
            )
        };
        crate::Error::from_raw(err)?;
        let prop_hash = unsafe { crate::props::PropHash::from_ptr(props) };

        // The C API only sets fetched_rev if SVN_INVALID_REVNUM was passed.
        // If a specific revision was requested, fetched_rev remains 0.
        let actual_fetched_rev = if fetched_rev == 0 {
            None
        } else {
            Revnum::from_raw(fetched_rev)
        };

        Ok((actual_fetched_rev, prop_hash.to_hashmap()))
    }

    /// Gets a directory listing from the repository.
    pub fn get_dir(
        &mut self,
        path: impl TryInto<RelPath, Error = Error<'static>>,
        rev: Revnum,
        dirent_fields: crate::DirentField,
    ) -> Result<(Revnum, HashMap<String, Dirent>, HashMap<String, Vec<u8>>), Error<'_>> {
        let relpath = path.try_into()?;
        let path = std::ffi::CString::new(relpath.as_str()).unwrap();
        let pool = Pool::new();
        let mut props = std::ptr::null_mut();
        // Initialize fetched_rev to the requested revision. SVN will update it if needed,
        // but may leave it unchanged if the revision is explicit.
        let mut fetched_rev = rev.0;
        let mut dirents = std::ptr::null_mut();
        let dirent_fields = dirent_fields.bits();
        let err = unsafe {
            subversion_sys::svn_ra_get_dir2(
                self.ptr,
                &mut dirents,
                &mut fetched_rev,
                &mut props,
                path.as_ptr(),
                rev.into(),
                dirent_fields,
                pool.as_mut_ptr(),
            )
        };
        crate::Error::from_raw(err)?;
        let prop_hash = unsafe { crate::props::PropHash::from_ptr(props) };
        let dirents_hash = unsafe { crate::hash::DirentHash::from_ptr(dirents) };
        let props = prop_hash.to_hashmap();
        let dirents = dirents_hash.to_hashmap();
        Ok((Revnum::from_raw(fetched_rev).unwrap(), dirents, props))
    }

    /// Lists entries in a directory.
    pub fn list(
        &mut self,
        path: &str,
        rev: Revnum,
        patterns: Option<&[&str]>,
        depth: Depth,
        dirent_fields: crate::DirentField,
        dirent_receiver: &dyn Fn(&str, &Dirent) -> Result<(), crate::Error<'static>>,
    ) -> Result<(), Error<'static>> {
        let path = std::ffi::CString::new(path).unwrap();
        let pool = Pool::new();

        // Convert patterns to CStrings and keep them alive
        let pattern_cstrings: Option<Vec<std::ffi::CString>> = patterns.map(|patterns| {
            patterns
                .iter()
                .map(|p| std::ffi::CString::new(*p).unwrap())
                .collect()
        });

        let patterns_array: Option<apr::tables::TypedArray<*const std::os::raw::c_char>> =
            pattern_cstrings.as_ref().map(|cstrings| {
                let mut array = apr::tables::TypedArray::<*const std::os::raw::c_char>::new(
                    &pool,
                    cstrings.len() as i32,
                );
                for cstring in cstrings {
                    array.push(cstring.as_ptr());
                }
                array
            });
        let dirent_fields = dirent_fields.bits();
        let err = unsafe {
            subversion_sys::svn_ra_list(
                self.ptr,
                path.as_ptr(),
                rev.into(),
                if let Some(array) = patterns_array.as_ref() {
                    array.as_ptr()
                } else {
                    std::ptr::null()
                },
                depth.into(),
                dirent_fields,
                Some(wrap_dirent_receiver),
                &dirent_receiver as *const _ as *mut _,
                pool.as_mut_ptr(),
            )
        };
        Error::from_raw(err)?;
        Ok(())
    }

    /// Gets merge information for paths.
    pub fn get_mergeinfo(
        &mut self,
        paths: &[&str],
        revision: Revnum,
        inherit: crate::mergeinfo::MergeinfoInheritance,
        include_descendants: bool,
    ) -> Result<HashMap<String, crate::mergeinfo::Mergeinfo>, Error<'_>> {
        let pool = Pool::new();
        let c_paths: Vec<std::ffi::CString> = paths
            .iter()
            .map(|p| std::ffi::CString::new(*p).unwrap())
            .collect();
        let mut paths_array =
            apr::tables::TypedArray::<*const std::os::raw::c_char>::new(&pool, paths.len() as i32);
        for c_path in &c_paths {
            paths_array.push(c_path.as_ptr());
        }
        let paths = paths_array;
        let mut mergeinfo = std::ptr::null_mut();
        let err = unsafe {
            subversion_sys::svn_ra_get_mergeinfo(
                self.ptr,
                &mut mergeinfo,
                paths.as_ptr(),
                revision.into(),
                inherit.into(),
                include_descendants.into(),
                pool.as_mut_ptr(),
            )
        };
        Error::from_raw(err)?;
        if mergeinfo.is_null() {
            return Ok(HashMap::new());
        }
        let mergeinfo = unsafe { apr::hash::Hash::from_ptr(mergeinfo) };
        Ok(mergeinfo
            .iter()
            .map(|(k, v)| {
                // The value is already apr_hash_t* (svn_mergeinfo_t)
                (String::from_utf8_lossy(k).into_owned(), unsafe {
                    crate::mergeinfo::Mergeinfo::from_ptr_and_pool(
                        v as *mut apr_sys::apr_hash_t,
                        apr::Pool::new(),
                    )
                })
            })
            .collect())
    }

    /// Performs an update operation.
    ///
    /// The returned reporter borrows from the session and must not outlive it.
    pub fn do_update<'s>(
        &'s mut self,
        revision_to_update_to: Revnum,
        update_target: &str,
        depth: Depth,
        send_copyfrom_args: bool,
        ignore_ancestry: bool,
        editor: &mut crate::delta::WrapEditor,
    ) -> Result<Box<dyn Reporter + Send + 's>, Error<'s>> {
        let update_target = std::ffi::CString::new(update_target)?;
        let pool = Pool::new();
        let scratch_pool = Pool::new();
        let mut reporter = std::ptr::null();
        let mut report_baton = std::ptr::null_mut();
        let (editor_ptr, editor_baton) = editor.as_raw_parts();
        let err = unsafe {
            subversion_sys::svn_ra_do_update3(
                self.ptr,
                &mut reporter,
                &mut report_baton,
                revision_to_update_to.into(),
                update_target.as_ptr(),
                depth.into(),
                send_copyfrom_args.into(),
                ignore_ancestry.into(),
                editor_ptr,
                editor_baton,
                pool.as_mut_ptr(),
                scratch_pool.as_mut_ptr(),
            )
        };
        Error::from_raw(err)?;
        Ok(Box::new(WrapReporter {
            reporter,
            baton: report_baton,
            _pool: pool,
            callback_batons: Vec::new(),
            _phantom: PhantomData,
        }) as Box<dyn Reporter + Send>)
    }

    /// Performs a switch operation.
    ///
    /// The returned reporter borrows from the session and must not outlive it.
    pub fn do_switch<'s>(
        &'s mut self,
        revision_to_switch_to: Revnum,
        switch_target: &str,
        depth: Depth,
        switch_url: &str,
        send_copyfrom_args: bool,
        ignore_ancestry: bool,
        editor: &mut crate::delta::WrapEditor,
    ) -> Result<Box<dyn Reporter + Send + 's>, Error<'s>> {
        let switch_target = std::ffi::CString::new(switch_target)?;
        let switch_url = crate::uri::canonicalize_uri(switch_url)?;
        let switch_url = std::ffi::CString::new(switch_url.as_str())?;
        let pool = Pool::new();
        let scratch_pool = Pool::new();
        let mut reporter = std::ptr::null();
        let mut report_baton = std::ptr::null_mut();
        let (editor_ptr, editor_baton) = editor.as_raw_parts();
        let err = unsafe {
            subversion_sys::svn_ra_do_switch3(
                self.ptr,
                &mut reporter,
                &mut report_baton,
                revision_to_switch_to.into(),
                switch_target.as_ptr(),
                depth.into(),
                switch_url.as_ptr(),
                send_copyfrom_args.into(),
                ignore_ancestry.into(),
                editor_ptr,
                editor_baton,
                pool.as_mut_ptr(),
                scratch_pool.as_mut_ptr(),
            )
        };
        Error::from_raw(err)?;
        Ok(Box::new(WrapReporter {
            reporter,
            baton: report_baton,
            _pool: pool,
            callback_batons: Vec::new(),
            _phantom: PhantomData,
        }) as Box<dyn Reporter + Send>)
    }

    /// Checks the node kind of a path at a specific revision.
    pub fn check_path(
        &mut self,
        path: impl TryInto<RelPath, Error = Error<'static>>,
        rev: Revnum,
    ) -> Result<crate::NodeKind, Error<'static>> {
        let relpath = path.try_into()?;
        let path = std::ffi::CString::new(relpath.as_str()).unwrap();
        let mut kind = 0;
        let pool = Pool::new();
        let err = unsafe {
            subversion_sys::svn_ra_check_path(
                self.ptr,
                path.as_ptr(),
                rev.into(),
                &mut kind,
                pool.as_mut_ptr(),
            )
        };
        Error::from_raw(err)?;
        Ok(crate::NodeKind::from(kind))
    }

    /// Check if a path exists in the repository at the given revision
    ///
    /// This is a convenience method that uses check_path internally.
    /// Returns true if the path exists (is a file or directory), false otherwise.
    pub fn path_exists(
        &mut self,
        path: impl TryInto<RelPath, Error = Error<'static>>,
        rev: Revnum,
    ) -> Result<bool, Error<'static>> {
        let kind = self.check_path(path, rev)?;
        Ok(!matches!(kind, crate::NodeKind::None))
    }

    /// Performs a status operation.
    pub fn do_status(
        &mut self,
        status_target: impl TryInto<RelPath, Error = Error<'static>>,
        revision: Revnum,
        depth: Depth,
        status_editor: &mut crate::delta::WrapEditor,
    ) -> Result<(), Error<'static>> {
        let relpath = status_target.try_into()?;
        let status_target = std::ffi::CString::new(relpath.as_str()).unwrap();
        let pool = Pool::new();
        let mut reporter = std::ptr::null();
        let mut report_baton = std::ptr::null_mut();
        let (editor_ptr, editor_baton) = status_editor.as_raw_parts();
        let err = unsafe {
            subversion_sys::svn_ra_do_status2(
                self.ptr,
                &mut reporter,
                &mut report_baton,
                status_target.as_ptr() as *const _,
                revision.into(),
                depth.into(),
                editor_ptr,
                editor_baton,
                pool.as_mut_ptr(),
            )
        };
        Error::from_raw(err)?;
        Ok(())
    }

    /// Gets information about a path at a specific revision.
    pub fn stat(
        &mut self,
        path: impl TryInto<RelPath, Error = Error<'static>>,
        rev: Revnum,
    ) -> Result<Dirent, Error<'static>> {
        let relpath = path.try_into()?;
        let path = std::ffi::CString::new(relpath.as_str()).unwrap();
        let mut dirent = std::ptr::null_mut();
        let pool = Pool::new();
        let err = unsafe {
            subversion_sys::svn_ra_stat(
                self.ptr,
                path.as_ptr(),
                rev.into(),
                &mut dirent,
                pool.as_mut_ptr(),
            )
        };
        Error::from_raw(err)?;
        Ok(Dirent::from_raw(dirent))
    }

    /// Gets the repository UUID.
    pub fn get_uuid(&mut self) -> Result<String, Error<'static>> {
        let pool = Pool::new();
        let mut uuid = std::ptr::null();
        let err =
            unsafe { subversion_sys::svn_ra_get_uuid2(self.ptr, &mut uuid, pool.as_mut_ptr()) };
        Error::from_raw(err)?;
        let uuid = unsafe { std::ffi::CStr::from_ptr(uuid) };
        Ok(uuid.to_string_lossy().into_owned())
    }

    /// Gets the repository root URL.
    pub fn get_repos_root(&mut self) -> Result<String, Error<'static>> {
        with_tmp_pool(|pool| {
            let mut url = std::ptr::null();
            let err = unsafe {
                subversion_sys::svn_ra_get_repos_root2(self.ptr, &mut url, pool.as_mut_ptr())
            };
            Error::from_raw(err)?;
            let url = unsafe { std::ffi::CStr::from_ptr(url) };
            Ok(url.to_str().unwrap().to_string())
        })
    }

    /// Get repository information
    ///
    /// Returns a struct containing:
    /// - Repository UUID
    /// - Repository root URL
    /// - Latest revision number
    /// - Session URL
    pub fn get_repository_info(&mut self) -> Result<RepositoryInfo, Error<'static>> {
        Ok(RepositoryInfo {
            uuid: self.get_uuid()?,
            root_url: self.get_repos_root()?,
            latest_revision: self.get_latest_revnum()?,
            session_url: self.get_session_url()?,
        })
    }

    /// Gets the revision when a path was deleted.
    pub fn get_deleted_rev(
        &mut self,
        path: &str,
        peg_revision: Revnum,
        end_revision: Revnum,
    ) -> Result<Revnum, Error<'static>> {
        let path = std::ffi::CString::new(path).unwrap();
        let mut rev = 0;
        let pool = Pool::new();
        let err = unsafe {
            subversion_sys::svn_ra_get_deleted_rev(
                self.ptr,
                path.as_ptr(),
                peg_revision.into(),
                end_revision.into(),
                &mut rev,
                pool.as_mut_ptr(),
            )
        };
        Error::from_raw(err)?;
        Ok(Revnum::from_raw(rev).unwrap())
    }

    /// Checks if the repository has a specific capability.
    pub fn has_capability(&mut self, capability: &str) -> Result<bool, Error<'static>> {
        let capability = std::ffi::CString::new(capability).unwrap();
        let mut has = 0;
        let pool = Pool::new();
        let err = unsafe {
            subversion_sys::svn_ra_has_capability(
                self.ptr,
                &mut has,
                capability.as_ptr(),
                pool.as_mut_ptr(),
            )
        };
        Error::from_raw(err)?;
        Ok(has != 0)
    }

    /// Get multiple files from the repository at once
    ///
    /// This is more efficient than calling get_file() multiple times as it
    /// allows the RA layer to batch network operations.
    pub fn get_files(
        &mut self,
        paths: &[&str],
        rev: Revnum,
    ) -> Result<Vec<(String, Vec<u8>, HashMap<String, Vec<u8>>)>, Error<'static>> {
        let mut results = Vec::new();

        for path in paths {
            // Use a StringBuf-backed stream to collect the file contents
            let mut stringbuf = crate::io::StringBuf::new();
            let mut stream = crate::io::Stream::from_stringbuf(&mut stringbuf);

            let (_fetched_rev, props) = self.get_file(path, rev, &mut stream)?;

            // Get contents from the stringbuf
            let contents = stringbuf.as_bytes().to_vec();

            results.push((path.to_string(), contents, props));
        }

        Ok(results)
    }

    /// Performs a diff operation.
    ///
    /// The returned reporter borrows from the session and must not outlive it.
    pub fn diff<'s>(
        &'s mut self,
        revision: Revnum,
        diff_target: impl TryInto<RelPath, Error = Error<'static>>,
        depth: Depth,
        ignore_ancestry: bool,
        text_deltas: bool,
        versus_url: &str,
        diff_editor: &mut crate::delta::WrapEditor,
    ) -> Result<Box<dyn Reporter + Send + 's>, Error<'s>> {
        let relpath = diff_target.try_into()?;
        let diff_target = std::ffi::CString::new(relpath.as_str()).unwrap();
        let versus_url = std::ffi::CString::new(versus_url).unwrap();
        let pool = Pool::new();
        let mut reporter = std::ptr::null();
        let mut report_baton = std::ptr::null_mut();
        let (editor_ptr, editor_baton) = diff_editor.as_raw_parts();
        let err = unsafe {
            subversion_sys::svn_ra_do_diff3(
                self.ptr,
                &mut reporter,
                &mut report_baton,
                revision.into(),
                diff_target.as_ptr() as *const _,
                depth.into(),
                ignore_ancestry.into(),
                text_deltas.into(),
                versus_url.as_ptr() as *const _,
                editor_ptr,
                editor_baton,
                pool.as_mut_ptr(),
            )
        };
        Error::from_raw(err)?;
        Ok(Box::new(WrapReporter {
            reporter,
            baton: report_baton,
            _pool: pool,
            callback_batons: Vec::new(),
            _phantom: PhantomData,
        }) as Box<dyn Reporter + Send>)
    }

    /// Gets log entries for paths.
    pub fn get_log(
        &mut self,
        paths: &[&str],
        start: Revnum,
        end: Revnum,
        options: &GetLogOptions,
        log_receiver: &mut dyn FnMut(&crate::LogEntry) -> Result<(), Error<'static>>,
    ) -> Result<(), Error<'static>> {
        let pool = Pool::new();

        // Convert paths to proper C strings
        let path_cstrings: Vec<std::ffi::CString> = paths
            .iter()
            .map(|p| std::ffi::CString::new(*p).unwrap())
            .collect();
        let mut paths_array =
            apr::tables::TypedArray::<*const std::os::raw::c_char>::new(&pool, paths.len() as i32);
        for cstr in &path_cstrings {
            paths_array.push(cstr.as_ptr());
        }

        // Convert revprops to proper C strings
        // None means all revprops (pass NULL to SVN), Some(&[]) means none
        let revprop_cstrings: Vec<std::ffi::CString> = options
            .revprops
            .map(|rp| {
                rp.iter()
                    .map(|p| std::ffi::CString::new(*p).unwrap())
                    .collect()
            })
            .unwrap_or_default();
        let mut revprops_array = apr::tables::TypedArray::<*const std::os::raw::c_char>::new(
            &pool,
            revprop_cstrings.len() as i32,
        );
        for cstr in &revprop_cstrings {
            revprops_array.push(cstr.as_ptr());
        }
        let revprops_ptr = if options.revprops.is_some() {
            unsafe { revprops_array.as_ptr() }
        } else {
            std::ptr::null()
        };

        // Box the reference like the original code
        let baton = Box::into_raw(Box::new(log_receiver)) as *mut std::ffi::c_void;

        let err = unsafe {
            subversion_sys::svn_ra_get_log2(
                self.ptr,
                paths_array.as_ptr(),
                start.into(),
                end.into(),
                options.limit as _,
                options.discover_changed_paths.into(),
                options.strict_node_history.into(),
                options.include_merged_revisions.into(),
                revprops_ptr,
                Some(crate::wrap_log_entry_receiver),
                baton,
                pool.as_mut_ptr(),
            )
        };

        // Clean up the boxed callback
        let _ = unsafe {
            Box::from_raw(
                baton as *mut &mut dyn FnMut(&crate::LogEntry) -> Result<(), Error<'static>>,
            )
        };

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

    /// Retrieve log entries as a streaming iterator.
    ///
    /// Unlike [`get_log()`](Self::get_log) which uses a callback, this returns an
    /// iterator that yields log entries lazily. Internally runs `get_log()` on a
    /// worker thread and streams entries back via a channel.
    ///
    /// The iterator borrows `self` mutably, preventing other operations on the
    /// session until it is dropped. Dropping the iterator early cancels the
    /// remaining log retrieval.
    pub fn iter_logs(
        &mut self,
        paths: &[&str],
        start: Revnum,
        end: Revnum,
        options: &GetLogOptions,
    ) -> RaLogIterator<'_> {
        let (tx, rx) = std::sync::mpsc::sync_channel::<Result<OwnedLogEntry, Error<'static>>>(4);

        // Clone data that needs to move to the worker thread
        let paths: Vec<String> = paths.iter().map(|s| s.to_string()).collect();
        let limit = options.limit;
        let discover_changed_paths = options.discover_changed_paths;
        let strict_node_history = options.strict_node_history;
        let include_merged_revisions = options.include_merged_revisions;
        let revprops: Option<Vec<String>> = options
            .revprops
            .map(|rp| rp.iter().map(|s| s.to_string()).collect());

        // Safety: we send a raw pointer to `self` to the worker thread.
        // This is safe because:
        // 1. The returned RaLogIterator borrows &mut self, preventing other access
        // 2. RaLogIterator::drop joins the thread, so the thread cannot outlive the borrow
        let session_addr = self as *mut Session as usize;

        let handle = std::thread::spawn(move || {
            let session = unsafe { &mut *(session_addr as *mut Session) };
            let path_refs: Vec<&str> = paths.iter().map(|s| s.as_str()).collect();
            let revprop_refs: Option<Vec<&str>> = revprops
                .as_ref()
                .map(|rp| rp.iter().map(|s| s.as_str()).collect());
            let opts = GetLogOptions {
                limit,
                discover_changed_paths,
                strict_node_history,
                include_merged_revisions,
                revprops: revprop_refs.as_deref(),
            };

            let result = session.get_log(&path_refs, start, end, &opts, &mut |entry| {
                let owned = OwnedLogEntry::from_log_entry(entry);
                if tx.send(Ok(owned)).is_err() {
                    return Err(Error::with_raw_status(
                        subversion_sys::svn_errno_t_SVN_ERR_CANCELLED as i32,
                        None,
                        "Log iteration cancelled",
                    ));
                }
                Ok(())
            });

            if let Err(e) = result {
                if e.raw_apr_err() != subversion_sys::svn_errno_t_SVN_ERR_CANCELLED as i32 {
                    let _ = tx.send(Err(e));
                }
            }
        });

        RaLogIterator {
            rx,
            handle: Some(handle),
            _phantom: PhantomData,
        }
    }

    /// Gets the locations of a path at multiple revisions.
    pub fn get_locations(
        &mut self,
        path: impl TryInto<RelPath, Error = Error<'static>>,
        peg_revision: Revnum,
        location_revisions: &[Revnum],
    ) -> Result<HashMap<Revnum, String>, Error<'_>> {
        let relpath = path.try_into()?;
        let path = std::ffi::CString::new(relpath.as_str()).unwrap();
        let pool = Pool::new();
        let mut location_revisions_array =
            apr::tables::TypedArray::<subversion_sys::svn_revnum_t>::new(
                &pool,
                location_revisions.len() as i32,
            );
        for rev in location_revisions {
            location_revisions_array.push((*rev).into());
        }
        let location_revisions = location_revisions_array;
        let mut locations = std::ptr::null_mut();
        let err = unsafe {
            subversion_sys::svn_ra_get_locations(
                self.ptr,
                &mut locations,
                path.as_ptr(),
                peg_revision.into(),
                location_revisions.as_ptr(),
                pool.as_mut_ptr(),
            )
        };
        Error::from_raw(err)?;

        let iter = unsafe { apr::hash::Hash::from_ptr(locations) };

        let mut locations = HashMap::new();
        for (k, v) in iter.iter() {
            // The key is a pointer to svn_revnum_t (i64)
            let revnum = unsafe { *(k.as_ptr() as *const subversion_sys::svn_revnum_t) };
            // The value is a C string (path)
            locations.insert(Revnum(revnum), unsafe {
                std::ffi::CStr::from_ptr(v as *const std::ffi::c_char)
                    .to_string_lossy()
                    .into_owned()
            });
        }

        Ok(locations)
    }

    /// Gets location segments for a path.
    pub fn get_location_segments(
        &mut self,
        path: impl TryInto<RelPath, Error = Error<'static>>,
        peg_revision: Revnum,
        start: Revnum,
        end: Revnum,
        location_receiver: &dyn Fn(&crate::LocationSegment) -> Result<(), Error<'static>>,
    ) -> Result<(), Error<'static>> {
        let relpath = path.try_into()?;
        let path = std::ffi::CString::new(relpath.as_str()).unwrap();
        let pool = Pool::new();
        let err = unsafe {
            subversion_sys::svn_ra_get_location_segments(
                self.ptr,
                path.as_ptr(),
                peg_revision.into(),
                start.into(),
                end.into(),
                Some(wrap_location_segment_receiver),
                &location_receiver as *const _ as *mut _,
                pool.as_mut_ptr(),
            )
        };
        Error::from_raw(err)?;
        Ok(())
    }

    /// Locks paths in the repository.
    pub fn lock(
        &mut self,
        path_revs: &HashMap<String, Revnum>,
        comment: &str,
        steal_lock: bool,
        mut lock_func: impl FnMut(
            &str,
            bool,
            Option<&crate::Lock>,
            Option<&Error>,
        ) -> Result<(), Error<'static>>,
    ) -> Result<(), Error<'static>> {
        let pool = Pool::new();
        let scratch_pool = std::rc::Rc::new(Pool::new());
        let revnum_values: Vec<_> = path_revs.values().map(|v| v.0).collect();
        // Convert paths to proper C strings
        let path_cstrings: Vec<std::ffi::CString> = path_revs
            .keys()
            .map(|k| std::ffi::CString::new(k.as_str()).unwrap())
            .collect();
        let mut hash = apr::hash::Hash::new(&scratch_pool);
        for (cstring, revnum_val) in path_cstrings.iter().zip(revnum_values.iter()) {
            unsafe {
                hash.insert(
                    cstring.as_bytes_with_nul(),
                    revnum_val as *const _ as *mut std::ffi::c_void,
                );
            }
        }
        let comment = std::ffi::CString::new(comment).unwrap();

        // Box the reference like get_log does
        let baton = Box::into_raw(Box::new(
            &mut lock_func
                as &mut dyn FnMut(
                    &str,
                    bool,
                    Option<&crate::Lock>,
                    Option<&Error>,
                ) -> Result<(), Error<'static>>,
        )) as *mut std::ffi::c_void;

        let err = unsafe {
            subversion_sys::svn_ra_lock(
                self.ptr,
                hash.as_mut_ptr(),
                comment.as_ptr(),
                steal_lock.into(),
                Some(wrap_lock_func),
                baton,
                pool.as_mut_ptr(),
            )
        };

        // Clean up the boxed reference
        let _ = unsafe {
            Box::from_raw(
                baton
                    as *mut &mut dyn FnMut(
                        &str,
                        bool,
                        Option<&crate::Lock>,
                        Option<&Error>,
                    ) -> Result<(), Error<'static>>,
            )
        };

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

    /// Unlocks paths in the repository.
    pub fn unlock(
        &mut self,
        path_tokens: &HashMap<String, String>,
        break_lock: bool,
        mut lock_func: impl FnMut(
            &str,
            bool,
            Option<&crate::Lock>,
            Option<&Error>,
        ) -> Result<(), Error<'static>>,
    ) -> Result<(), Error<'static>> {
        let pool = Pool::new();
        let scratch_pool = std::rc::Rc::new(Pool::new());
        let mut hash = apr::hash::Hash::new(&scratch_pool);
        // Convert paths and tokens to proper C strings and keep them alive
        let path_cstrings: Vec<std::ffi::CString> = path_tokens
            .keys()
            .map(|k| std::ffi::CString::new(k.as_str()).unwrap())
            .collect();
        let token_cstrings: Vec<std::ffi::CString> = path_tokens
            .values()
            .map(|v| std::ffi::CString::new(v.as_str()).unwrap())
            .collect();
        for (path_cstring, token_cstring) in path_cstrings.iter().zip(token_cstrings.iter()) {
            unsafe {
                hash.insert(
                    path_cstring.as_bytes_with_nul(),
                    token_cstring.as_ptr() as *mut std::ffi::c_void,
                );
            }
        }

        // Box the reference like get_log does
        let baton = Box::into_raw(Box::new(
            &mut lock_func
                as &mut dyn FnMut(
                    &str,
                    bool,
                    Option<&crate::Lock>,
                    Option<&Error>,
                ) -> Result<(), Error<'static>>,
        )) as *mut std::ffi::c_void;

        let err = unsafe {
            subversion_sys::svn_ra_unlock(
                self.ptr,
                hash.as_mut_ptr(),
                break_lock.into(),
                Some(wrap_lock_func),
                baton,
                pool.as_mut_ptr(),
            )
        };

        // Clean up the boxed reference
        let _ = unsafe {
            Box::from_raw(
                baton
                    as *mut &mut dyn FnMut(
                        &str,
                        bool,
                        Option<&crate::Lock>,
                        Option<&Error>,
                    ) -> Result<(), Error<'static>>,
            )
        };

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

    /// Gets lock information for a path.
    ///
    /// Returns `None` if the path is not locked.
    /// The returned lock borrows from the session to ensure proper lifetime.
    pub fn get_lock(
        &self,
        path: impl TryInto<RelPath, Error = Error<'static>>,
    ) -> Result<Option<crate::Lock<'_>>, Error<'_>> {
        let relpath = path.try_into()?;
        let path = std::ffi::CString::new(relpath.as_str()).unwrap();
        let mut lock = std::ptr::null_mut();
        // Create a pool for the lock data
        let pool = Pool::new();
        let err = unsafe {
            subversion_sys::svn_ra_get_lock(self.ptr, &mut lock, path.as_ptr(), pool.as_mut_ptr())
        };
        Error::from_raw(err)?;
        if lock.is_null() {
            Ok(None)
        } else {
            let pool_handle = apr::PoolHandle::owned(pool);
            Ok(Some(crate::Lock::from_raw(lock, pool_handle)))
        }
    }

    /// Gets all locks under a path.
    pub fn get_locks(
        &mut self,
        path: impl TryInto<RelPath, Error = Error<'static>>,
        depth: Depth,
    ) -> Result<HashMap<String, crate::Lock<'_>>, Error<'_>> {
        let relpath = path.try_into()?;
        let path = std::ffi::CString::new(relpath.as_str()).unwrap();
        let mut locks = std::ptr::null_mut();
        let pool = Pool::new();
        let err = unsafe {
            subversion_sys::svn_ra_get_locks2(
                self.ptr,
                &mut locks,
                path.as_ptr(),
                depth.into(),
                pool.as_mut_ptr(),
            )
        };
        Error::from_raw(err)?;
        let hash = unsafe { apr::hash::Hash::from_ptr(locks) };
        Ok(hash
            .iter()
            .map(|(k, v)| {
                // Duplicate each lock into its own pool so it can be owned independently
                let lock_pool = Pool::new();
                let duplicated = unsafe {
                    subversion_sys::svn_lock_dup(
                        v as *const subversion_sys::svn_lock_t,
                        lock_pool.as_mut_ptr(),
                    )
                };
                let pool_handle = apr::PoolHandle::owned(lock_pool);
                (
                    String::from_utf8_lossy(k).into_owned(),
                    crate::Lock::from_raw(duplicated, pool_handle),
                )
            })
            .collect())
    }

    /// Replay the changes from a single revision through an editor.
    ///
    /// Changes will be limited to those that occur under this session's URL, and
    /// the server will assume that the client has no knowledge of revisions
    /// prior to `low_water_mark`.
    ///
    /// If `send_deltas` is true, the actual text and property changes will be
    /// sent; otherwise dummy text deltas and null property changes are sent.
    ///
    /// Wraps `svn_ra_replay`.
    pub fn replay(
        &mut self,
        revision: Revnum,
        low_water_mark: Revnum,
        send_deltas: bool,
        editor: &mut crate::delta::WrapEditor,
    ) -> Result<(), Error<'static>> {
        let pool = Pool::new();
        let (editor_ptr, editor_baton) = editor.as_raw_parts();
        let err = unsafe {
            subversion_sys::svn_ra_replay(
                self.ptr,
                revision.into(),
                low_water_mark.into(),
                send_deltas.into(),
                editor_ptr,
                editor_baton,
                pool.as_mut_ptr(),
            )
        };
        Error::from_raw(err)?;
        Ok(())
    }

    /// Replay a range of revisions, calling callbacks for each.
    ///
    /// For each revision in `start_revision..=end_revision`, calls `revstart`
    /// which must return a `WrapEditor` to receive the replay. After the
    /// revision is replayed, calls `revfinish` with the same editor.
    ///
    /// `low_water_mark` tells the server the oldest revision the client knows
    /// about. If `send_deltas` is true, actual content changes are sent.
    ///
    /// Wraps `svn_ra_replay_range`.
    pub fn replay_range(
        &mut self,
        start_revision: Revnum,
        end_revision: Revnum,
        low_water_mark: Revnum,
        send_deltas: bool,
        mut revstart: impl FnMut(
            Revnum,
            &HashMap<String, Vec<u8>>,
        ) -> Result<crate::delta::WrapEditor<'static>, Error<'static>>,
        mut revfinish: impl FnMut(
            Revnum,
            &HashMap<String, Vec<u8>>,
            &mut crate::delta::WrapEditor<'static>,
        ) -> Result<(), Error<'static>>,
    ) -> Result<(), Error<'static>> {
        // We use raw pointers to the closures to pass them through C callbacks.
        // Safety: the raw pointers are only used within the scope of svn_ra_replay_range
        // below, and the closures live on the stack for that entire duration.
        //
        // The baton stores *mut c_void pointers (not trait object fat pointers) to avoid
        // requiring 'static bounds on the closures. The extern "C" wrapper functions
        // cast these back to the concrete closure types via the type parameters on
        // replay_range_inner.
        struct ReplayRangeBaton {
            revstart: *mut std::ffi::c_void,
            revfinish: *mut std::ffi::c_void,
            current_editor: Option<crate::delta::WrapEditor<'static>>,
        }

        fn replay_range_inner<RS, RF>(
            session_ptr: *mut subversion_sys::svn_ra_session_t,
            start_revision: Revnum,
            end_revision: Revnum,
            low_water_mark: Revnum,
            send_deltas: bool,
            revstart: &mut RS,
            revfinish: &mut RF,
        ) -> Result<(), Error<'static>>
        where
            RS: FnMut(
                Revnum,
                &HashMap<String, Vec<u8>>,
            ) -> Result<crate::delta::WrapEditor<'static>, Error<'static>>,
            RF: FnMut(
                Revnum,
                &HashMap<String, Vec<u8>>,
                &mut crate::delta::WrapEditor<'static>,
            ) -> Result<(), Error<'static>>,
        {
            extern "C" fn revstart_wrapper<RS>(
                revision: subversion_sys::svn_revnum_t,
                replay_baton: *mut std::ffi::c_void,
                editor: *mut *const subversion_sys::svn_delta_editor_t,
                edit_baton: *mut *mut std::ffi::c_void,
                rev_props: *mut apr_sys::apr_hash_t,
                _pool: *mut apr_sys::apr_pool_t,
            ) -> *mut subversion_sys::svn_error_t
            where
                RS: FnMut(
                    Revnum,
                    &HashMap<String, Vec<u8>>,
                )
                    -> Result<crate::delta::WrapEditor<'static>, Error<'static>>,
            {
                let baton = unsafe { &mut *(replay_baton as *mut ReplayRangeBaton) };

                let rev_props_map = if rev_props.is_null() {
                    HashMap::new()
                } else {
                    let prop_hash = unsafe { crate::props::PropHash::from_ptr(rev_props) };
                    prop_hash.to_hashmap()
                };

                let rev = match Revnum::from_raw(revision) {
                    Some(r) => r,
                    None => {
                        return unsafe {
                            Error::from_message("Invalid revision in replay_range revstart")
                                .into_raw()
                        };
                    }
                };

                let revstart = unsafe { &mut *(baton.revstart as *mut RS) };
                match revstart(rev, &rev_props_map) {
                    Ok(wrap_editor) => {
                        baton.current_editor = Some(wrap_editor);
                        let (editor_ptr, editor_baton_ptr) =
                            baton.current_editor.as_ref().unwrap().as_raw_parts();
                        unsafe {
                            *editor = editor_ptr;
                            *edit_baton = editor_baton_ptr;
                        }
                        std::ptr::null_mut()
                    }
                    Err(e) => unsafe { e.into_raw() },
                }
            }

            extern "C" fn revfinish_wrapper<RF>(
                revision: subversion_sys::svn_revnum_t,
                replay_baton: *mut std::ffi::c_void,
                _editor: *const subversion_sys::svn_delta_editor_t,
                _edit_baton: *mut std::ffi::c_void,
                rev_props: *mut apr_sys::apr_hash_t,
                _pool: *mut apr_sys::apr_pool_t,
            ) -> *mut subversion_sys::svn_error_t
            where
                RF: FnMut(
                    Revnum,
                    &HashMap<String, Vec<u8>>,
                    &mut crate::delta::WrapEditor<'static>,
                ) -> Result<(), Error<'static>>,
            {
                let baton = unsafe { &mut *(replay_baton as *mut ReplayRangeBaton) };

                let rev_props_map = if rev_props.is_null() {
                    HashMap::new()
                } else {
                    let prop_hash = unsafe { crate::props::PropHash::from_ptr(rev_props) };
                    prop_hash.to_hashmap()
                };

                let rev = match Revnum::from_raw(revision) {
                    Some(r) => r,
                    None => {
                        return unsafe {
                            Error::from_message("Invalid revision in replay_range revfinish")
                                .into_raw()
                        };
                    }
                };

                let revfinish = unsafe { &mut *(baton.revfinish as *mut RF) };
                let result = match baton.current_editor.as_mut() {
                    Some(editor) => revfinish(rev, &rev_props_map, editor),
                    None => Err(Error::from_message(
                        "revfinish called without a current editor",
                    )),
                };

                // Drop the editor after revfinish
                baton.current_editor = None;

                match result {
                    Ok(()) => std::ptr::null_mut(),
                    Err(e) => unsafe { e.into_raw() },
                }
            }

            let mut baton = ReplayRangeBaton {
                revstart: revstart as *mut RS as *mut std::ffi::c_void,
                revfinish: revfinish as *mut RF as *mut std::ffi::c_void,
                current_editor: None,
            };
            let baton_ptr = &mut baton as *mut ReplayRangeBaton;

            let pool = Pool::new();
            let err = unsafe {
                subversion_sys::svn_ra_replay_range(
                    session_ptr,
                    start_revision.into(),
                    end_revision.into(),
                    low_water_mark.into(),
                    send_deltas.into(),
                    Some(revstart_wrapper::<RS>),
                    Some(revfinish_wrapper::<RF>),
                    baton_ptr as *mut std::ffi::c_void,
                    pool.as_mut_ptr(),
                )
            };

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

        replay_range_inner(
            self.ptr,
            start_revision,
            end_revision,
            low_water_mark,
            send_deltas,
            &mut revstart,
            &mut revfinish,
        )
    }

    /// Get the history of a file as revisions.
    ///
    /// Calls `file_rev_handler` for each revision that modified the file.
    /// The handler receives the path, revision, revision properties,
    /// whether this is from a merge, and the property diffs for that revision.
    ///
    /// To receive the text delta for a revision, return `Some(handler)` where
    /// `handler` is a [`TxDeltaHandler`] that will be called with each delta
    /// window (and a final `None` to signal end-of-delta). Return `None` to
    /// skip the text delta for that revision.
    pub fn get_file_revs(
        &mut self,
        path: impl TryInto<RelPath, Error = Error<'static>>,
        start: Revnum,
        end: Revnum,
        include_merged_revisions: bool,
        mut file_rev_handler: impl FnMut(
            &str,
            Revnum,
            &HashMap<String, Vec<u8>>,
            bool,
            Option<(&str, Revnum)>,
            Option<(&str, Revnum)>,
            &HashMap<String, Vec<u8>>,
        ) -> Result<Option<TxDeltaHandler>, Error<'static>>,
    ) -> Result<(), Error<'static>> {
        let relpath = path.try_into()?;
        let path_cstr = std::ffi::CString::new(relpath.as_str())?;
        let pool = Pool::new();

        type HandlerFn = dyn FnMut(
            &str,
            Revnum,
            &HashMap<String, Vec<u8>>,
            bool,
            Option<(&str, Revnum)>,
            Option<(&str, Revnum)>,
            &HashMap<String, Vec<u8>>,
        ) -> Result<Option<TxDeltaHandler>, Error<'static>>;

        // Box a &mut dyn reference (like lock/get_log) — no 'static needed.
        // The cast to &mut HandlerFn creates a trait object whose lifetime is
        // tied to this function scope, which is safe because svn_ra_get_file_revs2
        // is synchronous and the pointer is freed before we return.
        let handler_ref: &mut dyn FnMut(
            &str,
            Revnum,
            &HashMap<String, Vec<u8>>,
            bool,
            Option<(&str, Revnum)>,
            Option<(&str, Revnum)>,
            &HashMap<String, Vec<u8>>,
        ) -> Result<Option<TxDeltaHandler>, Error<'static>> = &mut file_rev_handler;
        let baton = Box::into_raw(Box::new(handler_ref)) as *mut std::ffi::c_void;

        extern "C" fn txdelta_trampoline(
            window: *mut subversion_sys::svn_txdelta_window_t,
            baton: *mut std::ffi::c_void,
        ) -> *mut subversion_sys::svn_error_t {
            let handler = unsafe { &mut *(baton as *mut TxDeltaHandler) };
            if window.is_null() {
                let result = handler(None);
                // NULL window = end of delta; free the handler
                unsafe {
                    drop(Box::from_raw(baton as *mut TxDeltaHandler));
                }
                match result {
                    Ok(()) => std::ptr::null_mut(),
                    Err(e) => unsafe { e.into_raw() },
                }
            } else {
                let win = unsafe { crate::delta::TxDeltaWindowRef::from_raw(window) };
                match handler(Some(&win)) {
                    Ok(()) => std::ptr::null_mut(),
                    Err(e) => unsafe { e.into_raw() },
                }
            }
        }

        extern "C" fn file_rev_handler_wrapper(
            baton: *mut std::ffi::c_void,
            path: *const std::os::raw::c_char,
            rev: subversion_sys::svn_revnum_t,
            rev_props: *mut apr_sys::apr_hash_t,
            result_of_merge: subversion_sys::svn_boolean_t,
            txdelta_handler_out: *mut subversion_sys::svn_txdelta_window_handler_t,
            txdelta_baton_out: *mut *mut std::ffi::c_void,
            prop_diffs: *mut apr_sys::apr_array_header_t,
            _pool: *mut apr_sys::apr_pool_t,
        ) -> *mut subversion_sys::svn_error_t {
            let handler: &mut &mut HandlerFn = unsafe { &mut *(baton as *mut &mut HandlerFn) };

            let path_str = unsafe { std::ffi::CStr::from_ptr(path).to_str().unwrap() };

            let rev_props_hash = if rev_props.is_null() {
                HashMap::new()
            } else {
                let prop_hash = unsafe { crate::props::PropHash::from_ptr(rev_props) };
                prop_hash.to_hashmap()
            };

            let prop_diffs_map = if prop_diffs.is_null() {
                HashMap::new()
            } else {
                let array = unsafe {
                    std::slice::from_raw_parts(
                        (*prop_diffs).elts as *const subversion_sys::svn_prop_t,
                        (*prop_diffs).nelts as usize,
                    )
                };
                let mut props = HashMap::new();
                for prop in array {
                    let name = unsafe { std::ffi::CStr::from_ptr(prop.name).to_str().unwrap() };
                    let value = if prop.value.is_null() {
                        Vec::new()
                    } else {
                        unsafe {
                            Vec::from(std::slice::from_raw_parts(
                                (*prop.value).data as *const u8,
                                (*prop.value).len,
                            ))
                        }
                    };
                    props.insert(name.to_string(), value);
                }
                props
            };

            match handler(
                path_str,
                Revnum::from_raw(rev).unwrap(),
                &rev_props_hash,
                result_of_merge != 0,
                None,
                None,
                &prop_diffs_map,
            ) {
                Ok(Some(delta_handler)) => {
                    // Only install the handler if SVN wants delta info;
                    // otherwise drop it to avoid leaking the Box.
                    if !txdelta_handler_out.is_null() && !txdelta_baton_out.is_null() {
                        let delta_ptr =
                            Box::into_raw(Box::new(delta_handler)) as *mut std::ffi::c_void;
                        unsafe {
                            *txdelta_handler_out = Some(txdelta_trampoline);
                            *txdelta_baton_out = delta_ptr;
                        }
                    }
                    std::ptr::null_mut()
                }
                Ok(None) => {
                    unsafe {
                        if !txdelta_handler_out.is_null() {
                            *txdelta_handler_out = None;
                        }
                        if !txdelta_baton_out.is_null() {
                            *txdelta_baton_out = std::ptr::null_mut();
                        }
                    }
                    std::ptr::null_mut()
                }
                Err(e) => unsafe { e.into_raw() },
            }
        }

        let err = unsafe {
            subversion_sys::svn_ra_get_file_revs2(
                self.ptr,
                path_cstr.as_ptr(),
                start.into(),
                end.into(),
                include_merged_revisions.into(),
                Some(file_rev_handler_wrapper),
                baton,
                pool.as_mut_ptr(),
            )
        };

        // Clean up the boxed reference.
        // SAFETY: baton was created from Box::into_raw above and the FFI
        // call is synchronous, so the pointer is still valid.
        let _ = unsafe {
            Box::from_raw(
                baton
                    as *mut &mut dyn FnMut(
                        &str,
                        Revnum,
                        &HashMap<String, Vec<u8>>,
                        bool,
                        Option<(&str, Revnum)>,
                        Option<(&str, Revnum)>,
                        &HashMap<String, Vec<u8>>,
                    )
                        -> Result<Option<TxDeltaHandler>, Error<'static>>,
            )
        };

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

    /// Gets inherited properties for a path.
    pub fn get_inherited_props(
        &mut self,
        path: &str,
        revision: Revnum,
    ) -> Result<Vec<(String, HashMap<String, Vec<u8>>)>, Error<'_>> {
        let path_cstr = std::ffi::CString::new(path)?;
        let pool = Pool::new();
        let mut inherited_props_array: *mut apr_sys::apr_array_header_t = std::ptr::null_mut();

        let scratch_pool = Pool::new();
        let err = unsafe {
            subversion_sys::svn_ra_get_inherited_props(
                self.ptr,
                &mut inherited_props_array,
                path_cstr.as_ptr(),
                revision.into(),
                pool.as_mut_ptr(),
                scratch_pool.as_mut_ptr(),
            )
        };

        Error::from_raw(err)?;

        if inherited_props_array.is_null() {
            return Ok(Vec::new());
        }

        // The array contains POINTERS to svn_prop_inherited_item_t structures
        let array = unsafe {
            std::slice::from_raw_parts(
                (*inherited_props_array).elts
                    as *const *const subversion_sys::svn_prop_inherited_item_t,
                (*inherited_props_array).nelts as usize,
            )
        };

        let mut result = Vec::new();
        for item_ptr in array.iter() {
            if item_ptr.is_null() {
                continue;
            }
            let item = unsafe { &**item_ptr };
            // Debug: Check if the item has valid pointers
            if item.path_or_url.is_null() {
                // Skip items with null path_or_url
                continue;
            }
            let path_or_url = unsafe {
                std::ffi::CStr::from_ptr(item.path_or_url)
                    .to_string_lossy()
                    .to_string()
            };

            // Convert prop_hash to HashMap
            let props = if item.prop_hash.is_null() {
                HashMap::new()
            } else {
                // prop_hash is apr_hash_t with (const char *) keys and (svn_string_t *) values
                // We must use raw Hash because the hash can contain NULL values
                let hash = unsafe { apr::hash::Hash::from_ptr(item.prop_hash) };
                let mut props = HashMap::new();

                for (key, value) in hash.iter() {
                    if value.is_null() {
                        // Skip NULL values - these represent deleted properties
                        continue;
                    }

                    // The value is a pointer to svn_string_t stored directly in the hash
                    // Cast it to svn_string_t* and dereference it
                    let svn_str_ptr = value as *const subversion_sys::svn_string_t;
                    let svn_str = unsafe { &*svn_str_ptr };

                    // Use helper function to safely extract data
                    let data = crate::svn_string_helpers::to_vec(svn_str);
                    props.insert(String::from_utf8_lossy(key).into_owned(), data);
                }

                props
            };

            result.push((path_or_url, props));
        }

        Ok(result)
    }

    /// Perform a diff operation between two revisions
    ///
    /// This wraps svn_ra_do_diff3 to compute differences between revisions.
    /// The diff editor callbacks will be invoked to describe the differences.
    ///
    /// The returned reporter borrows from the session and must not outlive it.
    pub fn do_diff<'s>(
        &'s mut self,
        revision: Revnum,
        diff_target: &str,
        options: &mut DoDiffOptions<'_, 's>,
    ) -> Result<Box<dyn Reporter + Send + 's>, Error<'s>> {
        let pool = Pool::new();

        let mut reporter: *const subversion_sys::svn_ra_reporter3_t = std::ptr::null();
        let mut report_baton: *mut std::ffi::c_void = std::ptr::null_mut();

        let diff_target_cstr = std::ffi::CString::new(diff_target).unwrap();
        let versus_url = crate::uri::canonicalize_uri(options.versus_url)?;
        let versus_url_cstr = std::ffi::CString::new(versus_url.as_str()).unwrap();

        let (editor_ptr, editor_baton) = options.diff_editor.as_raw_parts();
        let err = unsafe {
            subversion_sys::svn_ra_do_diff3(
                self.ptr,
                &mut reporter,
                &mut report_baton,
                revision.0,
                diff_target_cstr.as_ptr(),
                options.depth.into(),
                options.ignore_ancestry as i32,
                options.text_deltas as i32,
                versus_url_cstr.as_ptr(),
                editor_ptr,
                editor_baton,
                pool.as_mut_ptr(),
            )
        };

        Error::from_raw(err)?;

        Ok(Box::new(WrapReporter {
            reporter,
            baton: report_baton,
            _pool: pool,
            callback_batons: Vec::new(),
            _phantom: PhantomData,
        }))
    }

    /// Get properties of a directory without fetching its entries
    ///
    /// This is more efficient than get_dir when you only need properties.
    /// Returns a tuple of (properties, fetched_revision).
    pub fn get_dir_props(
        &mut self,
        path: &str,
        revision: Revnum,
    ) -> Result<(HashMap<String, Vec<u8>>, Revnum), Error<'_>> {
        let pool = Pool::new();
        let path_cstr = std::ffi::CString::new(path).unwrap();

        let mut props: *mut apr::hash::apr_hash_t = std::ptr::null_mut();
        let mut fetched_rev: subversion_sys::svn_revnum_t = 0;

        // Use svn_ra_get_dir2 with dirent_fields set to 0 to skip entries
        let err = unsafe {
            subversion_sys::svn_ra_get_dir2(
                self.ptr,
                std::ptr::null_mut(), // Don't fetch entries
                &mut fetched_rev,
                &mut props,
                path_cstr.as_ptr(),
                revision.0,
                0, // dirent_fields = 0 means don't fetch entry info
                pool.as_mut_ptr(),
            )
        };

        Error::from_raw(err)?;

        let properties = if props.is_null() {
            HashMap::new()
        } else {
            let prop_hash = unsafe { crate::props::PropHash::from_ptr(props) };
            prop_hash.to_hashmap()
        };

        Ok((properties, Revnum(fetched_rev as _)))
    }

    /// Set or delete a revision property (with optional old value check)
    ///
    /// If value is None, the property will be deleted.
    /// If old_value is provided, it must match the current value for the change to succeed.
    pub fn change_rev_prop2(
        &mut self,
        rev: Revnum,
        name: &str,
        old_value: Option<&[u8]>,
        value: Option<&[u8]>,
    ) -> Result<(), Error<'static>> {
        let pool = Pool::new();
        let name_cstr = std::ffi::CString::new(name).unwrap();

        // Create svn_string_t for the values
        let old_value_svn = old_value.map(|v| crate::string::BStr::from_bytes(v, &pool));
        let old_value_ptr = old_value_svn.as_ref().map(|v| v.as_ptr());
        let old_value_ptr_ptr = match old_value_ptr {
            Some(ptr) => &ptr as *const *const subversion_sys::svn_string_t,
            None => std::ptr::null(),
        };

        let value_ptr = if let Some(val) = value {
            let svn_str = crate::string::BStr::from_bytes(val, &pool);
            svn_str.as_ptr()
        } else {
            std::ptr::null()
        };

        let err = unsafe {
            subversion_sys::svn_ra_change_rev_prop2(
                self.ptr,
                rev.0,
                name_cstr.as_ptr(),
                old_value_ptr_ptr,
                value_ptr,
                pool.as_mut_ptr(),
            )
        };

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

/// Returns a string listing all available repository access modules.
pub fn modules() -> Result<String, Error<'static>> {
    let pool = Pool::new();
    let buf = unsafe { subversion_sys::svn_stringbuf_create(c"".as_ptr(), pool.as_mut_ptr()) };

    let err = unsafe { subversion_sys::svn_ra_print_modules(buf, pool.as_mut_ptr()) };

    Error::from_raw(err)?;

    Ok(unsafe {
        std::ffi::CStr::from_ptr((*buf).data)
            .to_string_lossy()
            .into_owned()
    })
}

/// Generate a C reporter vtable that forwards calls to a Rust Reporter implementation.
fn rust_reporter_vtable<R: Reporter>() -> subversion_sys::svn_ra_reporter3_t {
    subversion_sys::svn_ra_reporter3_t {
        set_path: Some(rust_reporter_set_path::<R>),
        delete_path: Some(rust_reporter_delete_path::<R>),
        link_path: Some(rust_reporter_link_path::<R>),
        finish_report: Some(rust_reporter_finish_report::<R>),
        abort_report: Some(rust_reporter_abort_report::<R>),
    }
}

unsafe extern "C" fn rust_reporter_set_path<R: Reporter>(
    report_baton: *mut std::ffi::c_void,
    path: *const std::os::raw::c_char,
    revision: subversion_sys::svn_revnum_t,
    depth: subversion_sys::svn_depth_t,
    start_empty: subversion_sys::svn_boolean_t,
    lock_token: *const std::os::raw::c_char,
    _pool: *mut apr_sys::apr_pool_t,
) -> *mut subversion_sys::svn_error_t {
    let reporter = &mut *(report_baton as *mut R);
    let path_str = std::ffi::CStr::from_ptr(path).to_string_lossy();
    let lock_token_str = if lock_token.is_null() {
        ""
    } else {
        std::ffi::CStr::from_ptr(lock_token).to_str().unwrap_or("")
    };
    let rev = Revnum::from_raw(revision).unwrap_or(Revnum::invalid());
    match reporter.set_path(
        &path_str,
        rev,
        Depth::from(depth),
        start_empty != 0,
        lock_token_str,
    ) {
        Ok(()) => std::ptr::null_mut(),
        Err(e) => e.into_raw(),
    }
}

unsafe extern "C" fn rust_reporter_delete_path<R: Reporter>(
    report_baton: *mut std::ffi::c_void,
    path: *const std::os::raw::c_char,
    _pool: *mut apr_sys::apr_pool_t,
) -> *mut subversion_sys::svn_error_t {
    let reporter = &mut *(report_baton as *mut R);
    let path_str = std::ffi::CStr::from_ptr(path).to_string_lossy();
    match reporter.delete_path(&path_str) {
        Ok(()) => std::ptr::null_mut(),
        Err(e) => e.into_raw(),
    }
}

unsafe extern "C" fn rust_reporter_link_path<R: Reporter>(
    report_baton: *mut std::ffi::c_void,
    path: *const std::os::raw::c_char,
    url: *const std::os::raw::c_char,
    revision: subversion_sys::svn_revnum_t,
    depth: subversion_sys::svn_depth_t,
    start_empty: subversion_sys::svn_boolean_t,
    lock_token: *const std::os::raw::c_char,
    _pool: *mut apr_sys::apr_pool_t,
) -> *mut subversion_sys::svn_error_t {
    let reporter = &mut *(report_baton as *mut R);
    let path_str = std::ffi::CStr::from_ptr(path).to_string_lossy();
    let url_str = std::ffi::CStr::from_ptr(url).to_string_lossy();
    let lock_token_str = if lock_token.is_null() {
        ""
    } else {
        std::ffi::CStr::from_ptr(lock_token).to_str().unwrap_or("")
    };
    let rev = Revnum::from_raw(revision).unwrap_or(Revnum::invalid());
    match reporter.link_path(
        &path_str,
        &url_str,
        rev,
        Depth::from(depth),
        start_empty != 0,
        lock_token_str,
    ) {
        Ok(()) => std::ptr::null_mut(),
        Err(e) => e.into_raw(),
    }
}

unsafe extern "C" fn rust_reporter_finish_report<R: Reporter>(
    report_baton: *mut std::ffi::c_void,
    _pool: *mut apr_sys::apr_pool_t,
) -> *mut subversion_sys::svn_error_t {
    let reporter = &mut *(report_baton as *mut R);
    match reporter.finish_report() {
        Ok(()) => std::ptr::null_mut(),
        Err(e) => e.into_raw(),
    }
}

unsafe extern "C" fn rust_reporter_abort_report<R: Reporter>(
    report_baton: *mut std::ffi::c_void,
    _pool: *mut apr_sys::apr_pool_t,
) -> *mut subversion_sys::svn_error_t {
    let reporter = &mut *(report_baton as *mut R);
    match reporter.abort_report() {
        Ok(()) => std::ptr::null_mut(),
        Err(e) => e.into_raw(),
    }
}

/// Reporter wrapper with RAII cleanup
pub struct WrapReporter {
    reporter: *const subversion_sys::svn_ra_reporter3_t,
    baton: *mut std::ffi::c_void,
    _pool: apr::Pool<'static>,
    callback_batons: Vec<(*mut std::ffi::c_void, crate::delta::DropperFn)>,
    _phantom: PhantomData<*mut ()>,
}

impl WrapReporter {
    /// Returns the reporter pointer
    pub fn as_ptr(&self) -> *const subversion_sys::svn_ra_reporter3_t {
        self.reporter
    }

    /// Returns the reporter baton
    pub fn as_baton(&self) -> *mut std::ffi::c_void {
        self.baton
    }

    /// Wrap a custom Rust Reporter implementation for use with C APIs.
    ///
    /// This creates a WrapReporter that forwards calls from C to your Rust reporter.
    ///
    /// # Type Parameters
    ///
    /// * `R` - The Reporter implementation
    ///
    /// # Example
    ///
    /// ```ignore
    /// let my_reporter = MyCustomReporter::new();
    /// let mut wrap_reporter = WrapReporter::from_rust_reporter(my_reporter);
    /// ```
    pub fn from_rust_reporter<R: Reporter + 'static>(reporter: R) -> Self {
        let pool = Pool::new();

        // Box the reporter and get raw pointer
        let reporter_box = Box::new(reporter);
        let reporter_ptr = Box::into_raw(reporter_box) as *mut std::ffi::c_void;

        // Create dropper function for the reporter
        let dropper: crate::delta::DropperFn = |ptr| unsafe {
            let _ = Box::from_raw(ptr as *mut R);
        };

        // Generate vtable and allocate in pool
        let vtable = rust_reporter_vtable::<R>();
        let vtable_ptr = unsafe {
            let ptr = apr_sys::apr_palloc(
                pool.as_mut_ptr(),
                std::mem::size_of::<subversion_sys::svn_ra_reporter3_t>(),
            ) as *mut subversion_sys::svn_ra_reporter3_t;
            *ptr = vtable;
            ptr as *const subversion_sys::svn_ra_reporter3_t
        };

        WrapReporter {
            reporter: vtable_ptr,
            baton: reporter_ptr,
            _pool: pool,
            callback_batons: vec![(reporter_ptr, dropper)],
            _phantom: PhantomData,
        }
    }
}

impl Drop for WrapReporter {
    fn drop(&mut self) {
        for (ptr, dropper) in &self.callback_batons {
            unsafe { dropper(*ptr) };
        }
    }
}

unsafe impl Send for WrapReporter {}

impl Reporter for WrapReporter {
    fn set_path(
        &mut self,
        path: &str,
        rev: Revnum,
        depth: Depth,
        start_empty: bool,
        lock_token: &str,
    ) -> Result<(), Error<'static>> {
        let path = std::ffi::CString::new(path).unwrap();
        let lock_token = std::ffi::CString::new(lock_token).unwrap();
        let pool = Pool::new();
        let err = unsafe {
            (*self.reporter).set_path.unwrap()(
                self.baton,
                path.as_ptr(),
                rev.into(),
                depth.into(),
                start_empty.into(),
                lock_token.as_ptr(),
                pool.as_mut_ptr(),
            )
        };
        Error::from_raw(err)?;
        Ok(())
    }

    fn delete_path(&mut self, path: &str) -> Result<(), Error<'static>> {
        let path = std::ffi::CString::new(path).unwrap();
        let pool = Pool::new();
        let err = unsafe {
            (*self.reporter).delete_path.unwrap()(self.baton, path.as_ptr(), pool.as_mut_ptr())
        };
        Error::from_raw(err)?;
        Ok(())
    }

    fn link_path(
        &mut self,
        path: &str,
        url: &str,
        rev: Revnum,
        depth: Depth,
        start_empty: bool,
        lock_token: &str,
    ) -> Result<(), Error<'static>> {
        let path = std::ffi::CString::new(path).unwrap();
        let url = std::ffi::CString::new(url).unwrap();
        let lock_token = std::ffi::CString::new(lock_token).unwrap();
        let pool = Pool::new();
        let err = unsafe {
            (*self.reporter).link_path.unwrap()(
                self.baton,
                path.as_ptr(),
                url.as_ptr(),
                rev.into(),
                depth.into(),
                start_empty.into(),
                lock_token.as_ptr(),
                pool.as_mut_ptr(),
            )
        };
        Error::from_raw(err)?;
        Ok(())
    }

    fn finish_report(&mut self) -> Result<(), Error<'static>> {
        let pool = Pool::new();
        let err = unsafe { (*self.reporter).finish_report.unwrap()(self.baton, pool.as_mut_ptr()) };
        Error::from_raw(err)?;
        Ok(())
    }

    fn abort_report(&mut self) -> Result<(), Error<'static>> {
        let pool = Pool::new();
        let err = unsafe { (*self.reporter).abort_report.unwrap()(self.baton, pool.as_mut_ptr()) };
        Error::from_raw(err)?;
        Ok(())
    }
}

/// Returns the version information for the repository access library.
pub fn version() -> crate::Version {
    unsafe { crate::Version(subversion_sys::svn_ra_version()) }
}

/// Trait for reporting working copy state to the repository.
pub trait Reporter {
    /// Reports the state of a path in the working copy.
    fn set_path(
        &mut self,
        path: &str,
        rev: Revnum,
        depth: Depth,
        start_empty: bool,
        lock_token: &str,
    ) -> Result<(), Error<'static>>;

    /// Reports that a path has been deleted from the working copy.
    fn delete_path(&mut self, path: &str) -> Result<(), Error<'static>>;

    /// Links a path to a URL in the repository.
    fn link_path(
        &mut self,
        path: &str,
        url: &str,
        rev: Revnum,
        depth: Depth,
        start_empty: bool,
        lock_token: &str,
    ) -> Result<(), Error<'static>>;

    /// Finishes the report and triggers the update/diff.
    fn finish_report(&mut self) -> Result<(), Error<'static>>;

    /// Aborts the report without triggering the update/diff.
    fn abort_report(&mut self) -> Result<(), Error<'static>>;
}

/// RA callbacks with RAII cleanup
pub struct Callbacks {
    ptr: *mut subversion_sys::svn_ra_callbacks2_t,
    _pool: apr::Pool<'static>,
    // Keep auth_baton alive for the lifetime of the callbacks, pinned to ensure stable address
    auth_baton: Option<std::pin::Pin<Box<crate::auth::AuthBaton>>>,
    // Keep cancel_func alive for the lifetime of the callbacks
    cancel_func: Option<Box<Box<dyn Fn() -> Result<(), Error<'static>>>>>,
    // Keep progress_func alive for the lifetime of the callbacks
    progress_func: Option<Box<Box<dyn Fn(i64, i64)>>>,
    _phantom: PhantomData<*mut ()>,
}

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

impl Default for Callbacks {
    fn default() -> Self {
        Self::new().unwrap()
    }
}

impl Callbacks {
    /// Creates new repository access callbacks.
    pub fn new() -> Result<Callbacks, crate::Error<'static>> {
        let pool = apr::Pool::new();
        let mut callbacks = std::ptr::null_mut();
        unsafe {
            let err = subversion_sys::svn_ra_create_callbacks(&mut callbacks, pool.as_mut_ptr());
            svn_result(err)?;
        }
        Ok(Callbacks {
            ptr: callbacks,
            _pool: pool,
            auth_baton: None,
            cancel_func: None,
            progress_func: None,
            _phantom: PhantomData,
        })
    }

    /// Sets the authentication baton for the callbacks (owned).
    pub fn set_auth_baton(&mut self, auth_baton: crate::auth::AuthBaton) {
        // Pin the auth_baton in a Box to get a stable address
        let mut pinned_baton = Box::pin(auth_baton);
        unsafe {
            // Get a mutable reference through the pin and then get its pointer
            let baton_ptr = pinned_baton.as_mut().get_mut().as_mut_ptr();
            (*self.ptr).auth_baton = baton_ptr;
        }
        // Keep the pinned auth_baton alive
        self.auth_baton = Some(pinned_baton);
    }

    /// Sets the authentication baton for the callbacks (borrowed).
    ///
    /// # Safety
    ///
    /// The caller must ensure the AuthBaton outlives these Callbacks.
    pub unsafe fn set_auth_baton_borrowed(&mut self, auth_baton: &mut crate::auth::AuthBaton) {
        (*self.ptr).auth_baton = auth_baton.as_mut_ptr();
    }

    /// Sets the cancellation callback for the RA session.
    ///
    /// This callback will be invoked periodically during RA operations to check
    /// if the operation should be cancelled. Return Ok(()) to continue or Err()
    /// to cancel the operation.
    pub fn set_cancel_func(
        &mut self,
        cancel_func: impl Fn() -> Result<(), Error<'static>> + 'static,
    ) {
        let boxed: Box<Box<dyn Fn() -> Result<(), Error<'static>>>> =
            Box::new(Box::new(cancel_func));
        unsafe {
            (*self.ptr).cancel_func = Some(crate::wrap_cancel_func);
        }
        // Store the callback to keep it alive
        self.cancel_func = Some(boxed);
    }

    /// Sets the progress notification callback for the RA session.
    ///
    /// This callback will be invoked periodically during RA operations to report
    /// progress. The first parameter is bytes processed, the second is total bytes
    /// (or -1 if unknown).
    pub fn set_progress_func(&mut self, progress_func: impl Fn(i64, i64) + 'static) {
        let boxed: Box<Box<dyn Fn(i64, i64)>> = Box::new(Box::new(progress_func));
        let baton_ptr = Box::into_raw(boxed) as *mut std::ffi::c_void;
        unsafe {
            (*self.ptr).progress_func = Some(wrap_progress_func);
            (*self.ptr).progress_baton = baton_ptr;
        }
        // Store the callback to keep it alive
        self.progress_func =
            Some(unsafe { Box::from_raw(baton_ptr as *mut Box<dyn Fn(i64, i64)>) });
    }

    /// Get the callback baton pointer for use with svn_ra_open.
    /// This returns the cancel_func baton if set, otherwise null.
    fn get_callback_baton(&self) -> *mut std::ffi::c_void {
        self.cancel_func
            .as_ref()
            .map(|b| {
                b.as_ref() as *const Box<dyn Fn() -> Result<(), Error<'static>>>
                    as *mut std::ffi::c_void
            })
            .unwrap_or(std::ptr::null_mut())
    }

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

/// Wrapper for progress notification callbacks
extern "C" fn wrap_progress_func(
    progress: i64,
    total: i64,
    baton: *mut std::ffi::c_void,
    _pool: *mut apr_sys::apr_pool_t,
) {
    if baton.is_null() {
        return;
    }

    let callback = unsafe { &*(baton as *const Box<dyn Fn(i64, i64)>) };
    callback(progress, total);
}

/// Get the Subversion API version
///
/// Returns a tuple of (major, minor, patch) version numbers.
pub fn api_version() -> (i32, i32, i32) {
    (
        subversion_sys::SVN_VER_MAJOR as i32,
        subversion_sys::SVN_VER_MINOR as i32,
        subversion_sys::SVN_VER_PATCH as i32,
    )
}

/// Get the RA ABI (Application Binary Interface) version number
///
/// This is the version number used for the RA plugin interface.
pub fn abi_version() -> i32 {
    subversion_sys::SVN_RA_ABI_VERSION as i32
}

#[cfg(all(test, feature = "repos"))]
mod tests {
    use super::*;
    use crate::mergeinfo::MergeinfoInheritance;
    use crate::{LocationSegment, Lock};

    #[test]
    fn test_relpath_canonicalize() {
        // A leading slash is stripped and the path is canonicalized.
        assert_eq!(RelPath::canonicalize("/a/b").unwrap().as_str(), "a/b");
        assert_eq!(RelPath::canonicalize("a//b/").unwrap().as_str(), "a/b");
        assert_eq!(RelPath::canonicalize("").unwrap().as_str(), "");
    }

    #[test]
    fn test_relpath_from_canonical() {
        // An already-canonical relative path is accepted unchanged.
        assert_eq!(
            RelPath::from_canonical("a/b".to_string()).unwrap().as_str(),
            "a/b"
        );
        // Non-canonical (double slash) and absolute paths are rejected.
        assert!(RelPath::from_canonical("a//b".to_string()).is_err());
        assert!(RelPath::from_canonical("/a".to_string()).is_err());
    }

    #[test]
    fn test_relpath_display_and_try_from() {
        let p = RelPath::canonicalize("/x/y").unwrap();
        assert_eq!(format!("{}", p), "x/y");

        let from_str: RelPath = "a/b".try_into().unwrap();
        assert_eq!(from_str.as_str(), "a/b");
        let from_string: RelPath = String::from("c/d").try_into().unwrap();
        assert_eq!(from_string.as_str(), "c/d");
        // TryFrom uses the strict (from_canonical) path: a leading slash fails.
        assert!(RelPath::try_from("/abs").is_err());
    }

    #[test]
    fn test_get_log_options_builder() {
        let opts = GetLogOptions::default()
            .with_limit(5)
            .with_discover_changed_paths(true)
            .with_strict_node_history(true)
            .with_include_merged_revisions(true)
            .with_revprops(Some(&["svn:log"]));
        assert_eq!(opts.limit, 5);
        assert!(opts.discover_changed_paths);
        assert!(opts.strict_node_history);
        assert!(opts.include_merged_revisions);
        assert_eq!(opts.revprops, Some(&["svn:log"][..]));

        // Each setter only changes its own field.
        let only_limit = GetLogOptions::default().with_limit(3);
        assert_eq!(only_limit.limit, 3);
        assert!(!only_limit.discover_changed_paths);
        assert!(!only_limit.strict_node_history);
    }

    /// Helper function to create a test repository and return its file:// URL
    fn create_test_repo() -> (tempfile::TempDir, String, crate::repos::Repos) {
        let temp_dir = tempfile::tempdir().unwrap();
        let repo_path = temp_dir.path().join("test_repo");
        let repo = crate::repos::Repos::create(&repo_path).unwrap();
        let url = crate::path_to_file_url(&repo_path);
        (temp_dir, url, repo)
    }

    /// Helper to create a test repo and open an RA session to it
    /// Returns callbacks to keep them alive for the session's lifetime
    fn create_test_repo_with_session() -> (
        tempfile::TempDir,
        crate::repos::Repos,
        Session<'static>,
        Box<Callbacks>,
    ) {
        let (temp_dir, url, repo) = create_test_repo();

        // Create callbacks with authentication
        let mut callbacks = Box::new(Callbacks::new().unwrap());

        // Set up authentication with a default username for file:// operations
        // AuthBaton now owns the providers, so they stay alive
        let providers = vec![
            crate::auth::get_username_provider(),
            crate::auth::get_simple_provider(None::<&fn(&str) -> Result<bool, Error<'static>>>),
        ];
        let mut auth_baton = crate::auth::AuthBaton::open(providers).unwrap();

        // Set the default username for operations that require it (like locking)
        auth_baton
            .set(crate::auth::AuthSetting::DefaultUsername("testuser"))
            .unwrap();

        callbacks.set_auth_baton(auth_baton);

        // Use unsafe to extend the lifetime - we ensure callbacks outlive session by returning both
        let (session, _, _) = unsafe {
            let callbacks_ptr = &mut *callbacks as *mut Callbacks;
            Session::open(&url, None, Some(&mut *callbacks_ptr), None).unwrap()
        };
        (temp_dir, repo, session, callbacks)
    }

    #[test]
    fn test_callbacks_creation() {
        let callbacks = Callbacks::new().unwrap();
        assert!(!callbacks.ptr.is_null());
    }

    #[test]
    fn test_set_auth_baton_borrowed() {
        let (_temp_dir, url, _repo) = create_test_repo();

        let providers = vec![crate::auth::get_username_provider()];
        let mut auth_baton = crate::auth::AuthBaton::open(providers).unwrap();
        auth_baton
            .set(crate::auth::AuthSetting::DefaultUsername("testuser"))
            .unwrap();

        let mut callbacks = Box::new(Callbacks::new().unwrap());

        // Safety: auth_baton outlives callbacks and session in this test
        unsafe { callbacks.set_auth_baton_borrowed(&mut auth_baton) };

        let callbacks_ptr = &mut *callbacks as *mut Callbacks;
        let (mut session, _, _) =
            unsafe { Session::open(&url, None, Some(&mut *callbacks_ptr), None).unwrap() };

        // Verify the session works with the borrowed auth baton
        let latest = session.get_latest_revnum().unwrap();
        assert_eq!(latest, Revnum(0));
    }

    #[test]
    fn test_get_repository_info() {
        let (_temp_dir, _repo, mut session, _callbacks) = create_test_repo_with_session();

        let info = session.get_repository_info().unwrap();

        // Check that all fields are populated
        assert!(!info.uuid.is_empty());
        assert!(info.root_url.starts_with("file://"));
        assert_eq!(info.latest_revision, crate::Revnum(0)); // New repo starts at r0
        assert!(info.session_url.starts_with("file://"));
        assert_eq!(info.root_url, info.session_url); // For a new repo, both should be the same
    }

    #[test]
    fn test_path_exists() {
        let (_temp_dir, _repo, mut session, _callbacks) = create_test_repo_with_session();

        // Test that root exists
        assert!(session.path_exists("", crate::Revnum(0)).unwrap());

        // Test that a non-existent path doesn't exist
        assert!(!session
            .path_exists("nonexistent.txt", crate::Revnum(0))
            .unwrap());
    }

    #[test]
    fn test_get_files() {
        let (_temp_dir, repo, mut session, _callbacks) = create_test_repo_with_session();

        // Create some test files in the repository
        let fs = repo.fs().unwrap();
        let mut txn = fs.begin_txn(crate::Revnum(0), 0).unwrap();
        let mut txn_root = txn.root().unwrap();

        // Create test files
        txn_root.make_file("/file1.txt").unwrap();
        let mut stream1 = txn_root.apply_text("/file1.txt", None).unwrap();
        stream1.write(b"Content of file 1").unwrap();
        stream1.close().unwrap();

        txn_root.make_file("/file2.txt").unwrap();
        let mut stream2 = txn_root.apply_text("/file2.txt", None).unwrap();
        stream2.write(b"Content of file 2").unwrap();
        stream2.close().unwrap();

        // Commit the transaction
        txn.commit().unwrap();

        // Now test get_files (RA methods use repository-relative paths, not absolute)
        let files = session
            .get_files(&["file1.txt", "file2.txt"], crate::Revnum(1))
            .unwrap();

        assert_eq!(files.len(), 2);

        // Check first file
        assert_eq!(files[0].0, "file1.txt");
        assert_eq!(files[0].1, b"Content of file 1");

        // Check second file
        assert_eq!(files[1].0, "file2.txt");
        assert_eq!(files[1].1, b"Content of file 2");
    }

    #[test]
    fn test_get_file_with_wrap_write() {
        let (_temp_dir, repo, mut session, _callbacks) = create_test_repo_with_session();

        // Create a test file in the repository
        let fs = repo.fs().unwrap();
        let mut txn = fs.begin_txn(crate::Revnum(0), 0).unwrap();
        let mut txn_root = txn.root().unwrap();

        // Create test file
        txn_root.make_file("/testfile.txt").unwrap();
        let mut stream = txn_root.apply_text("/testfile.txt", None).unwrap();
        stream.write(b"Test content from file").unwrap();
        stream.close().unwrap();

        // Commit the transaction
        let rev = txn.commit().unwrap();
        println!("Committed revision: {}", rev.0);

        // Now use wrap_write to create a stream for fetching the file
        let mut buffer: Vec<u8> = Vec::new();
        let mut output_stream = crate::io::wrap_write(&mut buffer).unwrap();

        // First, test that we can write directly to the stream
        output_stream.write(b"Direct write test").unwrap();
        output_stream.close().unwrap();
        println!(
            "Buffer after direct write: {:?}",
            String::from_utf8_lossy(&buffer)
        );
        assert_eq!(buffer, b"Direct write test");

        // Clear buffer and create new stream for actual test
        buffer.clear();
        let mut output_stream = crate::io::wrap_write(&mut buffer).unwrap();

        // Call get_file with the wrap_write stream
        println!("Calling get_file...");
        let (fetched_rev, props) = session
            .get_file("testfile.txt", crate::Revnum(1), &mut output_stream)
            .unwrap();
        println!(
            "get_file returned: fetched_rev={:?}, props.len()={}",
            fetched_rev,
            props.len()
        );

        // Close the stream to ensure all data is flushed
        output_stream.close().unwrap();
        println!(
            "Buffer after get_file: {:?}",
            String::from_utf8_lossy(&buffer)
        );

        // Verify the results
        // When a specific revision is passed, the C API doesn't set fetched_rev
        assert_eq!(
            fetched_rev, None,
            "fetched_rev should be None when specific revision is passed"
        );
        assert_eq!(
            buffer, b"Test content from file",
            "Stream should contain the file contents"
        );
        assert!(!props.is_empty(), "Properties should be returned");
    }

    #[test]
    fn test_get_file_with_invalid_revnum() {
        let (_temp_dir, repo, mut session, _callbacks) = create_test_repo_with_session();

        // Create a test file in the repository
        let fs = repo.fs().unwrap();
        let mut txn = fs.begin_txn(crate::Revnum(0), 0).unwrap();
        let mut txn_root = txn.root().unwrap();

        // Create test file
        txn_root.make_file("/testfile.txt").unwrap();
        let mut stream = txn_root.apply_text("/testfile.txt", None).unwrap();
        stream.write(b"Test content").unwrap();
        stream.close().unwrap();

        // Commit the transaction
        let committed_rev = txn.commit().unwrap();

        // Use SVN_INVALID_REVNUM to fetch HEAD
        let mut buffer: Vec<u8> = Vec::new();
        let mut output_stream = crate::io::wrap_write(&mut buffer).unwrap();

        let (fetched_rev, props) = session
            .get_file("testfile.txt", crate::Revnum(-1), &mut output_stream)
            .unwrap();

        output_stream.close().unwrap();

        // When SVN_INVALID_REVNUM is passed, fetched_rev should be set to the actual revision
        assert_eq!(
            fetched_rev,
            Some(committed_rev),
            "fetched_rev should be Some({}) when SVN_INVALID_REVNUM is passed",
            committed_rev.0
        );
        assert_eq!(buffer, b"Test content");
        assert!(!props.is_empty());
    }

    #[test]
    fn test_repo_creation_only() {
        let (_temp_dir, url, _repo) = create_test_repo();
        println!("Created repo at: {}", url);
        // Just test that we can create a repository without opening a session
    }

    #[test]
    fn test_session_opening_only() {
        let (_temp_dir, url, _repo) = create_test_repo();
        // Try to open a session - this is where the segfault might occur
        let result = Session::open(&url, None, None, None);
        println!("Session open result: {:?}", result.is_ok());
    }

    #[test]
    fn test_simple_get_log() {
        let (_temp_dir, _repo, mut session, _callbacks) = create_test_repo_with_session();

        // Try a very simple get_log call without any complex setup
        let mut call_count = 0;
        let mut log_receiver = |_log_entry: &crate::LogEntry| -> Result<(), Error<'static>> {
            call_count += 1;
            Ok(())
        };
        let result = session.get_log(
            &[""],
            crate::Revnum::from(0u32),
            crate::Revnum::from(0u32),
            &GetLogOptions::default(),
            &mut log_receiver,
        );
        println!(
            "get_log result: {:?}, calls: {}",
            result.is_ok(),
            call_count
        );
    }

    #[test]
    fn test_get_log_with_error() {
        let (_temp_dir, _repo, mut session, _callbacks) = create_test_repo_with_session();

        // Test callback that returns an error
        let mut log_receiver = |_log_entry: &crate::LogEntry| -> Result<(), Error<'static>> {
            Err(Error::from_message("Test error from callback"))
        };
        let result = session.get_log(
            &[""],
            crate::Revnum::from(0u32),
            crate::Revnum::from(0u32),
            &GetLogOptions::default(),
            &mut log_receiver,
        );
        println!("get_log with error result: {:?}", result);
        assert!(result.is_err());
    }

    #[test]
    fn test_session_url_validation() {
        // Test that Session creation requires proper URL format
        // We can't test actual connection without a real SVN server

        // Invalid URL should fail
        let result = Session::open("not-a-url", None, None, None);
        assert!(result.is_err());

        // File URL might work depending on system
        let _result = Session::open("file:///tmp/test", None, None, None);
        // Don't assert on this as it depends on system configuration
    }

    #[test]
    fn test_reporter_trait_safety() {
        // Ensure Reporter types have proper Send marker
        fn _assert_send<T: Send>() {}
        _assert_send::<WrapReporter>();
    }

    #[test]
    fn test_lock_struct() {
        // Test Lock struct creation from raw
        let pool = apr::Pool::new();
        let lock_raw: *mut subversion_sys::svn_lock_t = pool.calloc();
        unsafe {
            (*lock_raw).path = apr::strings::pstrdup_raw("/test/path", &pool).unwrap() as *const _;
            (*lock_raw).token = apr::strings::pstrdup_raw("lock-token", &pool).unwrap() as *const _;
            (*lock_raw).owner = apr::strings::pstrdup_raw("test-owner", &pool).unwrap() as *const _;
            (*lock_raw).comment =
                apr::strings::pstrdup_raw("test comment", &pool).unwrap() as *const _;
            (*lock_raw).is_dav_comment = 0;
            (*lock_raw).creation_date = 0;
            (*lock_raw).expiration_date = 0;
        }

        let pool_handle = apr::PoolHandle::owned(pool);
        let lock = Lock::from_raw(lock_raw, pool_handle);
        assert_eq!(lock.path(), "/test/path");
        assert_eq!(lock.token(), "lock-token");
        assert_eq!(lock.owner(), "test-owner");
        assert_eq!(lock.comment(), "test comment");
        assert!(!lock.is_dav_comment());
    }

    #[test]
    fn test_dirent_struct() {
        // Test Dirent struct fields
        let pool = apr::Pool::new();
        let dirent_raw: *mut subversion_sys::svn_dirent_t = pool.calloc();
        unsafe {
            (*dirent_raw).kind = subversion_sys::svn_node_kind_t_svn_node_file;
            (*dirent_raw).size = 1024;
            (*dirent_raw).has_props = 1;
            (*dirent_raw).created_rev = 42;
            (*dirent_raw).time = 1000000;
            (*dirent_raw).last_author =
                apr::strings::pstrdup_raw("author", &pool).unwrap() as *const _;
        }

        let dirent = Dirent::from_raw(dirent_raw);
        assert_eq!(dirent.kind(), crate::NodeKind::File);
        assert_eq!(dirent.size(), 1024);
        assert!(dirent.has_props());
        assert_eq!(dirent.created_rev(), Some(crate::Revnum(42)));
        assert_eq!(dirent.last_author(), Some("author"));
    }

    #[test]
    fn test_location_segment() {
        // Test LocationSegment struct
        let pool = apr::Pool::new();
        let segment_raw: *mut subversion_sys::svn_location_segment_t = pool.calloc();
        unsafe {
            (*segment_raw).range_start = 10;
            (*segment_raw).range_end = 20;
            (*segment_raw).path =
                apr::strings::pstrdup_raw("/trunk/src", &pool).unwrap() as *const _;
        }

        let segment = LocationSegment::from_raw(segment_raw);
        let range = segment.range();
        assert_eq!(range.start, crate::Revnum(20)); // Note: range is end..start
        assert_eq!(range.end, crate::Revnum(10));
        assert_eq!(segment.path(), "/trunk/src");
    }

    #[test]
    fn test_mergeinfo_inheritance() {
        // Test MergeinfoInheritance enum conversion
        assert_eq!(
            MergeinfoInheritance::from(
                subversion_sys::svn_mergeinfo_inheritance_t_svn_mergeinfo_inherited
            ),
            MergeinfoInheritance::Inherited
        );
        assert_eq!(
            MergeinfoInheritance::from(
                subversion_sys::svn_mergeinfo_inheritance_t_svn_mergeinfo_nearest_ancestor
            ),
            MergeinfoInheritance::NearestAncestor
        );
        assert_eq!(
            MergeinfoInheritance::from(
                subversion_sys::svn_mergeinfo_inheritance_t_svn_mergeinfo_explicit
            ),
            MergeinfoInheritance::Explicit
        );
    }

    // Removed test_editor_trait as it had compilation issues

    #[test]
    fn test_file_revision_struct() {
        // Test FileRevision creation
        let pool = apr::Pool::new();
        let fr_raw: *mut subversion_sys::svn_repos_node_t = pool.calloc();
        unsafe {
            // These fields don't exist in svn_repos_node_t
            // (*fr_raw).id = std::ptr::null_mut();
            // (*fr_raw).predecessor_id = std::ptr::null_mut();
            // (*fr_raw).predecessor_count = 0;
            (*fr_raw).copyfrom_path = std::ptr::null();
            (*fr_raw).copyfrom_rev = -1; // SVN_INVALID_REVNUM
            (*fr_raw).action = b'A' as i8;
            (*fr_raw).text_mod = 1;
            (*fr_raw).prop_mod = 1;
            // (*fr_raw).created_path = apr::strings::pstrdup_raw("/test", &pool).unwrap() as *const _; // Field doesn't exist
            (*fr_raw).kind = subversion_sys::svn_node_kind_t_svn_node_file;
        }

        // FileRevision wraps svn_repos_node_t (based on the fields)
        // This just ensures the types compile correctly
    }

    #[test]
    fn test_no_send_no_sync() {
        // Verify that Session is !Send and !Sync due to PhantomData<*mut ()>
        fn assert_not_send<T>()
        where
            T: ?Sized,
        {
            // This function body is empty - the check happens at compile time
            // If T were Send, this would fail to compile
        }

        fn assert_not_sync<T>()
        where
            T: ?Sized,
        {
            // This function body is empty - the check happens at compile time
            // If T were Sync, this would fail to compile
        }

        // These will compile only if Session is !Send and !Sync
        assert_not_send::<Session>();
        assert_not_sync::<Session>();
    }

    #[test]
    fn test_get_file_revs() {
        let (_temp_dir, _repo, mut session, _callbacks) = create_test_repo_with_session();

        // Test the get_file_revs signature - it should work even on an empty repo
        // though it might not return any data
        let mut handler_called = false;

        let result = session.get_file_revs(
            "nonexistent.txt",
            crate::Revnum(1),
            crate::Revnum(1),
            false,
            |_path, _rev, _rev_props, _result_of_merge, _copyfrom, _merged, _prop_diffs| {
                handler_called = true;
                Ok(None)
            },
        );

        // File doesn't exist, so should return an error
        assert!(result.is_err());
        assert!(!handler_called);
    }

    #[test]
    fn test_get_file_revs_collects_revisions() {
        let (_temp_dir, repo, mut session, _callbacks) = create_test_repo_with_session();

        // Create a file and modify it across multiple revisions
        let fs = repo.fs().unwrap();

        // Rev 1: Create file
        let mut txn = fs.begin_txn(crate::Revnum::from(0u32), 0).unwrap();
        let mut root = txn.root().unwrap();
        root.make_file("/test.txt").unwrap();
        {
            let mut stream = root.apply_text("/test.txt", None).unwrap();
            use std::io::Write;
            stream.write_all(b"Initial content\n").unwrap();
        }
        root.change_node_prop("/test.txt", "custom:prop1", b"value1")
            .unwrap();
        txn.commit().unwrap();

        // Rev 2: Modify file
        let mut txn = fs.begin_txn(crate::Revnum::from(1u32), 0).unwrap();
        let mut root = txn.root().unwrap();
        {
            let mut stream = root.apply_text("/test.txt", None).unwrap();
            use std::io::Write;
            stream.write_all(b"Modified content\n").unwrap();
        }
        root.change_node_prop("/test.txt", "custom:prop2", b"value2")
            .unwrap();
        txn.commit().unwrap();

        // Rev 3: Modify file again
        let mut txn = fs.begin_txn(crate::Revnum::from(2u32), 0).unwrap();
        let mut root = txn.root().unwrap();
        {
            let mut stream = root.apply_text("/test.txt", None).unwrap();
            use std::io::Write;
            stream.write_all(b"Final content\n").unwrap();
        }
        txn.commit().unwrap();

        // Now call get_file_revs and collect the revisions
        let mut revisions_seen = Vec::new();

        let result = session.get_file_revs(
            "test.txt",
            crate::Revnum(1),
            crate::Revnum(3),
            false,
            |_path, rev, _rev_props, _result_of_merge, _copyfrom, _merged, prop_diffs| {
                revisions_seen.push(rev.0);
                let _ = prop_diffs.len();
                Ok(None)
            },
        );

        assert!(result.is_ok(), "get_file_revs should succeed");
        assert!(
            !revisions_seen.is_empty(),
            "Should have seen at least one revision"
        );
        assert!(revisions_seen.contains(&1), "Should have seen revision 1");
    }

    #[test]
    fn test_get_file_revs_empty_range() {
        let (_temp_dir, repo, mut session, _callbacks) = create_test_repo_with_session();

        // Create a file
        let fs = repo.fs().unwrap();
        let mut txn = fs.begin_txn(crate::Revnum::from(0u32), 0).unwrap();
        let mut root = txn.root().unwrap();
        root.make_file("/test.txt").unwrap();
        {
            let mut stream = root.apply_text("/test.txt", None).unwrap();
            use std::io::Write;
            stream.write_all(b"Content\n").unwrap();
        }
        txn.commit().unwrap();

        // Call with an invalid revision range (start > end should return no revisions)
        let mut handler_called = false;

        let result = session.get_file_revs(
            "test.txt",
            crate::Revnum(10),
            crate::Revnum(1),
            false,
            |_path, _rev, _rev_props, _result_of_merge, _copyfrom, _merged, _prop_diffs| {
                handler_called = true;
                Ok(None)
            },
        );

        // Should either succeed with no calls or return an error
        // The important thing is it doesn't crash
        if result.is_ok() {
            assert!(
                !handler_called,
                "Handler should not be called for invalid range"
            );
        }
    }

    #[test]
    fn test_get_inherited_props() {
        let (_temp_dir, repo, mut session, _callbacks) = create_test_repo_with_session();

        // Create a directory structure with properties
        let fs = repo.fs().unwrap();
        let mut txn = fs.begin_txn(crate::Revnum::from(0u32), 0).unwrap();
        let mut root = txn.root().unwrap();

        // Create directories
        root.make_dir("/trunk").unwrap();
        root.make_dir("/trunk/src").unwrap();
        root.make_dir("/trunk/src/lib").unwrap();

        // Set properties at different levels
        root.change_node_prop("/trunk", "prop:level1", b"value1")
            .unwrap();
        root.change_node_prop("/trunk/src", "prop:level2", b"value2")
            .unwrap();
        root.change_node_prop("/trunk/src/lib", "prop:level3", b"value3")
            .unwrap();

        // Commit the transaction
        let rev = txn.commit().unwrap();

        // Get inherited properties for the deepest path (RA methods use repository-relative paths)
        let inherited = session.get_inherited_props("trunk/src/lib", rev).unwrap();

        // We should get properties from parent paths
        // The exact format depends on SVN's implementation, but we should get some inherited props
        // Note: inherited props typically don't include the node's own props, just parents'
        for (path, props) in &inherited {
            println!("Inherited from {}: {:?}", path, props);
            // Each parent path should have contributed properties
            if path.ends_with("trunk") {
                assert!(props.contains_key("prop:level1"));
            } else if path.ends_with("trunk/src") {
                assert!(props.contains_key("prop:level2"));
            }
        }
    }

    #[test]
    fn test_get_log_full() {
        let (_temp_dir, repo, mut session, _callbacks) = create_test_repo_with_session();

        let fs = repo.fs().unwrap();

        // Create several commits
        let mut revisions = Vec::new();

        for i in 1..=3 {
            let base_rev = if revisions.is_empty() {
                crate::Revnum::from(0u32)
            } else {
                *revisions.last().unwrap()
            };

            let mut txn = fs.begin_txn(base_rev, 0).unwrap();
            let mut root = txn.root().unwrap();

            let filename = format!("/file{}.txt", i);
            root.make_file(filename.as_str()).unwrap();
            let mut stream = root.apply_text(filename.as_str(), None).unwrap();
            use std::io::Write;
            writeln!(stream, "Content for file {}", i).unwrap();
            drop(stream);

            // Set revision properties
            txn.change_prop("svn:log", &format!("Commit {}", i))
                .unwrap();
            txn.change_prop("svn:author", "test-user").unwrap();

            let rev = txn.commit().unwrap();
            revisions.push(rev);
        }

        // Test get_log with various options
        let mut log_entries: Vec<(crate::Revnum, String, String)> = Vec::new();

        session
            .get_log(
                &[""], // Root path
                revisions[0],
                *revisions.last().unwrap(),
                &GetLogOptions::default()
                    .with_discover_changed_paths(true)
                    .with_revprops(Some(&["svn:log", "svn:author", "svn:date"])),
                &mut |log_entry| {
                    if let Some(revision) = log_entry.revision() {
                        let author = log_entry.author().unwrap_or("").to_string();
                        let message = log_entry.message().unwrap_or("").to_string();
                        log_entries.push((revision, author, message));
                    }
                    Ok(())
                },
            )
            .unwrap();

        // Verify we got all commits
        assert_eq!(log_entries.len(), 3);

        // Check that authors and messages are correct
        for (i, (rev, author, message)) in log_entries.iter().enumerate() {
            assert_eq!(author, "test-user");
            assert_eq!(message, &format!("Commit {}", i + 1));
            assert!(rev.as_u64() >= 1);
        }
    }

    #[test]
    fn test_iter_logs() {
        let (_temp_dir, repo, mut session, _callbacks) = create_test_repo_with_session();

        let fs = repo.fs().unwrap();

        // Create several commits
        let mut revisions = Vec::new();

        for i in 1..=3 {
            let base_rev = if revisions.is_empty() {
                crate::Revnum::from(0u32)
            } else {
                *revisions.last().unwrap()
            };

            let mut txn = fs.begin_txn(base_rev, 0).unwrap();
            let mut root = txn.root().unwrap();

            let filename = format!("/file{}.txt", i);
            root.make_file(filename.as_str()).unwrap();
            let mut stream = root.apply_text(filename.as_str(), None).unwrap();
            use std::io::Write;
            writeln!(stream, "Content for file {}", i).unwrap();
            drop(stream);

            txn.change_prop("svn:log", &format!("Commit {}", i))
                .unwrap();
            txn.change_prop("svn:author", "test-user").unwrap();

            let rev = txn.commit().unwrap();
            revisions.push(rev);
        }

        // Test iter_logs
        let entries: Vec<crate::Revnum> = session
            .iter_logs(
                &[""],
                revisions[0],
                *revisions.last().unwrap(),
                &GetLogOptions::default()
                    .with_discover_changed_paths(true)
                    .with_revprops(Some(&["svn:log", "svn:author"])),
            )
            .map(|e| e.unwrap().revision().unwrap())
            .collect();

        assert_eq!(entries.len(), 3);
    }

    #[test]
    fn test_iter_logs_early_drop() {
        let (_temp_dir, repo, mut session, _callbacks) = create_test_repo_with_session();

        let fs = repo.fs().unwrap();

        // Create several commits
        for i in 1..=5 {
            let base_rev = crate::Revnum::from((i - 1) as u32);

            let mut txn = fs.begin_txn(base_rev, 0).unwrap();
            let mut root = txn.root().unwrap();

            let filename = format!("/file{}.txt", i);
            root.make_file(filename.as_str()).unwrap();
            let mut stream = root.apply_text(filename.as_str(), None).unwrap();
            use std::io::Write;
            writeln!(stream, "Content {}", i).unwrap();
            drop(stream);

            txn.change_prop("svn:log", &format!("Commit {}", i))
                .unwrap();
            let _rev = txn.commit().unwrap();
        }

        // Take only the first 2 entries, then drop the iterator
        {
            let mut iter = session.iter_logs(
                &[""],
                crate::Revnum::from(1u32),
                crate::Revnum::from(5u32),
                &GetLogOptions::default(),
            );
            let first = iter.next().unwrap().unwrap();
            assert!(first.revision().is_some());
            let second = iter.next().unwrap().unwrap();
            assert!(second.revision().is_some());
            // Drop iter here — should cancel remaining retrieval
        }

        // Session should still be usable after iterator is dropped
        let latest = session.get_latest_revnum().unwrap();
        assert_eq!(latest, crate::Revnum::from(5u32));
    }

    #[test]
    fn test_iter_logs_error() {
        let (_temp_dir, _repo, mut session, _callbacks) = create_test_repo_with_session();

        // Request logs for a non-existent revision range — should yield an error
        let mut iter = session.iter_logs(
            &[""],
            crate::Revnum::from(0u32),
            crate::Revnum::from(1000u32),
            &GetLogOptions::default(),
        );
        let result = iter.next();
        assert!(
            result.is_some(),
            "iter_logs should yield an error, not be empty"
        );
        match result.unwrap() {
            Ok(_) => panic!("expected an error, got Ok"),
            Err(e) => {
                let msg = e.to_string();
                assert!(
                    msg.contains("1000"),
                    "error should mention the invalid revision: {}",
                    msg
                );
            }
        }
    }

    #[test]
    fn test_replay() {
        let (_temp_dir, repo, mut session, _callbacks) = create_test_repo_with_session();

        // Create a file so there's something to replay
        let fs = repo.fs().unwrap();
        let mut txn = fs.begin_txn(crate::Revnum::from(0u32), 0).unwrap();
        let mut root = txn.root().unwrap();
        root.make_file("/test.txt").unwrap();
        {
            let mut stream = root.apply_text("/test.txt", None).unwrap();
            use std::io::Write;
            stream.write_all(b"Hello\n").unwrap();
        }
        txn.commit().unwrap();

        // Replay revision 1 using a default (no-op) editor
        let pool = apr::Pool::new();
        let mut editor = crate::delta::default_editor(pool);
        let result = session.replay(crate::Revnum(1), crate::Revnum(0), true, &mut editor);
        assert!(result.is_ok(), "replay should succeed: {:?}", result.err());
    }

    #[test]
    fn test_replay_range() {
        let (_temp_dir, repo, mut session, _callbacks) = create_test_repo_with_session();

        // Create two revisions
        let fs = repo.fs().unwrap();
        let mut txn = fs.begin_txn(crate::Revnum::from(0u32), 0).unwrap();
        let mut root = txn.root().unwrap();
        root.make_file("/test.txt").unwrap();
        {
            let mut stream = root.apply_text("/test.txt", None).unwrap();
            use std::io::Write;
            stream.write_all(b"Rev 1\n").unwrap();
        }
        txn.commit().unwrap();

        let mut txn = fs.begin_txn(crate::Revnum::from(1u32), 0).unwrap();
        let mut root = txn.root().unwrap();
        {
            let mut stream = root.apply_text("/test.txt", None).unwrap();
            use std::io::Write;
            stream.write_all(b"Rev 2\n").unwrap();
        }
        txn.commit().unwrap();

        // Replay the range, tracking which revisions we see
        let mut revisions_started = Vec::new();
        let mut revisions_finished = Vec::new();

        let result = session.replay_range(
            crate::Revnum(1),
            crate::Revnum(2),
            crate::Revnum(0),
            true,
            |rev, _rev_props| {
                revisions_started.push(rev.0);
                let pool = apr::Pool::new();
                Ok(crate::delta::default_editor(pool))
            },
            |rev, _rev_props, _editor| {
                revisions_finished.push(rev.0);
                Ok(())
            },
        );

        assert!(
            result.is_ok(),
            "replay_range should succeed: {:?}",
            result.err()
        );
        assert_eq!(revisions_started, vec![1, 2]);
        assert_eq!(revisions_finished, vec![1, 2]);
    }

    #[test]
    fn test_replay_range_receives_rev_props() {
        let (_temp_dir, repo, mut session, _callbacks) = create_test_repo_with_session();

        let fs = repo.fs().unwrap();
        let mut txn = fs.begin_txn(crate::Revnum::from(0u32), 0).unwrap();
        let mut root = txn.root().unwrap();
        root.make_file("/test.txt").unwrap();
        {
            let mut stream = root.apply_text("/test.txt", None).unwrap();
            use std::io::Write;
            stream.write_all(b"content\n").unwrap();
        }
        txn.commit().unwrap();

        let mut start_props = Vec::new();
        let mut finish_props = Vec::new();

        session
            .replay_range(
                crate::Revnum(1),
                crate::Revnum(1),
                crate::Revnum(0),
                false,
                |_rev, rev_props| {
                    start_props.push(rev_props.clone());
                    let pool = apr::Pool::new();
                    Ok(crate::delta::default_editor(pool))
                },
                |_rev, rev_props, _editor| {
                    finish_props.push(rev_props.clone());
                    Ok(())
                },
            )
            .unwrap();

        assert_eq!(start_props.len(), 1);
        assert_eq!(finish_props.len(), 1);
        // Rev props should contain at least svn:date and svn:author
        assert!(
            start_props[0].contains_key("svn:date"),
            "rev_props should contain svn:date, got keys: {:?}",
            start_props[0].keys().collect::<Vec<_>>()
        );
    }

    #[test]
    fn test_replay_with_rust_editor() {
        use std::cell::RefCell;
        use std::rc::Rc;

        let (_temp_dir, repo, mut session, _callbacks) = create_test_repo_with_session();

        let fs = repo.fs().unwrap();
        let mut txn = fs.begin_txn(crate::Revnum::from(0u32), 0).unwrap();
        let mut root = txn.root().unwrap();
        root.make_file("/hello.txt").unwrap();
        {
            let mut stream = root.apply_text("/hello.txt", None).unwrap();
            use std::io::Write;
            stream.write_all(b"hello\n").unwrap();
        }
        txn.commit().unwrap();

        // Use a recording editor via from_rust_editor
        let ops = Rc::new(RefCell::new(Vec::<String>::new()));

        struct RecordingEditor {
            ops: Rc<RefCell<Vec<String>>>,
        }
        struct RecordingDir {
            ops: Rc<RefCell<Vec<String>>>,
        }
        struct RecordingFile {
            ops: Rc<RefCell<Vec<String>>>,
        }

        impl crate::delta::Editor for RecordingEditor {
            type RootEditor = RecordingDir;
            fn set_target_revision(
                &mut self,
                revision: Option<crate::Revnum>,
            ) -> Result<(), crate::Error<'_>> {
                self.ops
                    .borrow_mut()
                    .push(format!("set_target_revision({:?})", revision));
                Ok(())
            }
            fn open_root(
                &mut self,
                base_revision: Option<crate::Revnum>,
            ) -> Result<RecordingDir, crate::Error<'_>> {
                self.ops
                    .borrow_mut()
                    .push(format!("open_root({:?})", base_revision));
                Ok(RecordingDir {
                    ops: self.ops.clone(),
                })
            }
            fn close(&mut self) -> Result<(), crate::Error<'_>> {
                self.ops.borrow_mut().push("close_edit".to_string());
                Ok(())
            }
            fn abort(&mut self) -> Result<(), crate::Error<'_>> {
                Ok(())
            }
        }

        impl crate::delta::DirectoryEditor for RecordingDir {
            type SubDirectory = RecordingDir;
            type File = RecordingFile;
            fn delete_entry(
                &mut self,
                _path: &str,
                _rev: Option<crate::Revnum>,
            ) -> Result<(), crate::Error<'_>> {
                Ok(())
            }
            fn add_directory(
                &mut self,
                path: &str,
                _copyfrom: Option<(&str, crate::Revnum)>,
            ) -> Result<RecordingDir, crate::Error<'_>> {
                self.ops
                    .borrow_mut()
                    .push(format!("add_directory({})", path));
                Ok(RecordingDir {
                    ops: self.ops.clone(),
                })
            }
            fn open_directory(
                &mut self,
                path: &str,
                _base_revision: Option<crate::Revnum>,
            ) -> Result<RecordingDir, crate::Error<'_>> {
                self.ops
                    .borrow_mut()
                    .push(format!("open_directory({})", path));
                Ok(RecordingDir {
                    ops: self.ops.clone(),
                })
            }
            fn change_prop(
                &mut self,
                _name: &str,
                _value: Option<&[u8]>,
            ) -> Result<(), crate::Error<'_>> {
                Ok(())
            }
            fn close(&mut self) -> Result<(), crate::Error<'_>> {
                Ok(())
            }
            fn absent_directory(&mut self, _path: &str) -> Result<(), crate::Error<'_>> {
                Ok(())
            }
            fn add_file(
                &mut self,
                path: &str,
                _copyfrom: Option<(&str, crate::Revnum)>,
            ) -> Result<RecordingFile, crate::Error<'_>> {
                self.ops.borrow_mut().push(format!("add_file({})", path));
                Ok(RecordingFile {
                    ops: self.ops.clone(),
                })
            }
            fn open_file(
                &mut self,
                path: &str,
                _base_revision: Option<crate::Revnum>,
            ) -> Result<RecordingFile, crate::Error<'_>> {
                self.ops.borrow_mut().push(format!("open_file({})", path));
                Ok(RecordingFile {
                    ops: self.ops.clone(),
                })
            }
            fn absent_file(&mut self, _path: &str) -> Result<(), crate::Error<'_>> {
                Ok(())
            }
        }

        impl crate::delta::FileEditor for RecordingFile {
            fn apply_textdelta(
                &mut self,
                _base_checksum: Option<&str>,
            ) -> Result<
                Box<
                    dyn for<'a> Fn(
                        &'a mut crate::delta::TxDeltaWindow,
                    ) -> Result<(), crate::Error<'static>>,
                >,
                crate::Error<'static>,
            > {
                self.ops.borrow_mut().push("apply_textdelta".to_string());
                Ok(Box::new(|_| Ok(())))
            }
            fn change_prop(
                &mut self,
                _name: &str,
                _value: Option<&[u8]>,
            ) -> Result<(), crate::Error<'static>> {
                Ok(())
            }
            fn close(&mut self, _text_checksum: Option<&str>) -> Result<(), crate::Error<'static>> {
                Ok(())
            }
        }

        let editor = RecordingEditor { ops: ops.clone() };
        let mut wrap = crate::delta::WrapEditor::from_rust_editor(editor);

        session
            .replay(crate::Revnum(1), crate::Revnum(0), true, &mut wrap)
            .unwrap();

        let recorded = ops.borrow();
        // Replay of rev 1 should have driven the editor with at least open_root and add_file
        assert!(
            recorded.iter().any(|s| s.starts_with("open_root")),
            "replay should have called open_root, got: {:?}",
            *recorded
        );
        assert!(
            recorded.iter().any(|s| s.contains("hello.txt")),
            "replay should have referenced hello.txt, got: {:?}",
            *recorded
        );
    }

    #[test]
    fn test_lock_unlock() {
        let (_temp_dir, repo, mut session, _callbacks) = create_test_repo_with_session();

        // Create a file to lock
        let fs = repo.fs().unwrap();
        let mut txn = fs.begin_txn(crate::Revnum(0), 0).unwrap();
        let mut root = txn.root().unwrap();

        root.make_file("/lockable.txt").unwrap();
        let mut stream = root.apply_text("/lockable.txt", None).unwrap();
        use std::io::Write;
        stream.write_all(b"Content to lock\n").unwrap();
        drop(stream);

        let rev = txn.commit().unwrap();

        // Test locking (RA methods use repository-relative paths)
        let mut lock_paths = HashMap::new();
        lock_paths.insert("lockable.txt".to_string(), rev);

        // Use Rc<RefCell<>> to capture mutable state in the closure
        let lock_called = std::rc::Rc::new(std::cell::RefCell::new(false));
        let received_token = std::rc::Rc::new(std::cell::RefCell::new(String::new()));

        let lock_called_clone = lock_called.clone();
        let received_token_clone = received_token.clone();

        session
            .lock(
                &lock_paths,
                "Test lock comment",
                false, // steal_lock
                move |path, locked, lock, error| {
                    *lock_called_clone.borrow_mut() = true;
                    if locked {
                        let lock = lock.expect("lock callback should provide lock when locked");
                        println!("Locked path: {} with token: {}", path, lock.token());
                        *received_token_clone.borrow_mut() = lock.token().to_string();
                    } else if let Some(err) = error {
                        println!("Failed to lock {}: {:?}", path, err.message());
                    }
                    Ok(())
                },
            )
            .unwrap();

        // Verify the callback was called and we got a token
        assert!(
            *lock_called.borrow(),
            "Lock callback should have been called"
        );
        assert!(
            !received_token.borrow().is_empty(),
            "Should have received a lock token"
        );

        // Test get_lock
        let lock = session
            .get_lock("lockable.txt")
            .unwrap()
            .expect("Lock should exist");
        assert_eq!(lock.path(), "/lockable.txt"); // SVN returns absolute paths within repo
        assert!(!lock.token().is_empty());

        // Test unlock using the lock token we got
        let mut unlock_paths = HashMap::new();
        unlock_paths.insert("lockable.txt".to_string(), lock.token().to_string());

        session
            .unlock(
                &unlock_paths,
                false, // break_lock
                |path, unlocked, _lock, error| {
                    if unlocked {
                        println!("Unlocked path: {}", path);
                    } else if let Some(err) = error {
                        println!("Failed to unlock {}: {:?}", path, err.message());
                    }
                    Ok(())
                },
            )
            .unwrap();
    }

    #[test]
    fn test_get_locations() {
        let (_temp_dir, repo, mut session, _callbacks) = create_test_repo_with_session();

        // Create a file and track its location across revisions
        let fs = repo.fs().unwrap();

        // Rev 1: Create file
        let mut txn = fs.begin_txn(crate::Revnum(0), 0).unwrap();
        let mut root = txn.root().unwrap();
        root.make_file("/original.txt").unwrap();
        let mut stream = root.apply_text("/original.txt", None).unwrap();
        use std::io::Write;
        stream.write_all(b"Original content\n").unwrap();
        drop(stream);
        let rev1 = txn.commit().unwrap();

        // Rev 2: Modify file
        let mut txn = fs.begin_txn(rev1, 0).unwrap();
        let mut root = txn.root().unwrap();
        let mut stream = root.apply_text("/original.txt", None).unwrap();
        stream.write_all(b"Modified content\n").unwrap();
        drop(stream);
        let rev2 = txn.commit().unwrap();

        // Get locations for the file at different revisions (RA methods use repository-relative paths)
        let locations = session
            .get_locations(
                "original.txt",
                rev2,          // peg_revision
                &[rev1, rev2], // location_revisions
            )
            .unwrap();

        // Check that we got locations for both revisions
        assert!(locations.contains_key(&rev1));
        assert!(locations.contains_key(&rev2));
        assert_eq!(locations.get(&rev1).unwrap(), "/original.txt");
        assert_eq!(locations.get(&rev2).unwrap(), "/original.txt");
    }

    #[test]
    fn test_get_dir_props() {
        let (_temp_dir, _repo, mut session, _callbacks) = create_test_repo_with_session();

        // Test get_dir_props on the root directory (empty repository)
        let (props, fetched_rev) = session.get_dir_props("", crate::Revnum(0)).unwrap();

        // In an empty repository, root might have some built-in properties
        // The test validates that the API works correctly

        // Verify that the fetched revision is valid (should be 0 for an empty repo)
        assert_eq!(fetched_rev.0, 0);

        // Verify we got a valid props HashMap (may be empty or contain default properties)
        assert!(props.is_empty() || !props.is_empty());

        // The function should succeed even if no custom properties are set
        // This tests the API without requiring repository modification
    }

    #[test]
    fn test_do_diff_options_builder() {
        // DoDiffOptions decouples the borrow from the editor's own lifetime,
        // so it can be built, inspected, and dropped in a normal scope.
        let mut editor = crate::delta::default_editor(apr::Pool::new());
        let opts = DoDiffOptions::new("file:///versus", &mut editor)
            .with_depth(crate::Depth::Files)
            .with_ignore_ancestry(true)
            .with_text_deltas(false);
        assert_eq!(opts.depth, crate::Depth::Files);
        assert!(opts.ignore_ancestry);
        assert!(!opts.text_deltas);
        assert_eq!(opts.versus_url, "file:///versus");
    }

    #[test]
    fn test_do_diff_options_defaults() {
        let mut editor = crate::delta::default_editor(apr::Pool::new());
        let opts = DoDiffOptions::new("file:///v", &mut editor);
        assert_eq!(opts.depth, crate::Depth::Infinity);
        assert!(!opts.ignore_ancestry);
        assert!(opts.text_deltas);
    }

    #[test]
    fn test_change_rev_prop2() {
        let (_temp_dir, _repo, mut session, _callbacks) = create_test_repo_with_session();

        // Test setting a revision property with change_rev_prop2
        let result = session.change_rev_prop2(
            crate::Revnum(0),
            "test:property",
            None, // no old value check
            Some(b"new value"),
        );

        // This might fail if the repository doesn't allow rev prop changes,
        // but the API should work
        match result {
            Ok(()) => {
                // Verify the property was set
                let prop = session.rev_prop(crate::Revnum(0), "test:property").unwrap();
                assert_eq!(prop.unwrap(), b"new value");
            }
            Err(_) => {
                // Expected if hooks prevent rev prop changes
                // The test still validates the API works
            }
        }
    }

    #[test]
    fn test_reparent_and_session_url() {
        let (_temp_dir, repo, mut session, _callbacks) = create_test_repo_with_session();

        // Get the initial session URL
        let initial_url = session.get_session_url().unwrap();
        assert!(initial_url.starts_with("file://"));

        // Create a subdirectory in the repository
        let fs = repo.fs().unwrap();
        let mut txn = fs.begin_txn(crate::Revnum(0), 0).unwrap();
        let mut root = txn.root().unwrap();
        root.make_dir("/subdir").unwrap();
        let _rev = txn.commit().unwrap();

        // Reparent to the subdirectory
        let new_url = format!("{}/subdir", initial_url);
        let result = session.reparent(&new_url);
        assert!(result.is_ok(), "Reparent should succeed: {:?}", result);

        // Verify the session URL changed
        let current_url = session.get_session_url().unwrap();
        assert_eq!(current_url, new_url);
        assert!(current_url.ends_with("/subdir"));

        // Reparent back to the root
        let result = session.reparent(&initial_url);
        assert!(result.is_ok(), "Reparent back should succeed: {:?}", result);

        // Verify we're back at the initial URL
        let final_url = session.get_session_url().unwrap();
        assert_eq!(final_url, initial_url);
    }

    #[test]
    fn test_list() {
        use std::sync::atomic::{AtomicUsize, Ordering};

        let (_temp_dir, repo, mut session, _callbacks) = create_test_repo_with_session();

        // Create some files and directories in the repository
        let fs = repo.fs().unwrap();
        let mut txn = fs.begin_txn(crate::Revnum(0), 0).unwrap();
        let mut root = txn.root().unwrap();

        // Create directory structure
        root.make_dir("/dir1").unwrap();
        root.make_dir("/dir2").unwrap();
        root.make_file("/file1.txt").unwrap();
        root.make_file("/dir1/file2.txt").unwrap();

        let rev = txn.commit().unwrap();

        // Test 1: List root directory
        let count = AtomicUsize::new(0);
        let callback = |_path: &str, _dirent: &Dirent| {
            count.fetch_add(1, Ordering::SeqCst);
            Ok(())
        };
        let result = session.list(
            "",
            rev,
            None,
            crate::Depth::Immediates,
            crate::DirentField::all(),
            &callback,
        );
        assert!(result.is_ok(), "List should succeed: {:?}", result);
        // SVN list includes the directory itself (/) plus its immediate children
        assert_eq!(
            count.load(Ordering::SeqCst),
            4,
            "Should have 4 entries (root + 2 dirs + 1 file)"
        );

        // Test 2: List subdirectory with depth=Infinity
        let subdir_count = AtomicUsize::new(0);
        let callback2 = |_path: &str, _dirent: &Dirent| {
            subdir_count.fetch_add(1, Ordering::SeqCst);
            Ok(())
        };
        let result = session.list(
            "dir1",
            rev,
            None,
            crate::Depth::Infinity,
            crate::DirentField::all(),
            &callback2,
        );
        assert!(
            result.is_ok(),
            "List subdirectory should succeed: {:?}",
            result
        );
        // Lists the directory itself plus its file
        assert_eq!(
            subdir_count.load(Ordering::SeqCst),
            2,
            "Should find dir1 and file2.txt"
        );

        // Test 3: List with depth=Empty (just the directory itself)
        let empty_count = AtomicUsize::new(0);
        let callback3 = |_path: &str, _dirent: &Dirent| {
            empty_count.fetch_add(1, Ordering::SeqCst);
            Ok(())
        };
        let result = session.list(
            "",
            rev,
            None,
            crate::Depth::Empty,
            crate::DirentField::all(),
            &callback3,
        );
        assert!(result.is_ok(), "List with depth=Empty should succeed");
        // Depth::Empty lists only the directory itself, not its children
        assert_eq!(
            empty_count.load(Ordering::SeqCst),
            1,
            "Depth::Empty should list only the directory itself"
        );
    }

    #[test]
    fn test_get_dated_revision() {
        let (_temp_dir, repo, mut session, _callbacks) = create_test_repo_with_session();

        // Create some revisions with different timestamps
        let fs = repo.fs().unwrap();

        // Create revision 1
        let mut txn = fs.begin_txn(crate::Revnum(0), 0).unwrap();
        let mut root = txn.root().unwrap();
        root.make_file("/file1.txt").unwrap();
        let rev1 = txn.commit().unwrap();

        // Sleep a bit to ensure different timestamps
        std::thread::sleep(std::time::Duration::from_millis(100));

        // Create revision 2
        let mut txn = fs.begin_txn(rev1, 0).unwrap();
        let mut root = txn.root().unwrap();
        root.make_file("/file2.txt").unwrap();
        let rev2 = txn.commit().unwrap();

        // Get the timestamp of rev1
        let props = fs.revision_proplist(rev1, false).unwrap();
        let rev1_date = props.get("svn:date").unwrap();
        let rev1_time = crate::time::from_cstring(&String::from_utf8_lossy(rev1_date)).unwrap();

        // Query for revision at rev1's timestamp - should return rev1
        let result = session.get_dated_revision(rev1_time.as_micros());
        assert!(
            result.is_ok(),
            "get_dated_revision should succeed: {:?}",
            result
        );
        assert_eq!(
            result.unwrap(),
            rev1,
            "Should return rev1 for rev1's timestamp"
        );

        // Query for current time - should return rev2 (latest)
        let result = session.get_dated_revision(apr::time::Time::now().as_micros());
        assert!(
            result.is_ok(),
            "get_dated_revision for current time should succeed"
        );
        assert_eq!(result.unwrap(), rev2, "Should return rev2 for current time");
    }

    #[test]
    fn test_rev_proplist() {
        let (_temp_dir, repo, mut session, _callbacks) = create_test_repo_with_session();

        // Create a revision
        let fs = repo.fs().unwrap();
        let mut txn = fs.begin_txn(crate::Revnum(0), 0).unwrap();
        let mut root = txn.root().unwrap();
        root.make_file("/file.txt").unwrap();
        let rev = txn.commit().unwrap();

        // Get the revision properties
        let result = session.rev_proplist(rev);
        assert!(result.is_ok(), "rev_proplist should succeed: {:?}", result);

        let props = result.unwrap();
        // Standard SVN revision properties should be present
        assert!(
            props.contains_key("svn:date"),
            "Should have svn:date property"
        );
        // svn:author may not be set in file:// repos without authentication

        // Verify we can read the svn:date property
        let date = props.get("svn:date").unwrap();
        assert!(!date.is_empty(), "svn:date should not be empty");
    }

    #[test]
    // SVN's pre-revprop-change hook runner forks and execs a shell script,
    // which is fork-unsafe on Darwin while the test harness runs many
    // threads: the hook child is intermittently killed by a signal. There
    // is nothing the binding can do about it, so skip this one on macOS.
    #[cfg_attr(target_os = "macos", ignore)]
    fn test_change_revprop() {
        use std::fs;
        use std::io::Write;

        let (_temp_dir, repo, mut session, _callbacks) = create_test_repo_with_session();

        // Create a pre-revprop-change hook to allow revprop changes
        #[allow(unused_mut)]
        let mut hook_path = repo.pre_revprop_change_hook_path();
        #[cfg(windows)]
        hook_path.set_extension("bat");
        let mut hook_file = fs::File::create(&hook_path).unwrap();
        #[cfg(unix)]
        {
            writeln!(hook_file, "#!/bin/sh").unwrap();
            writeln!(hook_file, "exit 0").unwrap();
        }
        #[cfg(windows)]
        {
            writeln!(hook_file, "@exit 0").unwrap();
        }
        hook_file.sync_all().unwrap();
        drop(hook_file); // Close the file before changing permissions
                         // Make it executable
        #[cfg(unix)]
        {
            use std::os::unix::fs::PermissionsExt;
            let mut perms = fs::metadata(&hook_path).unwrap().permissions();
            perms.set_mode(0o755);
            fs::set_permissions(&hook_path, perms).unwrap();
        }

        // Create a revision
        let fs_obj = repo.fs().unwrap();
        let mut txn = fs_obj.begin_txn(crate::Revnum(0), 0).unwrap();
        let mut root = txn.root().unwrap();
        root.make_file("/file.txt").unwrap();
        let rev = txn.commit().unwrap();

        // Set a custom revision property
        let prop_name = "custom:test";
        let prop_value = b"test value";

        let result = session.change_revprop(rev, prop_name, None, prop_value);
        assert!(
            result.is_ok(),
            "change_revprop should succeed: {:?}",
            result
        );

        // Verify the property was set
        let props = session.rev_proplist(rev).unwrap();
        assert!(
            props.contains_key(prop_name),
            "Should have custom:test property"
        );
        assert_eq!(props.get(prop_name).unwrap(), prop_value);

        // A None old value means "no atomicity check": it overwrites even
        // though the property already has a value.
        session
            .change_revprop(rev, prop_name, None, b"overwritten")
            .unwrap();
        assert_eq!(
            session.rev_proplist(rev).unwrap().get(prop_name).unwrap(),
            b"overwritten"
        );

        // A matching old value succeeds the atomicity check.
        session
            .change_revprop(rev, prop_name, Some(b"overwritten"), b"matched")
            .unwrap();
        assert_eq!(
            session.rev_proplist(rev).unwrap().get(prop_name).unwrap(),
            b"matched"
        );

        // A wrong old value fails the atomicity check.
        let result = session.change_revprop(rev, prop_name, Some(b"wrong old value"), b"another");
        assert!(
            result.is_err(),
            "change_revprop with wrong old value should fail"
        );
    }

    #[test]
    fn test_get_session_url() {
        let (_temp_dir, _repo, mut session, _callbacks) = create_test_repo_with_session();

        // Get the session URL
        let url = session.get_session_url().unwrap();

        // Should be a file:// URL
        assert!(url.starts_with("file://"));
        assert!(url.contains("test_repo"));
    }

    #[test]
    fn test_get_commit_editor() {
        use crate::delta::{DirectoryEditor, Editor, FileEditor};
        use std::collections::HashMap;

        let (_temp_dir, repo, mut session, _callbacks) = create_test_repo_with_session();

        // Create an initial revision using FS transaction so we have something to commit against
        let fs = repo.fs().unwrap();
        let mut txn = fs.begin_txn(crate::Revnum(0), 0).unwrap();
        let mut root = txn.root().unwrap();
        root.make_file("/initial.txt").unwrap();
        let base_rev = txn.commit().unwrap();

        // Prepare commit parameters
        let mut revprop_table = HashMap::new();
        revprop_table.insert("svn:log".to_string(), b"Test commit via editor".to_vec());

        let commit_rev = std::cell::RefCell::new(None);
        let commit_callback = |info: &crate::CommitInfo| {
            commit_rev.replace(Some(info.revision()));
            Ok(())
        };

        let lock_tokens = HashMap::new();

        // Get the commit editor
        let result = session.get_commit_editor(revprop_table, &commit_callback, lock_tokens, false);
        assert!(result.is_ok(), "get_commit_editor should succeed");

        let mut editor = result.unwrap();

        let mut root = editor.open_root(Some(base_rev)).unwrap();

        // Add a new file
        let mut file = root.add_file("newfile.txt", None).unwrap();

        // Apply text delta (empty file - just get the handler)
        let _handler = file.apply_textdelta(None).unwrap();
        // The handler is a closure that processes windows, we don't need to call it for empty content

        file.close(None).unwrap();

        root.close().unwrap();

        editor.close().unwrap();

        // Verify commit happened
        let rev = commit_rev.borrow();
        assert!(rev.is_some(), "Commit callback should have been called");
        assert!(
            rev.unwrap().as_u64() > base_rev.as_u64(),
            "Should have created a new revision"
        );
    }

    #[test]
    fn test_do_update() {
        let (_temp_dir, _repo, mut session, _callbacks) = create_test_repo_with_session();

        // Create a simple editor to track operations
        struct UpdateEditor {
            operations: std::rc::Rc<std::cell::RefCell<Vec<String>>>,
        }

        impl crate::delta::Editor for UpdateEditor {
            type RootEditor = UpdateDirEditor;

            fn set_target_revision(
                &mut self,
                revision: Option<crate::Revnum>,
            ) -> Result<(), crate::Error<'_>> {
                eprintln!(
                    "UpdateEditor::set_target_revision called with {:?}",
                    revision
                );
                self.operations
                    .borrow_mut()
                    .push(format!("set_target_revision({:?})", revision));
                Ok(())
            }

            fn open_root(
                &mut self,
                _base_revision: Option<crate::Revnum>,
            ) -> Result<UpdateDirEditor, crate::Error<'_>> {
                eprintln!("UpdateEditor::open_root called");
                self.operations.borrow_mut().push("open_root".to_string());
                Ok(UpdateDirEditor {
                    operations: self.operations.clone(),
                })
            }

            fn close(&mut self) -> Result<(), crate::Error<'_>> {
                self.operations.borrow_mut().push("close".to_string());
                Ok(())
            }

            fn abort(&mut self) -> Result<(), crate::Error<'_>> {
                self.operations.borrow_mut().push("abort".to_string());
                Ok(())
            }
        }

        struct UpdateDirEditor {
            operations: std::rc::Rc<std::cell::RefCell<Vec<String>>>,
        }

        impl crate::delta::DirectoryEditor for UpdateDirEditor {
            type SubDirectory = UpdateDirEditor;
            type File = UpdateFileEditor;

            fn delete_entry(
                &mut self,
                path: &str,
                _revision: Option<crate::Revnum>,
            ) -> Result<(), crate::Error<'_>> {
                self.operations
                    .borrow_mut()
                    .push(format!("delete_entry({})", path));
                Ok(())
            }

            fn add_directory(
                &mut self,
                path: &str,
                _copyfrom: Option<(&str, crate::Revnum)>,
            ) -> Result<UpdateDirEditor, crate::Error<'_>> {
                self.operations
                    .borrow_mut()
                    .push(format!("add_directory({})", path));
                Ok(UpdateDirEditor {
                    operations: self.operations.clone(),
                })
            }

            fn open_directory(
                &mut self,
                path: &str,
                _base_revision: Option<crate::Revnum>,
            ) -> Result<UpdateDirEditor, crate::Error<'_>> {
                self.operations
                    .borrow_mut()
                    .push(format!("open_directory({})", path));
                Ok(UpdateDirEditor {
                    operations: self.operations.clone(),
                })
            }

            fn change_prop(
                &mut self,
                name: &str,
                _value: Option<&[u8]>,
            ) -> Result<(), crate::Error<'_>> {
                self.operations
                    .borrow_mut()
                    .push(format!("change_prop({})", name));
                Ok(())
            }

            fn add_file(
                &mut self,
                path: &str,
                _copyfrom: Option<(&str, crate::Revnum)>,
            ) -> Result<UpdateFileEditor, crate::Error<'_>> {
                self.operations
                    .borrow_mut()
                    .push(format!("add_file({})", path));
                Ok(UpdateFileEditor {
                    operations: self.operations.clone(),
                })
            }

            fn open_file(
                &mut self,
                path: &str,
                _base_revision: Option<crate::Revnum>,
            ) -> Result<UpdateFileEditor, crate::Error<'_>> {
                self.operations
                    .borrow_mut()
                    .push(format!("open_file({})", path));
                Ok(UpdateFileEditor {
                    operations: self.operations.clone(),
                })
            }

            fn absent_file(&mut self, path: &str) -> Result<(), crate::Error<'_>> {
                self.operations
                    .borrow_mut()
                    .push(format!("absent_file({})", path));
                Ok(())
            }

            fn absent_directory(&mut self, path: &str) -> Result<(), crate::Error<'_>> {
                self.operations
                    .borrow_mut()
                    .push(format!("absent_directory({})", path));
                Ok(())
            }

            fn close(&mut self) -> Result<(), crate::Error<'_>> {
                self.operations
                    .borrow_mut()
                    .push("close_directory".to_string());
                Ok(())
            }
        }

        struct UpdateFileEditor {
            operations: std::rc::Rc<std::cell::RefCell<Vec<String>>>,
        }

        impl crate::delta::FileEditor for UpdateFileEditor {
            fn apply_textdelta(
                &mut self,
                _base_checksum: Option<&str>,
            ) -> Result<
                Box<
                    dyn for<'b> Fn(
                        &'b mut crate::delta::TxDeltaWindow,
                    ) -> Result<(), crate::Error<'static>>,
                >,
                crate::Error<'static>,
            > {
                self.operations
                    .borrow_mut()
                    .push("apply_textdelta".to_string());
                Ok(Box::new(|_window| Ok(())))
            }

            fn change_prop(
                &mut self,
                name: &str,
                _value: Option<&[u8]>,
            ) -> Result<(), crate::Error<'static>> {
                self.operations
                    .borrow_mut()
                    .push(format!("file_change_prop({})", name));
                Ok(())
            }

            fn close(&mut self, _text_checksum: Option<&str>) -> Result<(), crate::Error<'static>> {
                self.operations.borrow_mut().push("close_file".to_string());
                Ok(())
            }
        }

        let operations = std::rc::Rc::new(std::cell::RefCell::new(Vec::new()));
        let update_editor = UpdateEditor {
            operations: operations.clone(),
        };
        let mut editor = crate::delta::WrapEditor::from_rust_editor(update_editor);

        // Test do_update - we just verify it can be called and returns a reporter
        // Don't actually drive the update to completion as that requires a fully
        // functional editor implementation
        let result = session.do_update(
            crate::Revnum(0),
            "",
            crate::Depth::Infinity,
            false,
            false,
            &mut editor,
        );
        assert!(result.is_ok(), "do_update should succeed");

        let mut reporter = result.unwrap();

        // Abort the report instead of finishing it to avoid needing a complete editor
        reporter.abort_report().unwrap();
    }

    #[test]
    fn test_do_switch() {
        let (_temp_dir, _repo, mut session, _callbacks) = create_test_repo_with_session();

        // Create a minimal editor (reuse UpdateEditor from test_do_update)
        struct SwitchEditor;

        impl crate::delta::Editor for SwitchEditor {
            type RootEditor = SwitchDirEditor;

            fn set_target_revision(
                &mut self,
                _revision: Option<crate::Revnum>,
            ) -> Result<(), crate::Error<'_>> {
                Ok(())
            }

            fn open_root(
                &mut self,
                _base_revision: Option<crate::Revnum>,
            ) -> Result<SwitchDirEditor, crate::Error<'_>> {
                Ok(SwitchDirEditor)
            }

            fn close(&mut self) -> Result<(), crate::Error<'_>> {
                Ok(())
            }

            fn abort(&mut self) -> Result<(), crate::Error<'_>> {
                Ok(())
            }
        }

        struct SwitchDirEditor;

        impl crate::delta::DirectoryEditor for SwitchDirEditor {
            type SubDirectory = SwitchDirEditor;
            type File = SwitchFileEditor;

            fn delete_entry(
                &mut self,
                _path: &str,
                _revision: Option<crate::Revnum>,
            ) -> Result<(), crate::Error<'_>> {
                Ok(())
            }

            fn add_directory(
                &mut self,
                _path: &str,
                _copyfrom: Option<(&str, crate::Revnum)>,
            ) -> Result<SwitchDirEditor, crate::Error<'_>> {
                Ok(SwitchDirEditor)
            }

            fn open_directory(
                &mut self,
                _path: &str,
                _base_revision: Option<crate::Revnum>,
            ) -> Result<SwitchDirEditor, crate::Error<'_>> {
                Ok(SwitchDirEditor)
            }

            fn change_prop(
                &mut self,
                _name: &str,
                _value: Option<&[u8]>,
            ) -> Result<(), crate::Error<'_>> {
                Ok(())
            }

            fn add_file(
                &mut self,
                _path: &str,
                _copyfrom: Option<(&str, crate::Revnum)>,
            ) -> Result<SwitchFileEditor, crate::Error<'_>> {
                Ok(SwitchFileEditor)
            }

            fn open_file(
                &mut self,
                _path: &str,
                _base_revision: Option<crate::Revnum>,
            ) -> Result<SwitchFileEditor, crate::Error<'_>> {
                Ok(SwitchFileEditor)
            }

            fn absent_file(&mut self, _path: &str) -> Result<(), crate::Error<'_>> {
                Ok(())
            }

            fn absent_directory(&mut self, _path: &str) -> Result<(), crate::Error<'_>> {
                Ok(())
            }

            fn close(&mut self) -> Result<(), crate::Error<'_>> {
                Ok(())
            }
        }

        struct SwitchFileEditor;

        impl crate::delta::FileEditor for SwitchFileEditor {
            fn apply_textdelta(
                &mut self,
                _base_checksum: Option<&str>,
            ) -> Result<
                Box<
                    dyn for<'b> Fn(
                        &'b mut crate::delta::TxDeltaWindow,
                    ) -> Result<(), crate::Error<'static>>,
                >,
                crate::Error<'static>,
            > {
                Ok(Box::new(|_window| Ok(())))
            }

            fn change_prop(
                &mut self,
                _name: &str,
                _value: Option<&[u8]>,
            ) -> Result<(), crate::Error<'static>> {
                Ok(())
            }

            fn close(&mut self, _text_checksum: Option<&str>) -> Result<(), crate::Error<'static>> {
                Ok(())
            }
        }

        let switch_editor = SwitchEditor;
        let mut editor = crate::delta::WrapEditor::from_rust_editor(switch_editor);

        // Get the session URL to use as switch URL
        let url = session.get_session_url().unwrap();

        // Test do_switch - verify it can be called and returns a reporter
        let result = session.do_switch(
            crate::Revnum(0),
            "",
            crate::Depth::Infinity,
            &url,
            false,
            false,
            &mut editor,
        );
        assert!(result.is_ok(), "do_switch should succeed");

        let mut reporter = result.unwrap();
        reporter.abort_report().unwrap();
    }

    #[test]
    fn test_do_status() {
        let (_temp_dir, _repo, mut session, _callbacks) = create_test_repo_with_session();

        // Create a minimal editor
        struct StatusEditor;

        impl crate::delta::Editor for StatusEditor {
            type RootEditor = StatusDirEditor;

            fn set_target_revision(
                &mut self,
                _revision: Option<crate::Revnum>,
            ) -> Result<(), crate::Error<'_>> {
                Ok(())
            }

            fn open_root(
                &mut self,
                _base_revision: Option<crate::Revnum>,
            ) -> Result<StatusDirEditor, crate::Error<'_>> {
                Ok(StatusDirEditor)
            }

            fn close(&mut self) -> Result<(), crate::Error<'_>> {
                Ok(())
            }

            fn abort(&mut self) -> Result<(), crate::Error<'_>> {
                Ok(())
            }
        }

        struct StatusDirEditor;

        impl crate::delta::DirectoryEditor for StatusDirEditor {
            type SubDirectory = StatusDirEditor;
            type File = StatusFileEditor;

            fn delete_entry(
                &mut self,
                _path: &str,
                _revision: Option<crate::Revnum>,
            ) -> Result<(), crate::Error<'_>> {
                Ok(())
            }

            fn add_directory(
                &mut self,
                _path: &str,
                _copyfrom: Option<(&str, crate::Revnum)>,
            ) -> Result<StatusDirEditor, crate::Error<'_>> {
                Ok(StatusDirEditor)
            }

            fn open_directory(
                &mut self,
                _path: &str,
                _base_revision: Option<crate::Revnum>,
            ) -> Result<StatusDirEditor, crate::Error<'_>> {
                Ok(StatusDirEditor)
            }

            fn change_prop(
                &mut self,
                _name: &str,
                _value: Option<&[u8]>,
            ) -> Result<(), crate::Error<'_>> {
                Ok(())
            }

            fn add_file(
                &mut self,
                _path: &str,
                _copyfrom: Option<(&str, crate::Revnum)>,
            ) -> Result<StatusFileEditor, crate::Error<'_>> {
                Ok(StatusFileEditor)
            }

            fn open_file(
                &mut self,
                _path: &str,
                _base_revision: Option<crate::Revnum>,
            ) -> Result<StatusFileEditor, crate::Error<'_>> {
                Ok(StatusFileEditor)
            }

            fn absent_file(&mut self, _path: &str) -> Result<(), crate::Error<'_>> {
                Ok(())
            }

            fn absent_directory(&mut self, _path: &str) -> Result<(), crate::Error<'_>> {
                Ok(())
            }

            fn close(&mut self) -> Result<(), crate::Error<'_>> {
                Ok(())
            }
        }

        struct StatusFileEditor;

        impl crate::delta::FileEditor for StatusFileEditor {
            fn apply_textdelta(
                &mut self,
                _base_checksum: Option<&str>,
            ) -> Result<
                Box<
                    dyn for<'b> Fn(
                        &'b mut crate::delta::TxDeltaWindow,
                    ) -> Result<(), crate::Error<'static>>,
                >,
                crate::Error<'static>,
            > {
                Ok(Box::new(|_window| Ok(())))
            }

            fn change_prop(
                &mut self,
                _name: &str,
                _value: Option<&[u8]>,
            ) -> Result<(), crate::Error<'static>> {
                Ok(())
            }

            fn close(&mut self, _text_checksum: Option<&str>) -> Result<(), crate::Error<'static>> {
                Ok(())
            }
        }

        let status_editor = StatusEditor;
        let mut editor = crate::delta::WrapEditor::from_rust_editor(status_editor);

        // Test do_status - verify it can be called
        let result = session.do_status("", crate::Revnum(0), crate::Depth::Infinity, &mut editor);
        assert!(result.is_ok(), "do_status should succeed");
    }

    #[test]
    fn test_callbacks_set_cancel_func() {
        // Test that set_cancel_func can be called and callbacks are set up correctly
        let mut callbacks = Callbacks::new().unwrap();

        let cancel_called = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false));
        let cancel_called_clone = cancel_called.clone();

        callbacks.set_cancel_func(move || {
            cancel_called_clone.store(true, std::sync::atomic::Ordering::SeqCst);
            Ok(())
        });

        // Verify the callback function pointer is set
        unsafe {
            assert!((*callbacks.ptr).cancel_func.is_some());
        }
    }

    #[test]
    fn test_callbacks_set_progress_func() {
        // Test that set_progress_func can be called and callbacks are set up correctly
        let mut callbacks = Callbacks::new().unwrap();

        let progress_called = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false));
        let progress_called_clone = progress_called.clone();

        callbacks.set_progress_func(move |_progress, _total| {
            progress_called_clone.store(true, std::sync::atomic::Ordering::SeqCst);
        });

        // Verify the callback function pointer is set
        unsafe {
            assert!((*callbacks.ptr).progress_func.is_some());
            assert!(!(*callbacks.ptr).progress_baton.is_null());
        }
    }

    #[test]
    fn test_wrap_reporter_from_rust_reporter() {
        use std::sync::{Arc, Mutex};

        struct TestReporter {
            calls: Arc<Mutex<Vec<String>>>,
        }

        impl Reporter for TestReporter {
            fn set_path(
                &mut self,
                path: &str,
                _rev: crate::Revnum,
                _depth: crate::Depth,
                _start_empty: bool,
                _lock_token: &str,
            ) -> Result<(), crate::Error<'static>> {
                self.calls
                    .lock()
                    .unwrap()
                    .push(format!("set_path:{}", path));
                Ok(())
            }

            fn delete_path(&mut self, path: &str) -> Result<(), crate::Error<'static>> {
                self.calls
                    .lock()
                    .unwrap()
                    .push(format!("delete_path:{}", path));
                Ok(())
            }

            fn link_path(
                &mut self,
                path: &str,
                url: &str,
                _rev: crate::Revnum,
                _depth: crate::Depth,
                _start_empty: bool,
                _lock_token: &str,
            ) -> Result<(), crate::Error<'static>> {
                self.calls
                    .lock()
                    .unwrap()
                    .push(format!("link_path:{}:{}", path, url));
                Ok(())
            }

            fn finish_report(&mut self) -> Result<(), crate::Error<'static>> {
                self.calls.lock().unwrap().push("finish_report".to_string());
                Ok(())
            }

            fn abort_report(&mut self) -> Result<(), crate::Error<'static>> {
                self.calls.lock().unwrap().push("abort_report".to_string());
                Ok(())
            }
        }

        let calls = Arc::new(Mutex::new(Vec::new()));
        let reporter = TestReporter {
            calls: calls.clone(),
        };

        let wrap = WrapReporter::from_rust_reporter(reporter);

        // Verify the reporter and baton pointers are valid
        assert!(!wrap.as_ptr().is_null());
        assert!(!wrap.as_baton().is_null());

        // Call through the C vtable to verify forwarding works
        unsafe {
            let vtable = &*wrap.as_ptr();

            let path = std::ffi::CString::new("test/path").unwrap();
            let lock_token = std::ffi::CString::new("").unwrap();
            let pool = apr::Pool::new();

            let err = vtable.set_path.unwrap()(
                wrap.as_baton(),
                path.as_ptr(),
                1,
                subversion_sys::svn_depth_t_svn_depth_infinity,
                0,
                lock_token.as_ptr(),
                pool.as_mut_ptr(),
            );
            assert!(err.is_null());

            let err =
                vtable.delete_path.unwrap()(wrap.as_baton(), path.as_ptr(), pool.as_mut_ptr());
            assert!(err.is_null());

            let err = vtable.finish_report.unwrap()(wrap.as_baton(), pool.as_mut_ptr());
            assert!(err.is_null());
        }

        let recorded = calls.lock().unwrap();
        assert_eq!(recorded.len(), 3);
        assert_eq!(recorded[0], "set_path:test/path");
        assert_eq!(recorded[1], "delete_path:test/path");
        assert_eq!(recorded[2], "finish_report");
    }
}
