//! High-level Subversion client operations.
//!
//! This module provides the [`Context`](crate::client::Context) type and related functionality for performing
//! Subversion client operations such as checkout, update, commit, diff, merge, and more.
//!
//! # Overview
//!
//! The client module is the primary interface for applications that need to interact with
//! Subversion repositories as a client. It provides high-level operations that handle
//! authentication, conflict resolution, and other complex workflows automatically.
//!
//! ## Core Operations
//!
//! - **Repository operations**: checkout, update, switch, relocate
//! - **Working copy modifications**: add, delete, copy, move, mkdir
//! - **Committing changes**: commit with callbacks and revision properties
//! - **Querying**: status, log, info, list, cat
//! - **Comparison**: diff (between revisions, URLs, or working copy)
//! - **Merging**: merge with conflict detection and resolution
//! - **Properties**: get, set, list both versioned and revision properties
//! - **Maintenance**: cleanup, vacuum, upgrade
//!
//! # Example
//!
//! ```no_run
//! use subversion::client::Context;
//!
//! let mut ctx = Context::new().unwrap();
//!
//! // Checkout a repository
//! ctx.checkout(
//!     "https://svn.example.com/repo/trunk",
//!     "/path/to/working/copy",
//!     None, // HEAD revision
//!     true, // recursive
//! ).unwrap();
//!
//! // Get status of working copy
//! ctx.status("/path/to/working/copy", None, true, false, false, false, false, false, |status| {
//!     println!("{}: {:?}", status.path().display(), status.node_status());
//! }).unwrap();
//! ```

use crate::dirent::AsCanonicalDirent;
use crate::io::Dirent;
use crate::uri::AsCanonicalUri;
use crate::{
    svn_result, with_tmp_pool, Depth, Error, LogEntry, OwnedLogEntry, Revision, RevisionRange,
    Revnum, Version,
};
use apr::Pool;

use std::collections::HashMap;
use std::ops::ControlFlow;
use subversion_sys::svn_error_t;
use subversion_sys::{
    svn_client_add5, svn_client_checkout3, svn_client_cleanup2, svn_client_commit6,
    svn_client_conflict_get, svn_client_create_context2, svn_client_ctx_t, svn_client_delete4,
    svn_client_export5, svn_client_import5, svn_client_log5, svn_client_mkdir4,
    svn_client_proplist4, svn_client_status6, svn_client_switch3, svn_client_update4,
    svn_client_vacuum, svn_client_version,
};

/// Validate that a path or URL is absolute (as required by svn_client_info4).
/// URLs are always considered valid. Paths must be absolute.
fn validate_absolute_path_or_url(path_or_url: &str) -> Result<(), Error<'static>> {
    // URLs are valid
    if crate::path::is_url(path_or_url) {
        return Ok(());
    }

    // For paths, check if they are absolute
    let path_std = std::path::Path::new(path_or_url);
    if !path_std.is_absolute() {
        return Err(Error::from_message(&format!(
            "Path must be absolute, got relative path: '{}'",
            path_or_url
        )));
    }

    Ok(())
}

/// Returns the version information for the Subversion client library.
pub fn version() -> Version {
    unsafe { Version(svn_client_version()) }
}

extern "C" fn wrap_filter_callback(
    baton: *mut std::ffi::c_void,
    filtered: *mut subversion_sys::svn_boolean_t,
    local_abspath: *const i8,
    dirent: *const subversion_sys::svn_io_dirent2_t,
    _pool: *mut apr_sys::apr_pool_t,
) -> *mut svn_error_t {
    unsafe {
        let callback = &mut *(baton
            as *mut &mut dyn FnMut(&std::path::Path, &Dirent) -> Result<bool, Error<'static>>);
        let local_abspath: &std::path::Path = std::ffi::CStr::from_ptr(local_abspath)
            .to_str()
            .unwrap()
            .as_ref();
        let ret = callback(local_abspath, &Dirent::from(dirent));
        if let Ok(ret) = ret {
            *filtered = ret as subversion_sys::svn_boolean_t;
            std::ptr::null_mut()
        } else {
            ret.unwrap_err().as_mut_ptr()
        }
    }
}

extern "C" fn wrap_status_func(
    baton: *mut std::ffi::c_void,
    path: *const i8,
    status: *const subversion_sys::svn_client_status_t,
    pool: *mut apr_sys::apr_pool_t,
) -> *mut subversion_sys::svn_error_t {
    unsafe {
        let callback =
            &mut *(baton as *mut &mut dyn FnMut(&str, &Status) -> Result<(), Error<'static>>);
        let local_path = subversion_sys::svn_dirent_local_style(path, pool);
        let path_str = std::ffi::CStr::from_ptr(local_path).to_str().unwrap();
        let pool_handle = apr::PoolHandle::from_borrowed_raw(pool);
        let status_wrapper = Status {
            ptr: status,
            _pool: pool_handle,
        };
        let ret = callback(path_str, &status_wrapper);
        if let Err(err) = ret {
            err.into_raw()
        } else {
            std::ptr::null_mut()
        }
    }
}

/// C trampoline for cancel callbacks
extern "C" fn cancel_trampoline(baton: *mut std::ffi::c_void) -> *mut subversion_sys::svn_error_t {
    if baton.is_null() {
        return std::ptr::null_mut();
    }

    let handler = unsafe { &mut *(baton as *mut Box<dyn FnMut() -> bool + Send>) };
    if handler() {
        // Return SVN_ERR_CANCELLED (200015)
        let err = Error::new(
            apr::Status::from(200015_i32),
            None,
            "Operation cancelled by user",
        );
        unsafe { err.into_raw() }
    } else {
        std::ptr::null_mut()
    }
}

/// C trampoline for log message callbacks
extern "C" fn log_msg_trampoline(
    log_msg: *mut *const std::os::raw::c_char,
    tmp_file: *mut *const std::os::raw::c_char,
    commit_items: *const apr_sys::apr_array_header_t,
    baton: *mut std::ffi::c_void,
    pool: *mut apr_sys::apr_pool_t,
) -> *mut subversion_sys::svn_error_t {
    if baton.is_null() {
        unsafe {
            *log_msg = std::ptr::null();
            *tmp_file = std::ptr::null();
        }
        return std::ptr::null_mut();
    }

    unsafe {
        let callback =
            &mut *(baton as *mut &mut dyn FnMut(&[CommitItem]) -> Result<String, Error<'static>>);

        // Convert the C array of commit items to a Rust slice
        let items_vec = if commit_items.is_null() {
            Vec::new()
        } else {
            let array = &*commit_items;
            let count = array.nelts as usize;
            let elts = array.elts as *const *const subversion_sys::svn_client_commit_item3_t;
            (0..count)
                .map(|i| CommitItem::from_raw(*elts.add(i)))
                .collect::<Vec<_>>()
        };

        match callback(&items_vec) {
            Ok(msg) => {
                // Allocate the message in the provided pool
                let pool_handle = apr::PoolHandle::from_borrowed_raw(pool);
                let duplicated = apr::strings::pstrdup(&msg, &pool_handle).unwrap().as_ptr();
                *log_msg = duplicated;
                *tmp_file = std::ptr::null();
                std::ptr::null_mut()
            }
            Err(err) => {
                *log_msg = std::ptr::null();
                *tmp_file = std::ptr::null();
                err.into_raw()
            }
        }
    }
}

extern "C" fn wrap_proplist_receiver2(
    baton: *mut std::ffi::c_void,
    path: *const i8,
    props: *mut apr::hash::apr_hash_t,
    inherited_props: *mut apr::tables::apr_array_header_t,
    scratch_pool: *mut apr_sys::apr_pool_t,
) -> *mut subversion_sys::svn_error_t {
    unsafe {
        // SVN owns the scratch_pool, we just need to reference it
        let _scratch_pool = apr::PoolHandle::from_borrowed_raw(scratch_pool);
        let callback = baton
            as *mut &mut dyn FnMut(
                &str,
                &HashMap<String, Vec<u8>>,
                Option<&[crate::InheritedItem]>,
            ) -> Result<(), Error<'static>>;
        let callback = &mut *(callback);
        let path: &str = std::ffi::CStr::from_ptr(path).to_str().unwrap();
        let prop_hash = crate::props::PropHash::from_ptr(props);
        let props = prop_hash.to_hashmap();
        let inherited_props = if inherited_props.is_null() {
            None
        } else {
            let inherited_props = apr::tables::TypedArray::<
                *mut subversion_sys::svn_prop_inherited_item_t,
            >::from_ptr(inherited_props);
            Some(
                inherited_props
                    .iter()
                    .map(|x| crate::InheritedItem::from_raw(x))
                    .collect::<Vec<_>>(),
            )
        };
        let ret = callback(path, &props, inherited_props.as_deref());
        if let Err(err) = ret {
            err.into_raw()
        } else {
            std::ptr::null_mut()
        }
    }
}

extern "C" fn wrap_changelist_receiver(
    baton: *mut std::ffi::c_void,
    path: *const std::os::raw::c_char,
    changelist: *const std::os::raw::c_char,
    _pool: *mut apr_sys::apr_pool_t,
) -> *mut subversion_sys::svn_error_t {
    unsafe {
        // SVN can call this with NULL changelist for paths without changelists
        // We only want to report paths that actually have changelists
        if changelist.is_null() {
            return std::ptr::null_mut();
        }

        let callback = baton as *mut &mut dyn FnMut(&str, &str) -> Result<(), Error<'static>>;
        let callback = &mut *callback;

        let path_str = std::ffi::CStr::from_ptr(path).to_str().unwrap();
        let changelist_str = std::ffi::CStr::from_ptr(changelist).to_str().unwrap();

        match callback(path_str, changelist_str) {
            Ok(()) => std::ptr::null_mut(),
            Err(e) => e.into_raw(),
        }
    }
}

/// Information about a versioned item in the repository.
pub struct Info(*const subversion_sys::svn_client_info2_t);

/// Information about a line in a blamed file
pub struct BlameInfo {
    /// Line number in the file.
    pub line_no: i64,
    /// Revision that last modified this line.
    pub revision: Revnum,
    /// Revision properties.
    pub revprops: HashMap<String, Vec<u8>>,
    /// Merged revision if this line came from a merge.
    pub merged_revision: Option<Revnum>,
    /// Properties of the merged revision.
    pub merged_revprops: HashMap<String, Vec<u8>>,
    /// Path of the merged file.
    pub merged_path: Option<String>,
    /// The actual line content.
    pub line: String,
    /// Whether this line has local changes.
    pub local_change: bool,
}

impl Info {
    /// Returns the URL of the item.
    pub fn url(&self) -> &str {
        unsafe {
            let url = (*self.0).URL;
            std::ffi::CStr::from_ptr(url).to_str().unwrap()
        }
    }

    /// Returns the revision number of the item.
    pub fn revision(&self) -> Revnum {
        unsafe { Revnum::from_raw((*self.0).rev).unwrap() }
    }

    /// Returns the node kind (file, directory, etc.) of the item.
    pub fn kind(&self) -> subversion_sys::svn_node_kind_t {
        unsafe { (*self.0).kind }
    }

    /// Returns the repository root URL.
    pub fn repos_root_url(&self) -> &str {
        unsafe {
            let url = (*self.0).repos_root_URL;
            std::ffi::CStr::from_ptr(url).to_str().unwrap()
        }
    }

    /// Returns the repository UUID.
    pub fn repos_uuid(&self) -> &str {
        unsafe {
            let uuid = (*self.0).repos_UUID;
            std::ffi::CStr::from_ptr(uuid).to_str().unwrap()
        }
    }

    /// Returns the last changed revision number.
    /// Returns `None` if the revision is invalid (e.g. for revision 0).
    pub fn last_changed_rev(&self) -> Option<Revnum> {
        Revnum::from_raw(unsafe { (*self.0).last_changed_rev })
    }

    /// Returns the last changed date.
    pub fn last_changed_date(&self) -> apr::time::Time {
        unsafe { (*self.0).last_changed_date.into() }
    }

    /// Returns the last changed author.
    /// Returns `None` if the author is not set (e.g. for revision 0).
    pub fn last_changed_author(&self) -> Option<&str> {
        unsafe {
            let author = (*self.0).last_changed_author;
            if author.is_null() {
                None
            } else {
                Some(std::ffi::CStr::from_ptr(author).to_str().unwrap())
            }
        }
    }

    /// Returns the size of the file.
    /// Only applicable for files, not directories.
    /// Returns -1 (SVN_INVALID_FILESIZE) for working copy paths.
    pub fn size(&self) -> i64 {
        unsafe { (*self.0).size }
    }

    /// Returns working copy information, if available.
    ///
    /// This is `None` for remote (URL) targets.
    pub fn wc_info(&self) -> Option<WcInfo> {
        unsafe {
            let wc = (*self.0).wc_info;
            if wc.is_null() {
                None
            } else {
                Some(WcInfo(wc))
            }
        }
    }
}

/// Information about a working copy item.
pub struct WcInfo(*const subversion_sys::svn_wc_info_t);

/// A WcInfo that owns its backing memory pool.
///
/// Created by duplicating a borrowed `WcInfo` into a new pool so that
/// the data outlives the callback scope it was obtained from.
pub struct OwnedWcInfo {
    info: WcInfo,
    _pool: apr::Pool<'static>,
}

impl WcInfo {
    /// Duplicate this WcInfo into an owned copy that outlives the callback.
    pub fn dup(&self) -> OwnedWcInfo {
        let pool = apr::Pool::new();
        let ptr = unsafe { subversion_sys::svn_wc_info_dup(self.0, pool.as_mut_ptr()) };
        OwnedWcInfo {
            info: WcInfo(ptr),
            _pool: pool,
        }
    }

    /// Returns the schedule of this item (normal, add, delete, replace).
    pub fn schedule(&self) -> subversion_sys::svn_wc_schedule_t {
        unsafe { (*self.0).schedule }
    }

    /// If copied, the URL from which the copy was made.
    pub fn copyfrom_url(&self) -> Option<&str> {
        unsafe {
            let url = (*self.0).copyfrom_url;
            if url.is_null() {
                None
            } else {
                Some(std::ffi::CStr::from_ptr(url).to_str().unwrap())
            }
        }
    }

    /// If copied, the revision from which the copy was made.
    pub fn copyfrom_rev(&self) -> Option<Revnum> {
        Revnum::from_raw(unsafe { (*self.0).copyfrom_rev })
    }

    /// The changelist the item is in, if any.
    pub fn changelist(&self) -> Option<&str> {
        unsafe {
            let cl = (*self.0).changelist;
            if cl.is_null() {
                None
            } else {
                Some(std::ffi::CStr::from_ptr(cl).to_str().unwrap())
            }
        }
    }

    /// The depth of the item.
    pub fn depth(&self) -> subversion_sys::svn_depth_t {
        unsafe { (*self.0).depth }
    }

    /// The size of the file after translation into its local representation,
    /// or `SVN_INVALID_FILESIZE` if unknown.
    pub fn recorded_size(&self) -> i64 {
        unsafe { (*self.0).recorded_size }
    }

    /// The time at which the file had the recorded size and was considered
    /// unmodified.
    pub fn recorded_time(&self) -> apr::time::Time {
        unsafe { (*self.0).recorded_time.into() }
    }

    /// The local absolute path of the working copy root.
    pub fn wcroot_abspath(&self) -> Option<&str> {
        unsafe {
            let p = (*self.0).wcroot_abspath;
            if p.is_null() {
                None
            } else {
                Some(std::ffi::CStr::from_ptr(p).to_str().unwrap())
            }
        }
    }

    /// The path the node was moved from, if it was moved here.
    pub fn moved_from_abspath(&self) -> Option<&str> {
        unsafe {
            let p = (*self.0).moved_from_abspath;
            if p.is_null() {
                None
            } else {
                Some(std::ffi::CStr::from_ptr(p).to_str().unwrap())
            }
        }
    }

    /// The path the node was moved to, if it was moved away.
    pub fn moved_to_abspath(&self) -> Option<&str> {
        unsafe {
            let p = (*self.0).moved_to_abspath;
            if p.is_null() {
                None
            } else {
                Some(std::ffi::CStr::from_ptr(p).to_str().unwrap())
            }
        }
    }
}

impl OwnedWcInfo {
    /// Create an owned copy of a WcInfo by duplicating it into a new pool.
    pub fn from_wc_info(info: &WcInfo) -> Self {
        info.dup()
    }
}

// Safety: the pool and info are co-owned; the info's pointers reference
// memory allocated in the pool, and the pool is only dropped when the
// OwnedWcInfo is dropped.
unsafe impl Send for OwnedWcInfo {}

impl std::ops::Deref for OwnedWcInfo {
    type Target = WcInfo;
    fn deref(&self) -> &Self::Target {
        &self.info
    }
}

extern "C" fn wrap_info_receiver2(
    baton: *mut std::ffi::c_void,
    abspath_or_url: *const i8,
    info: *const subversion_sys::svn_client_info2_t,
    _scatch_pool: *mut apr_sys::apr_pool_t,
) -> *mut subversion_sys::svn_error_t {
    unsafe {
        let callback = &mut *(baton
            as *mut &mut dyn FnMut(&std::path::Path, &Info) -> Result<(), Error<'static>>);
        let abspath_or_url: &std::path::Path = std::ffi::CStr::from_ptr(abspath_or_url)
            .to_str()
            .unwrap()
            .as_ref();
        let ret = callback(abspath_or_url, &Info(info));
        if let Err(err) = ret {
            err.into_raw()
        } else {
            std::ptr::null_mut()
        }
    }
}

/// Callback wrapper for blame receiver
unsafe extern "C" fn blame_receiver_wrapper(
    baton: *mut std::ffi::c_void,
    line_no: apr_sys::apr_int64_t,
    revision: subversion_sys::svn_revnum_t,
    rev_props: *mut apr_sys::apr_hash_t,
    merged_revision: subversion_sys::svn_revnum_t,
    merged_rev_props: *mut apr_sys::apr_hash_t,
    merged_path: *const i8,
    line: *const subversion_sys::svn_string_t,
    local_change: subversion_sys::svn_boolean_t,
    _pool: *mut apr_sys::apr_pool_t,
) -> *mut subversion_sys::svn_error_t {
    let callback = &mut *(baton as *mut &mut dyn FnMut(BlameInfo) -> Result<(), Error<'static>>);

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

    // Extract merged revprops
    let merged_revprops = if !merged_rev_props.is_null() {
        let prop_hash = unsafe { crate::props::PropHash::from_ptr(merged_rev_props) };
        prop_hash.to_hashmap()
    } else {
        HashMap::new()
    };

    // Extract line content
    let line_str = if !line.is_null() {
        let line_ref = &*line;
        let str_data = line_ref.data;
        let str_len = line_ref.len;
        let bytes = std::slice::from_raw_parts(str_data as *const u8, str_len);
        String::from_utf8_lossy(bytes).into_owned()
    } else {
        String::new()
    };

    // Extract merged path
    let merged_path_str = if !merged_path.is_null() {
        Some(
            std::ffi::CStr::from_ptr(merged_path)
                .to_string_lossy()
                .into_owned(),
        )
    } else {
        None
    };

    let info = BlameInfo {
        line_no,
        revision: Revnum::from_raw(revision).unwrap_or(Revnum::from(0u64)),
        revprops,
        merged_revision: Revnum::from_raw(merged_revision),
        merged_revprops,
        merged_path: merged_path_str,
        line: line_str,
        local_change: local_change != 0,
    };

    match callback(info) {
        Ok(()) => std::ptr::null_mut(),
        Err(mut e) => e.as_mut_ptr(),
    }
}

/// Options for cat
#[derive(Debug, Clone, Copy, Default)]
/// Options for the cat operation to retrieve file contents.
pub struct CatOptions {
    /// Revision to retrieve.
    pub revision: Revision,
    /// Peg revision for the path.
    pub peg_revision: Revision,
    /// Whether to expand keywords in the file content.
    pub expand_keywords: bool,
}

/// Options for cleanup
#[derive(Debug, Clone, Copy, Default)]
/// Options for the cleanup operation to remove locks and complete unfinished operations.
pub struct CleanupOptions {
    /// Whether to break locks.
    pub break_locks: bool,
    /// Whether to fix recorded timestamps.
    pub fix_recorded_timestamps: bool,
    /// Whether to clear DAV cache.
    pub clear_dav_cache: bool,
    /// Whether to vacuum pristine copies.
    pub vacuum_pristines: bool,
    /// Whether to include externals.
    pub include_externals: bool,
}

/// Options for proplist
#[derive(Debug, Clone, Copy, Default)]
/// Options for listing properties on versioned items.
pub struct ProplistOptions<'a> {
    /// Peg revision for the path.
    pub peg_revision: Revision,
    /// Revision to list properties from.
    pub revision: Revision,
    /// Depth of the operation.
    pub depth: Depth,
    /// Changelists to filter by.
    pub changelists: Option<&'a [&'a str]>,
    /// Whether to get inherited properties.
    pub get_target_inherited_props: bool,
}

/// Options for export
#[derive(Debug, Clone, Copy, Default)]
/// Options for exporting a tree from the repository.
pub struct ExportOptions {
    /// Peg revision for the path.
    pub peg_revision: Revision,
    /// Revision to export.
    pub revision: Revision,
    /// Whether to overwrite existing files.
    pub overwrite: bool,
    /// Whether to ignore externals.
    pub ignore_externals: bool,
    /// Whether to ignore keywords.
    pub ignore_keywords: bool,
    /// Depth of the export.
    pub depth: Depth,
    /// Native end-of-line style.
    pub native_eol: crate::NativeEOL,
}

/// Options for vacuum
#[derive(Debug, Clone, Copy, Default)]
/// Options for vacuuming the working copy to remove unversioned and ignored items.
pub struct VacuumOptions {
    /// Whether to remove unversioned items.
    pub remove_unversioned_items: bool,
    /// Whether to remove ignored items.
    pub remove_ignored_items: bool,
    /// Whether to fix recorded timestamps.
    pub fix_recorded_timestamps: bool,
    /// Whether to vacuum pristine copies.
    pub vacuum_pristines: bool,
    /// Whether to include externals.
    pub include_externals: bool,
}

/// Options for a checkout
#[derive(Debug, Clone, Copy, Default)]
/// Options for checking out a working copy from a repository.
pub struct CheckoutOptions {
    /// Peg revision for the URL.
    pub peg_revision: Revision,
    /// Revision to check out.
    pub revision: Revision,
    /// Depth of the checkout.
    pub depth: Depth,
    /// Whether to ignore externals.
    pub ignore_externals: bool,
    /// Whether to allow unversioned obstructions.
    pub allow_unver_obstructions: bool,
}

/// Options for an update
#[derive(Debug, Clone, Copy, Default)]
/// Options for updating a working copy to a different revision.
pub struct UpdateOptions {
    /// Depth of the update.
    pub depth: Depth,
    /// Whether the depth setting is sticky.
    pub depth_is_sticky: bool,
    /// Whether to ignore externals.
    pub ignore_externals: bool,
    /// Whether to allow unversioned obstructions.
    pub allow_unver_obstructions: bool,
    /// Whether to treat adds as modifications.
    pub adds_as_modifications: bool,
    /// Whether to create parent directories.
    pub make_parents: bool,
}

/// Options for a switch
#[derive(Debug, Clone, Copy)]
/// Options for switching a working copy to a different URL.
pub struct SwitchOptions {
    /// Peg revision for the URL.
    pub peg_revision: Revision,
    /// Revision to switch to.
    pub revision: Revision,
    /// Depth of the switch.
    pub depth: Depth,
    /// Whether the depth setting is sticky.
    pub depth_is_sticky: bool,
    /// Whether to ignore externals.
    pub ignore_externals: bool,
    /// Whether to allow unversioned obstructions.
    pub allow_unver_obstructions: bool,
    /// Whether to ignore ancestry when comparing trees.
    pub ignore_ancestry: bool,
}

impl Default for SwitchOptions {
    fn default() -> Self {
        Self {
            peg_revision: Revision::Unspecified,
            revision: Revision::Head,
            depth: Depth::Infinity,
            depth_is_sticky: false,
            ignore_externals: false,
            allow_unver_obstructions: false,
            ignore_ancestry: false,
        }
    }
}

impl SwitchOptions {
    /// Creates a new SwitchOptions with default values.
    pub fn new() -> Self {
        Self::default()
    }

    /// Sets the peg revision.
    pub fn with_peg_revision(mut self, peg_revision: Revision) -> Self {
        self.peg_revision = peg_revision;
        self
    }

    /// Sets the revision.
    pub fn with_revision(mut self, revision: Revision) -> Self {
        self.revision = revision;
        self
    }

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

    /// Sets whether the depth is sticky.
    pub fn with_depth_is_sticky(mut self, depth_is_sticky: bool) -> Self {
        self.depth_is_sticky = depth_is_sticky;
        self
    }

    /// Sets whether to ignore externals.
    pub fn with_ignore_externals(mut self, ignore_externals: bool) -> Self {
        self.ignore_externals = ignore_externals;
        self
    }

    /// Sets whether to allow unversioned obstructions.
    pub fn with_allow_unver_obstructions(mut self, allow_unver_obstructions: bool) -> Self {
        self.allow_unver_obstructions = allow_unver_obstructions;
        self
    }

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

/// Options for add
#[derive(Debug, Clone, Copy)]
/// Options for adding files and directories to version control.
pub struct AddOptions {
    /// Depth of the add operation.
    pub depth: Depth,
    /// Whether to force the add.
    pub force: bool,
    /// Whether to add files that match ignore patterns.
    pub no_ignore: bool,
    /// Whether to disable automatic properties.
    pub no_autoprops: bool,
    /// Whether to add parent directories.
    pub add_parents: bool,
}

impl Default for AddOptions {
    fn default() -> Self {
        Self {
            depth: Depth::Infinity,
            force: false,
            no_ignore: false,
            no_autoprops: false,
            add_parents: false,
        }
    }
}

impl AddOptions {
    /// Creates a new AddOptions with default values.
    pub fn new() -> Self {
        Self::default()
    }

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

    /// Sets whether to force the add.
    pub fn with_force(mut self, force: bool) -> Self {
        self.force = force;
        self
    }

    /// Sets whether to add files that match ignore patterns.
    pub fn with_no_ignore(mut self, no_ignore: bool) -> Self {
        self.no_ignore = no_ignore;
        self
    }

    /// Sets whether to disable automatic properties.
    pub fn with_no_autoprops(mut self, no_autoprops: bool) -> Self {
        self.no_autoprops = no_autoprops;
        self
    }

    /// Sets whether to add parent directories.
    pub fn with_add_parents(mut self, add_parents: bool) -> Self {
        self.add_parents = add_parents;
        self
    }
}

/// Options for delete
/// Options for deleting versioned items.
#[derive(Default)]
pub struct DeleteOptions<'a> {
    /// Whether to force deletion even if there are local modifications.
    pub force: bool,
    /// Whether to keep the local copy of the file.
    pub keep_local: bool,
    /// Optional callback to invoke after commit.
    pub commit_callback:
        Option<&'a mut dyn FnMut(&crate::CommitInfo) -> Result<(), Error<'static>>>,
}

impl<'a> DeleteOptions<'a> {
    /// Creates a new DeleteOptions with default values.
    pub fn new() -> Self {
        Self::default()
    }

    /// Sets whether to force deletion.
    pub fn with_force(mut self, force: bool) -> Self {
        self.force = force;
        self
    }

    /// Sets whether to keep the local copy.
    pub fn with_keep_local(mut self, keep_local: bool) -> Self {
        self.keep_local = keep_local;
        self
    }

    /// Sets the commit callback.
    pub fn with_commit_callback(
        mut self,
        callback: &'a mut dyn FnMut(&crate::CommitInfo) -> Result<(), Error<'static>>,
    ) -> Self {
        self.commit_callback = Some(callback);
        self
    }
}

/// Options for copy operations
#[derive(Default)]
pub struct CopyOptions<'a> {
    /// Whether to copy sources as children of the destination.
    pub copy_as_child: bool,
    /// Whether to create parent directories.
    pub make_parents: bool,
    /// Whether to ignore externals.
    pub ignore_externals: bool,
    /// Whether to copy only metadata (not file contents).
    pub metadata_only: bool,
    /// Whether to pin externals.
    pub pin_externals: bool,
    /// Hash mapping external URLs to their pinned revisions.
    pub externals_to_pin: Option<std::collections::HashMap<String, String>>,
    /// Revision properties for the commit.
    pub revprop_table: Option<std::collections::HashMap<String, String>>,
    /// Optional callback to invoke after commit.
    pub commit_callback:
        Option<&'a mut dyn FnMut(&crate::CommitInfo) -> Result<(), Error<'static>>>,
}

impl<'a> CopyOptions<'a> {
    /// Creates a new CopyOptions with default values.
    pub fn new() -> Self {
        Self::default()
    }

    /// Sets whether to copy as child.
    pub fn with_copy_as_child(mut self, copy_as_child: bool) -> Self {
        self.copy_as_child = copy_as_child;
        self
    }

    /// Sets whether to create parent directories.
    pub fn with_make_parents(mut self, make_parents: bool) -> Self {
        self.make_parents = make_parents;
        self
    }

    /// Sets whether to ignore externals.
    pub fn with_ignore_externals(mut self, ignore_externals: bool) -> Self {
        self.ignore_externals = ignore_externals;
        self
    }

    /// Sets whether to copy only metadata.
    pub fn with_metadata_only(mut self, metadata_only: bool) -> Self {
        self.metadata_only = metadata_only;
        self
    }

    /// Sets whether to pin externals.
    pub fn with_pin_externals(mut self, pin_externals: bool) -> Self {
        self.pin_externals = pin_externals;
        self
    }

    /// Sets the externals to pin mapping.
    pub fn with_externals_to_pin(
        mut self,
        externals: std::collections::HashMap<String, String>,
    ) -> Self {
        self.externals_to_pin = Some(externals);
        self
    }

    /// Sets the revision properties.
    pub fn with_revprop_table(
        mut self,
        revprops: std::collections::HashMap<String, String>,
    ) -> Self {
        self.revprop_table = Some(revprops);
        self
    }

    /// Sets the commit callback.
    pub fn with_commit_callback(
        mut self,
        callback: &'a mut dyn FnMut(&crate::CommitInfo) -> Result<(), Error<'static>>,
    ) -> Self {
        self.commit_callback = Some(callback);
        self
    }
}

/// Options for move operations
pub struct MoveOptions<'a> {
    /// Whether to move sources as children of the destination.
    pub move_as_child: bool,
    /// Whether to create parent directories.
    pub make_parents: bool,
    /// Whether to allow mixed revisions.
    pub allow_mixed_revisions: bool,
    /// Whether to move only metadata (not file contents).
    pub metadata_only: bool,
    /// Revision properties for the commit.
    pub revprop_table: Option<std::collections::HashMap<String, Vec<u8>>>,
    /// Optional callback to invoke after commit.
    pub commit_callback:
        Option<&'a mut dyn FnMut(&crate::CommitInfo) -> Result<(), Error<'static>>>,
}

impl<'a> Default for MoveOptions<'a> {
    fn default() -> Self {
        Self {
            move_as_child: false,
            make_parents: false,
            allow_mixed_revisions: true,
            metadata_only: false,
            revprop_table: None,
            commit_callback: None,
        }
    }
}

impl<'a> MoveOptions<'a> {
    /// Creates a new MoveOptions with default values.
    pub fn new() -> Self {
        Self::default()
    }

    /// Sets whether to move as child.
    pub fn with_move_as_child(mut self, move_as_child: bool) -> Self {
        self.move_as_child = move_as_child;
        self
    }

    /// Sets whether to create parent directories.
    pub fn with_make_parents(mut self, make_parents: bool) -> Self {
        self.make_parents = make_parents;
        self
    }

    /// Sets whether to allow mixed revisions.
    pub fn with_allow_mixed_revisions(mut self, allow_mixed_revisions: bool) -> Self {
        self.allow_mixed_revisions = allow_mixed_revisions;
        self
    }

    /// Sets whether to move only metadata.
    pub fn with_metadata_only(mut self, metadata_only: bool) -> Self {
        self.metadata_only = metadata_only;
        self
    }

    /// Sets the revision properties.
    pub fn with_revprop_table(
        mut self,
        revprops: std::collections::HashMap<String, Vec<u8>>,
    ) -> Self {
        self.revprop_table = Some(revprops);
        self
    }

    /// Sets the commit callback.
    pub fn with_commit_callback(
        mut self,
        callback: &'a mut dyn FnMut(&crate::CommitInfo) -> Result<(), Error<'static>>,
    ) -> Self {
        self.commit_callback = Some(callback);
        self
    }
}

/// Options for merge_sources operations
#[derive(Debug, Clone)]
pub struct MergeSourcesOptions {
    /// Whether to ignore mergeinfo.
    pub ignore_mergeinfo: bool,
    /// Whether to ignore ancestry when comparing trees.
    pub diff_ignore_ancestry: bool,
    /// Whether to force deletion of modified files.
    pub force_delete: bool,
    /// Whether to record merge info but not apply changes.
    pub record_only: bool,
    /// Whether to perform a dry run.
    pub dry_run: bool,
    /// Whether to allow mixed revisions.
    pub allow_mixed_rev: bool,
    /// Array of merge options (like --ignore-eol-style).
    pub merge_options: Option<Vec<String>>,
}

impl Default for MergeSourcesOptions {
    fn default() -> Self {
        Self {
            ignore_mergeinfo: false,
            diff_ignore_ancestry: false,
            force_delete: false,
            record_only: false,
            dry_run: false,
            allow_mixed_rev: true,
            merge_options: None,
        }
    }
}

impl MergeSourcesOptions {
    /// Creates a new MergeSourcesOptions with default values.
    pub fn new() -> Self {
        Self::default()
    }

    /// Sets whether to ignore mergeinfo.
    pub fn with_ignore_mergeinfo(mut self, ignore: bool) -> Self {
        self.ignore_mergeinfo = ignore;
        self
    }

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

    /// Sets whether to force delete modified files.
    pub fn with_force_delete(mut self, force: bool) -> Self {
        self.force_delete = force;
        self
    }

    /// Sets whether to record merge info only.
    pub fn with_record_only(mut self, record_only: bool) -> Self {
        self.record_only = record_only;
        self
    }

    /// Sets whether to perform a dry run.
    pub fn with_dry_run(mut self, dry_run: bool) -> Self {
        self.dry_run = dry_run;
        self
    }

    /// Sets whether to allow mixed revisions.
    pub fn with_allow_mixed_rev(mut self, allow: bool) -> Self {
        self.allow_mixed_rev = allow;
        self
    }

    /// Sets the merge options (like "--ignore-eol-style").
    pub fn with_merge_options(mut self, options: Vec<String>) -> Self {
        self.merge_options = Some(options);
        self
    }
}

/// Options for patch operations
pub struct PatchOptions<'a> {
    /// Whether to perform a dry run without modifying files.
    pub dry_run: bool,
    /// Number of leading path components to strip from paths in the patch.
    pub strip_count: i32,
    /// Whether to apply the patch in reverse.
    pub reverse: bool,
    /// Whether to ignore whitespace differences.
    pub ignore_whitespace: bool,
    /// Whether to remove temporary files after patching.
    pub remove_tempfiles: bool,
    /// Optional callback to filter patch targets.
    pub patch_func: Option<
        &'a mut dyn FnMut(
            &mut bool,
            &str,
            &std::path::Path,
            &std::path::Path,
        ) -> Result<(), Error<'static>>,
    >,
}

impl<'a> Default for PatchOptions<'a> {
    fn default() -> Self {
        Self {
            dry_run: false,
            strip_count: 0,
            reverse: false,
            ignore_whitespace: false,
            remove_tempfiles: true,
            patch_func: None,
        }
    }
}

impl<'a> PatchOptions<'a> {
    /// Creates a new PatchOptions with default values.
    pub fn new() -> Self {
        Self::default()
    }

    /// Sets whether to perform a dry run.
    pub fn with_dry_run(mut self, dry_run: bool) -> Self {
        self.dry_run = dry_run;
        self
    }

    /// Sets the number of leading path components to strip.
    pub fn with_strip_count(mut self, count: i32) -> Self {
        self.strip_count = count;
        self
    }

    /// Sets whether to apply the patch in reverse.
    pub fn with_reverse(mut self, reverse: bool) -> Self {
        self.reverse = reverse;
        self
    }

    /// Sets whether to ignore whitespace.
    pub fn with_ignore_whitespace(mut self, ignore: bool) -> Self {
        self.ignore_whitespace = ignore;
        self
    }

    /// Sets whether to remove temporary files.
    pub fn with_remove_tempfiles(mut self, remove: bool) -> Self {
        self.remove_tempfiles = remove;
        self
    }

    /// Sets the patch filter callback.
    pub fn with_patch_func(
        mut self,
        func: &'a mut dyn FnMut(
            &mut bool,
            &str,
            &std::path::Path,
            &std::path::Path,
        ) -> Result<(), Error<'static>>,
    ) -> Self {
        self.patch_func = Some(func);
        self
    }
}

/// Options for mkdir operations
#[derive(Default)]
pub struct MkdirOptions<'a> {
    /// Whether to create parent directories.
    pub make_parents: bool,
    /// Revision properties for the commit.
    pub revprop_table: Option<std::collections::HashMap<String, Vec<u8>>>,
    /// Optional callback to invoke after commit.
    pub commit_callback:
        Option<&'a mut dyn FnMut(&crate::CommitInfo) -> Result<(), Error<'static>>>,
}

impl<'a> MkdirOptions<'a> {
    /// Creates a new MkdirOptions with default values.
    pub fn new() -> Self {
        Self::default()
    }

    /// Sets whether to create parent directories.
    pub fn with_make_parents(mut self, make_parents: bool) -> Self {
        self.make_parents = make_parents;
        self
    }

    /// Sets the revision properties.
    pub fn with_revprop_table(
        mut self,
        revprops: std::collections::HashMap<String, Vec<u8>>,
    ) -> Self {
        self.revprop_table = Some(revprops);
        self
    }

    /// Sets the commit callback.
    pub fn with_commit_callback(
        mut self,
        callback: &'a mut dyn FnMut(&crate::CommitInfo) -> Result<(), Error<'static>>,
    ) -> Self {
        self.commit_callback = Some(callback);
        self
    }
}

/// Options for import operations
pub struct ImportOptions<'a> {
    /// Recursion depth.
    pub depth: Depth,
    /// If true, don't use default ignores.
    pub no_ignore: bool,
    /// If true, don't use auto-props.
    pub no_autoprops: bool,
    /// If true, ignore unknown node types instead of erroring.
    pub ignore_unknown_node_types: bool,
    /// Revision properties for the commit.
    pub revprop_table: Option<std::collections::HashMap<String, String>>,
    /// Optional filter callback to control which files are imported.
    pub filter_callback: Option<
        &'a mut dyn FnMut(&mut bool, &std::path::Path, &Dirent) -> Result<(), Error<'static>>,
    >,
    /// Optional callback to invoke after commit.
    pub commit_callback:
        Option<&'a mut dyn FnMut(&crate::CommitInfo) -> Result<(), Error<'static>>>,
}

impl<'a> Default for ImportOptions<'a> {
    fn default() -> Self {
        Self {
            depth: Depth::Infinity,
            no_ignore: false,
            no_autoprops: false,
            ignore_unknown_node_types: false,
            revprop_table: None,
            filter_callback: None,
            commit_callback: None,
        }
    }
}

impl<'a> ImportOptions<'a> {
    /// Creates a new ImportOptions with default values.
    pub fn new() -> Self {
        Self::default()
    }

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

    /// Sets whether to ignore default ignores.
    pub fn with_no_ignore(mut self, no_ignore: bool) -> Self {
        self.no_ignore = no_ignore;
        self
    }

    /// Sets whether to disable auto-props.
    pub fn with_no_autoprops(mut self, no_autoprops: bool) -> Self {
        self.no_autoprops = no_autoprops;
        self
    }

    /// Sets whether to ignore unknown node types.
    pub fn with_ignore_unknown_node_types(mut self, ignore: bool) -> Self {
        self.ignore_unknown_node_types = ignore;
        self
    }

    /// Sets the revision properties.
    pub fn with_revprop_table(
        mut self,
        revprops: std::collections::HashMap<String, String>,
    ) -> Self {
        self.revprop_table = Some(revprops);
        self
    }

    /// Sets the filter callback.
    pub fn with_filter_callback(
        mut self,
        callback: &'a mut dyn FnMut(
            &mut bool,
            &std::path::Path,
            &Dirent,
        ) -> Result<(), Error<'static>>,
    ) -> Self {
        self.filter_callback = Some(callback);
        self
    }

    /// Sets the commit callback.
    pub fn with_commit_callback(
        mut self,
        callback: &'a mut dyn FnMut(&crate::CommitInfo) -> Result<(), Error<'static>>,
    ) -> Self {
        self.commit_callback = Some(callback);
        self
    }
}

/// Options for revert operations
#[derive(Debug, Clone)]
pub struct RevertOptions {
    /// Recursion depth.
    pub depth: Depth,
    /// Changelists to revert (None means all changelists).
    pub changelists: Option<Vec<String>>,
    /// Whether to clear changelists after reverting.
    pub clear_changelists: bool,
    /// If true, only revert metadata (properties), not file contents.
    pub metadata_only: bool,
    /// If true, keep local copies of added files when reverting.
    pub added_keep_local: bool,
}

impl Default for RevertOptions {
    fn default() -> Self {
        Self {
            depth: Depth::Infinity,
            changelists: None,
            clear_changelists: false,
            metadata_only: false,
            added_keep_local: false,
        }
    }
}

impl RevertOptions {
    /// Creates a new RevertOptions with default values.
    pub fn new() -> Self {
        Self::default()
    }

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

    /// Sets the changelists to revert.
    pub fn with_changelists(mut self, changelists: Vec<String>) -> Self {
        self.changelists = Some(changelists);
        self
    }

    /// Sets whether to clear changelists.
    pub fn with_clear_changelists(mut self, clear: bool) -> Self {
        self.clear_changelists = clear;
        self
    }

    /// Sets whether to only revert metadata.
    pub fn with_metadata_only(mut self, metadata_only: bool) -> Self {
        self.metadata_only = metadata_only;
        self
    }

    /// Sets whether to keep local copies of added files.
    pub fn with_added_keep_local(mut self, keep_local: bool) -> Self {
        self.added_keep_local = keep_local;
        self
    }
}

/// Represents inherited properties from a parent path.
///
/// Contains a path and the properties inherited from that path.
#[derive(Debug, Clone)]
pub struct InheritedPropItem {
    /// The absolute working copy path, relative filesystem path, or URL
    /// from which the properties are inherited.
    pub path_or_url: String,
    /// Hash map of inherited property names to values.
    pub properties: std::collections::HashMap<String, Vec<u8>>,
}

/// Options for property get operations
#[derive(Debug, Clone)]
pub struct PropGetOptions {
    /// Peg revision.
    pub peg_revision: Revision,
    /// Operative revision.
    pub revision: Revision,
    /// Recursion depth.
    pub depth: Depth,
    /// Changelists to limit operation to (None means all).
    pub changelists: Option<Vec<String>>,
}

impl Default for PropGetOptions {
    fn default() -> Self {
        Self {
            peg_revision: Revision::Unspecified,
            revision: Revision::Working,
            depth: Depth::Empty,
            changelists: None,
        }
    }
}

impl PropGetOptions {
    /// Creates a new PropGetOptions with default values.
    pub fn new() -> Self {
        Self::default()
    }

    /// Sets the peg revision.
    pub fn with_peg_revision(mut self, peg_revision: Revision) -> Self {
        self.peg_revision = peg_revision;
        self
    }

    /// Sets the operative revision.
    pub fn with_revision(mut self, revision: Revision) -> Self {
        self.revision = revision;
        self
    }

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

    /// Sets the changelists.
    pub fn with_changelists(mut self, changelists: Vec<String>) -> Self {
        self.changelists = Some(changelists);
        self
    }
}

/// Options for property set operations
#[derive(Debug, Clone)]
pub struct PropSetOptions {
    /// Recursion depth.
    pub depth: Depth,
    /// Whether to skip checks.
    pub skip_checks: bool,
    /// Base revision for URL (used for atomic revprop changes).
    pub base_revision_for_url: Revnum,
    /// Changelists to limit operation to (None means all).
    pub changelists: Option<Vec<String>>,
}

impl Default for PropSetOptions {
    fn default() -> Self {
        Self {
            depth: Depth::Empty,
            skip_checks: false,
            base_revision_for_url: Revnum(-1),
            changelists: None,
        }
    }
}

/// Options for remote property set operations
pub struct PropSetRemoteOptions<'a> {
    /// Whether to skip checks.
    pub skip_checks: bool,
    /// Base revision for URL (used for atomic property changes).
    pub base_revision_for_url: Revnum,
    /// Revision properties for the commit.
    pub revprop_table: Option<std::collections::HashMap<String, String>>,
    /// Callback for commit completion.
    pub commit_callback:
        Option<&'a mut dyn FnMut(&crate::CommitInfo) -> Result<(), Error<'static>>>,
}

impl<'a> Default for PropSetRemoteOptions<'a> {
    fn default() -> Self {
        Self {
            skip_checks: false,
            base_revision_for_url: Revnum(-1),
            revprop_table: None,
            commit_callback: None,
        }
    }
}

impl<'a> PropSetRemoteOptions<'a> {
    /// Creates new PropSetRemoteOptions with default values.
    pub fn new() -> Self {
        Self::default()
    }

    /// Sets whether to skip checks.
    pub fn with_skip_checks(mut self, skip_checks: bool) -> Self {
        self.skip_checks = skip_checks;
        self
    }

    /// Sets the base revision for URL.
    pub fn with_base_revision_for_url(mut self, base_revision_for_url: Revnum) -> Self {
        self.base_revision_for_url = base_revision_for_url;
        self
    }

    /// Sets the revision properties for the commit.
    pub fn with_revprop_table(
        mut self,
        revprop_table: std::collections::HashMap<String, String>,
    ) -> Self {
        self.revprop_table = Some(revprop_table);
        self
    }

    /// Sets the commit callback.
    pub fn with_commit_callback(
        mut self,
        callback: &'a mut dyn FnMut(&crate::CommitInfo) -> Result<(), Error<'static>>,
    ) -> Self {
        self.commit_callback = Some(callback);
        self
    }
}

impl PropSetOptions {
    /// Creates a new PropSetOptions with default values.
    pub fn new() -> Self {
        Self::default()
    }

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

    /// Sets whether to skip checks.
    pub fn with_skip_checks(mut self, skip_checks: bool) -> Self {
        self.skip_checks = skip_checks;
        self
    }

    /// Sets the base revision for URL.
    pub fn with_base_revision_for_url(mut self, base_revision: Revnum) -> Self {
        self.base_revision_for_url = base_revision;
        self
    }

    /// Sets the changelists.
    pub fn with_changelists(mut self, changelists: Vec<String>) -> Self {
        self.changelists = Some(changelists);
        self
    }
}

/// Options for diff operations
#[derive(Debug, Clone)]
pub struct DiffOptions {
    /// Diff-specific options to pass to diff engine.
    pub diff_options: Vec<String>,
    /// Recursion depth.
    pub depth: Depth,
    /// Whether to ignore ancestry when calculating diffs.
    pub ignore_ancestry: bool,
    /// If true, don't show diffs for added files.
    pub no_diff_added: bool,
    /// If true, don't show diffs for deleted files.
    pub no_diff_deleted: bool,
    /// If true, show copies as additions.
    pub show_copies_as_adds: bool,
    /// If true, ignore content type.
    pub ignore_content_type: bool,
    /// If true, ignore properties.
    pub ignore_properties: bool,
    /// If true, show only properties.
    pub properties_only: bool,
    /// If true, use Git diff format.
    pub use_git_diff_format: bool,
    /// If true, pretty-print svn:mergeinfo properties in the diff output.
    pub pretty_print_mergeinfo: bool,
    /// Encoding for headers.
    pub header_encoding: String,
    /// Changelists to limit operation to (None means all).
    pub changelists: Option<Vec<String>>,
}

impl Default for DiffOptions {
    fn default() -> Self {
        Self {
            diff_options: Vec::new(),
            depth: Depth::Infinity,
            ignore_ancestry: false,
            no_diff_added: false,
            no_diff_deleted: false,
            show_copies_as_adds: false,
            ignore_content_type: false,
            ignore_properties: false,
            properties_only: false,
            use_git_diff_format: false,
            pretty_print_mergeinfo: false,
            header_encoding: String::from("UTF-8"),
            changelists: None,
        }
    }
}

impl DiffOptions {
    /// Creates a new DiffOptions with default values.
    pub fn new() -> Self {
        Self::default()
    }

    /// Sets the diff options.
    pub fn with_diff_options(mut self, diff_options: Vec<String>) -> Self {
        self.diff_options = diff_options;
        self
    }

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

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

    /// Sets whether to skip diffs for added files.
    pub fn with_no_diff_added(mut self, no_diff_added: bool) -> Self {
        self.no_diff_added = no_diff_added;
        self
    }

    /// Sets whether to skip diffs for deleted files.
    pub fn with_no_diff_deleted(mut self, no_diff_deleted: bool) -> Self {
        self.no_diff_deleted = no_diff_deleted;
        self
    }

    /// Sets whether to show copies as additions.
    pub fn with_show_copies_as_adds(mut self, show_copies_as_adds: bool) -> Self {
        self.show_copies_as_adds = show_copies_as_adds;
        self
    }

    /// Sets whether to ignore content type.
    pub fn with_ignore_content_type(mut self, ignore_content_type: bool) -> Self {
        self.ignore_content_type = ignore_content_type;
        self
    }

    /// Sets whether to ignore properties.
    pub fn with_ignore_properties(mut self, ignore_properties: bool) -> Self {
        self.ignore_properties = ignore_properties;
        self
    }

    /// Sets whether to show only properties.
    pub fn with_properties_only(mut self, properties_only: bool) -> Self {
        self.properties_only = properties_only;
        self
    }

    /// Sets whether to use Git diff format.
    pub fn with_use_git_diff_format(mut self, use_git_diff_format: bool) -> Self {
        self.use_git_diff_format = use_git_diff_format;
        self
    }

    /// Sets the header encoding.
    pub fn with_header_encoding(mut self, header_encoding: String) -> Self {
        self.header_encoding = header_encoding;
        self
    }

    /// Sets the changelists.
    pub fn with_changelists(mut self, changelists: Vec<String>) -> Self {
        self.changelists = Some(changelists);
        self
    }
}

/// The kind of change in a diff summary
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DiffSummarizeKind {
    /// Normal diff
    Normal,
    /// Item was added
    Added,
    /// Item was modified
    Modified,
    /// Item was deleted
    Deleted,
}

impl From<subversion_sys::svn_client_diff_summarize_kind_t> for DiffSummarizeKind {
    fn from(kind: subversion_sys::svn_client_diff_summarize_kind_t) -> Self {
        match kind {
            subversion_sys::svn_client_diff_summarize_kind_t_svn_client_diff_summarize_kind_normal => {
                DiffSummarizeKind::Normal
            }
            subversion_sys::svn_client_diff_summarize_kind_t_svn_client_diff_summarize_kind_added => {
                DiffSummarizeKind::Added
            }
            subversion_sys::svn_client_diff_summarize_kind_t_svn_client_diff_summarize_kind_modified => {
                DiffSummarizeKind::Modified
            }
            subversion_sys::svn_client_diff_summarize_kind_t_svn_client_diff_summarize_kind_deleted => {
                DiffSummarizeKind::Deleted
            }
            _ => unreachable!("unknown svn_client_diff_summarize_kind_t value: {}", kind),
        }
    }
}

/// Summary of a diff between two paths
#[derive(Debug, Clone)]
pub struct DiffSummary {
    /// Path relative to the target
    pub path: String,
    /// The kind of change
    pub kind: DiffSummarizeKind,
    /// Whether properties changed
    pub prop_changed: bool,
    /// Node kind (file or directory)
    pub node_kind: subversion_sys::svn_node_kind_t,
}

impl DiffSummary {
    /// Create from raw C structure
    pub(crate) unsafe fn from_raw(raw: *const subversion_sys::svn_client_diff_summarize_t) -> Self {
        let path = std::ffi::CStr::from_ptr((*raw).path)
            .to_string_lossy()
            .into_owned();
        Self {
            path,
            kind: (*raw).summarize_kind.into(),
            prop_changed: (*raw).prop_changed != 0,
            node_kind: (*raw).node_kind,
        }
    }
}

/// Options for diff summarize operations
#[derive(Debug, Clone)]
pub struct DiffSummarizeOptions {
    /// Recursion depth (or use recurse boolean for older API)
    pub depth: Depth,
    /// Whether to ignore ancestry
    pub ignore_ancestry: bool,
    /// Changelists to limit operation to (None means all).
    pub changelists: Option<Vec<String>>,
}

impl Default for DiffSummarizeOptions {
    fn default() -> Self {
        Self {
            depth: Depth::Infinity,
            ignore_ancestry: false,
            changelists: None,
        }
    }
}

impl DiffSummarizeOptions {
    /// Creates new DiffSummarizeOptions with default values.
    pub fn new() -> Self {
        Self::default()
    }

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

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

    /// Sets the changelists.
    pub fn with_changelists(mut self, changelists: Vec<String>) -> Self {
        self.changelists = Some(changelists);
        self
    }
}

/// Options for log operations
#[derive(Debug, Clone)]
pub struct LogOptions {
    /// Peg revision for the target.
    pub peg_revision: Revision,
    /// Maximum number of log entries to retrieve (None = unlimited).
    pub limit: Option<i32>,
    /// Whether to discover changed paths.
    pub discover_changed_paths: bool,
    /// Whether to follow strict node history.
    pub strict_node_history: bool,
    /// Whether to include merged revisions.
    pub include_merged_revisions: bool,
    /// Revision properties to retrieve (None = all, Some(vec) = only specified).
    pub revprops: Option<Vec<String>>,
}

impl Default for LogOptions {
    fn default() -> Self {
        Self {
            peg_revision: Revision::Unspecified,
            limit: None,
            discover_changed_paths: false,
            strict_node_history: false,
            include_merged_revisions: false,
            revprops: None,
        }
    }
}

impl LogOptions {
    /// Creates a new LogOptions with default values.
    pub fn new() -> Self {
        Self::default()
    }

    /// Sets the peg revision.
    pub fn with_peg_revision(mut self, peg_revision: Revision) -> Self {
        self.peg_revision = peg_revision;
        self
    }

    /// Sets the limit for number of log entries.
    pub fn with_limit(mut self, limit: i32) -> Self {
        self.limit = Some(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 follow 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 = all, Some(vec) = only specified).
    pub fn with_revprops(mut self, revprops: Option<Vec<String>>) -> Self {
        self.revprops = revprops;
        self
    }
}

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

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

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

impl Drop for LogIterator<'_> {
    fn drop(&mut self) {
        // Drop the receiver to signal cancellation to the worker thread
        // (its next send will fail, causing it to return SVN_ERR_CANCELLED)
        drop(std::mem::replace(
            &mut self.rx,
            std::sync::mpsc::sync_channel(0).1,
        ));
        // Wait for the worker thread to finish, ensuring the &mut Context
        // borrow is released before this LogIterator's lifetime ends
        if let Some(handle) = self.handle.take() {
            let _ = handle.join();
        }
    }
}

/// Options for mergeinfo log operations
#[derive(Debug, Clone)]
pub struct MergeinfoLogOptions {
    /// Whether to find merged revisions (true) or eligible revisions (false)
    pub finding_merged: bool,
    /// Target peg revision
    pub target_peg_revision: Revision,
    /// Source peg revision
    pub source_peg_revision: Revision,
    /// Source start revision
    pub source_start_revision: Revision,
    /// Source end revision
    pub source_end_revision: Revision,
    /// Whether to discover changed paths
    pub discover_changed_paths: bool,
    /// Recursion depth
    pub depth: Depth,
    /// Revision properties to retrieve
    pub revprops: Vec<String>,
}

impl Default for MergeinfoLogOptions {
    fn default() -> Self {
        Self {
            finding_merged: true,
            target_peg_revision: Revision::Head,
            source_peg_revision: Revision::Head,
            source_start_revision: Revision::Head,
            source_end_revision: Revision::Number(Revnum(1)),
            discover_changed_paths: false,
            depth: Depth::Empty,
            revprops: vec![],
        }
    }
}

impl MergeinfoLogOptions {
    /// Creates new MergeinfoLogOptions with default values.
    pub fn new() -> Self {
        Self::default()
    }

    /// Sets whether to find merged (true) or eligible (false) revisions.
    pub fn with_finding_merged(mut self, finding_merged: bool) -> Self {
        self.finding_merged = finding_merged;
        self
    }

    /// Sets the target peg revision.
    pub fn with_target_peg_revision(mut self, revision: Revision) -> Self {
        self.target_peg_revision = revision;
        self
    }

    /// Sets the source peg revision.
    pub fn with_source_peg_revision(mut self, revision: Revision) -> Self {
        self.source_peg_revision = revision;
        self
    }

    /// Sets the source start revision.
    pub fn with_source_start_revision(mut self, revision: Revision) -> Self {
        self.source_start_revision = revision;
        self
    }

    /// Sets the source end revision.
    pub fn with_source_end_revision(mut self, revision: Revision) -> Self {
        self.source_end_revision = revision;
        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 the recursion depth.
    pub fn with_depth(mut self, depth: Depth) -> Self {
        self.depth = depth;
        self
    }

    /// Sets the revision properties to retrieve.
    pub fn with_revprops(mut self, revprops: Vec<String>) -> Self {
        self.revprops = revprops;
        self
    }
}

/// Options for blame operations
#[derive(Debug, Clone)]
pub struct BlameOptions {
    /// Peg revision for the target.
    pub peg_revision: Revision,
    /// Start revision for blame range.
    pub start_revision: Revision,
    /// End revision for blame range.
    pub end_revision: Revision,
    /// Diff options to pass to the diff engine.
    pub diff_options: Vec<String>,
    /// Whether to ignore MIME type.
    pub ignore_mime_type: bool,
    /// Whether to include merged revisions.
    pub include_merged_revisions: bool,
}

impl Default for BlameOptions {
    fn default() -> Self {
        Self {
            peg_revision: Revision::Unspecified,
            start_revision: Revision::Number(Revnum(1)),
            end_revision: Revision::Head,
            diff_options: Vec::new(),
            ignore_mime_type: false,
            include_merged_revisions: false,
        }
    }
}

impl BlameOptions {
    /// Creates a new BlameOptions with default values.
    pub fn new() -> Self {
        Self::default()
    }

    /// Sets the peg revision.
    pub fn with_peg_revision(mut self, peg_revision: Revision) -> Self {
        self.peg_revision = peg_revision;
        self
    }

    /// Sets the start revision.
    pub fn with_start_revision(mut self, start_revision: Revision) -> Self {
        self.start_revision = start_revision;
        self
    }

    /// Sets the end revision.
    pub fn with_end_revision(mut self, end_revision: Revision) -> Self {
        self.end_revision = end_revision;
        self
    }

    /// Sets the diff options.
    pub fn with_diff_options(mut self, diff_options: Vec<String>) -> Self {
        self.diff_options = diff_options;
        self
    }

    /// Sets whether to ignore MIME type.
    pub fn with_ignore_mime_type(mut self, ignore: bool) -> Self {
        self.ignore_mime_type = ignore;
        self
    }

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

/// Options for setting revision properties
#[derive(Debug, Clone)]
pub struct RevpropSetOptions {
    /// The revision to set the property on.
    pub revision: Revision,
    /// Original property value for atomic updates (None = don't check).
    pub original_propval: Option<Vec<u8>>,
    /// Whether to force the operation.
    pub force: bool,
}

impl Default for RevpropSetOptions {
    fn default() -> Self {
        Self {
            revision: Revision::Head,
            original_propval: None,
            force: false,
        }
    }
}

impl RevpropSetOptions {
    /// Creates a new RevpropSetOptions with default values.
    pub fn new() -> Self {
        Self::default()
    }

    /// Sets the revision.
    pub fn with_revision(mut self, revision: Revision) -> Self {
        self.revision = revision;
        self
    }

    /// Sets the original property value for atomic updates.
    pub fn with_original_propval(mut self, original_propval: Vec<u8>) -> Self {
        self.original_propval = Some(original_propval);
        self
    }

    /// Sets whether to force the operation.
    pub fn with_force(mut self, force: bool) -> Self {
        self.force = force;
        self
    }
}

/// Options for list operations
#[derive(Debug, Clone)]
pub struct ListOptions {
    /// Peg revision for the target.
    pub peg_revision: Revision,
    /// Operative revision.
    pub revision: Revision,
    /// Patterns to filter the listing (None = no filter).
    pub patterns: Option<Vec<String>>,
    /// Recursion depth.
    pub depth: Depth,
    /// Dirent fields to retrieve (bitfield).
    pub dirent_fields: u32,
    /// Whether to fetch locks.
    pub fetch_locks: bool,
    /// Whether to include externals.
    pub include_externals: bool,
}

impl Default for ListOptions {
    fn default() -> Self {
        Self {
            peg_revision: Revision::Unspecified,
            revision: Revision::Head,
            patterns: None,
            depth: Depth::Infinity,
            dirent_fields: 0xFFFFFFFF, // All fields
            fetch_locks: false,
            include_externals: false,
        }
    }
}

impl ListOptions {
    /// Creates a new ListOptions with default values.
    pub fn new() -> Self {
        Self::default()
    }

    /// Sets the peg revision.
    pub fn with_peg_revision(mut self, peg_revision: Revision) -> Self {
        self.peg_revision = peg_revision;
        self
    }

    /// Sets the operative revision.
    pub fn with_revision(mut self, revision: Revision) -> Self {
        self.revision = revision;
        self
    }

    /// Sets the patterns to filter the listing.
    pub fn with_patterns(mut self, patterns: Vec<String>) -> Self {
        self.patterns = Some(patterns);
        self
    }

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

    /// Sets the dirent fields to retrieve.
    pub fn with_dirent_fields(mut self, dirent_fields: u32) -> Self {
        self.dirent_fields = dirent_fields;
        self
    }

    /// Sets whether to fetch locks.
    pub fn with_fetch_locks(mut self, fetch_locks: bool) -> Self {
        self.fetch_locks = fetch_locks;
        self
    }

    /// Sets whether to include externals.
    pub fn with_include_externals(mut self, include_externals: bool) -> Self {
        self.include_externals = include_externals;
        self
    }
}

/// Options for info operations
#[derive(Debug, Clone)]
pub struct InfoOptions {
    /// Peg revision for the target.
    pub peg_revision: Revision,
    /// Operative revision.
    pub revision: Revision,
    /// Recursion depth.
    pub depth: Depth,
    /// Whether to fetch excluded items.
    pub fetch_excluded: bool,
    /// Whether to fetch actual-only items.
    pub fetch_actual_only: bool,
    /// Whether to include externals.
    pub include_externals: bool,
    /// Changelists to limit operation to (None = all).
    pub changelists: Option<Vec<String>>,
}

impl Default for InfoOptions {
    fn default() -> Self {
        Self {
            peg_revision: Revision::Unspecified,
            revision: Revision::Working,
            depth: Depth::Empty,
            fetch_excluded: false,
            fetch_actual_only: false,
            include_externals: false,
            changelists: None,
        }
    }
}

impl InfoOptions {
    /// Creates a new InfoOptions with default values.
    pub fn new() -> Self {
        Self::default()
    }

    /// Sets the peg revision.
    pub fn with_peg_revision(mut self, peg_revision: Revision) -> Self {
        self.peg_revision = peg_revision;
        self
    }

    /// Sets the operative revision.
    pub fn with_revision(mut self, revision: Revision) -> Self {
        self.revision = revision;
        self
    }

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

    /// Sets whether to fetch excluded items.
    pub fn with_fetch_excluded(mut self, fetch_excluded: bool) -> Self {
        self.fetch_excluded = fetch_excluded;
        self
    }

    /// Sets whether to fetch actual-only items.
    pub fn with_fetch_actual_only(mut self, fetch_actual_only: bool) -> Self {
        self.fetch_actual_only = fetch_actual_only;
        self
    }

    /// Sets whether to include externals.
    pub fn with_include_externals(mut self, include_externals: bool) -> Self {
        self.include_externals = include_externals;
        self
    }

    /// Sets the changelists.
    pub fn with_changelists(mut self, changelists: Vec<String>) -> Self {
        self.changelists = Some(changelists);
        self
    }
}

/// Represents an item being committed.
#[derive(Debug)]
pub struct CommitItem<'a> {
    ptr: *const subversion_sys::svn_client_commit_item3_t,
    _marker: std::marker::PhantomData<&'a ()>,
}

impl<'a> CommitItem<'a> {
    /// Creates a CommitItem from a raw pointer.
    fn from_raw(ptr: *const subversion_sys::svn_client_commit_item3_t) -> Self {
        Self {
            ptr,
            _marker: std::marker::PhantomData,
        }
    }

    /// Returns the working copy path of the item.
    pub fn path(&self) -> Option<&std::path::Path> {
        unsafe {
            if (*self.ptr).path.is_null() {
                None
            } else {
                Some(
                    std::ffi::CStr::from_ptr((*self.ptr).path)
                        .to_str()
                        .unwrap()
                        .as_ref(),
                )
            }
        }
    }

    /// Returns the URL of the item in the repository.
    pub fn url(&self) -> Option<&str> {
        unsafe {
            if (*self.ptr).url.is_null() {
                None
            } else {
                Some(std::ffi::CStr::from_ptr((*self.ptr).url).to_str().unwrap())
            }
        }
    }

    /// Returns the revision of the item's textbase.
    pub fn revision(&self) -> Revnum {
        unsafe { Revnum((*self.ptr).revision) }
    }

    /// Returns the copyfrom URL if this is a copied item.
    pub fn copyfrom_url(&self) -> Option<&str> {
        unsafe {
            if (*self.ptr).copyfrom_url.is_null() {
                None
            } else {
                Some(
                    std::ffi::CStr::from_ptr((*self.ptr).copyfrom_url)
                        .to_str()
                        .unwrap(),
                )
            }
        }
    }

    /// Returns the copyfrom revision if this is a copied item.
    pub fn copyfrom_rev(&self) -> Option<Revnum> {
        unsafe {
            if (*self.ptr).copyfrom_url.is_null() {
                None
            } else {
                Some(Revnum((*self.ptr).copyfrom_rev))
            }
        }
    }
}

/// Options for committing changes to the repository.
#[derive(Debug, Clone)]
pub struct CommitOptions {
    /// Depth of the commit.
    pub depth: Depth,
    /// Whether to keep locks after commit.
    pub keep_locks: bool,
    /// Whether to keep changelists after commit.
    pub keep_changelists: bool,
    /// Whether to commit as operations.
    pub commit_as_operations: bool,
    /// Whether to include file externals.
    pub include_file_externals: bool,
    /// Whether to include directory externals.
    pub include_dir_externals: bool,
    /// Changelists to commit.
    pub changelists: Option<Vec<String>>,
}

impl Default for CommitOptions {
    fn default() -> Self {
        Self {
            depth: Depth::Infinity,
            keep_locks: false,
            keep_changelists: false,
            commit_as_operations: true,
            include_file_externals: false,
            include_dir_externals: false,
            changelists: None,
        }
    }
}

impl CommitOptions {
    /// Creates a new CommitOptions with default values.
    pub fn new() -> Self {
        Self::default()
    }

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

    /// Sets whether to keep locks after commit.
    pub fn with_keep_locks(mut self, keep_locks: bool) -> Self {
        self.keep_locks = keep_locks;
        self
    }

    /// Sets whether to keep changelists after commit.
    pub fn with_keep_changelists(mut self, keep_changelists: bool) -> Self {
        self.keep_changelists = keep_changelists;
        self
    }

    /// Sets whether to commit as operations.
    pub fn with_commit_as_operations(mut self, commit_as_operations: bool) -> Self {
        self.commit_as_operations = commit_as_operations;
        self
    }

    /// Sets whether to include file externals.
    pub fn with_include_file_externals(mut self, include_file_externals: bool) -> Self {
        self.include_file_externals = include_file_externals;
        self
    }

    /// Sets whether to include directory externals.
    pub fn with_include_dir_externals(mut self, include_dir_externals: bool) -> Self {
        self.include_dir_externals = include_dir_externals;
        self
    }

    /// Sets the changelists to include in the commit.
    pub fn with_changelists(mut self, changelists: Vec<String>) -> Self {
        self.changelists = Some(changelists);
        self
    }
}

/// Options for status
#[derive(Debug, Clone)]
/// Options for retrieving status information about working copy items.
pub struct StatusOptions {
    /// Revision to check status against.
    pub revision: Revision,
    /// Depth of the status operation.
    pub depth: Depth,
    /// Whether to get all entries.
    pub get_all: bool,
    /// Whether to check out-of-date status.
    pub check_out_of_date: bool,
    /// Whether to check the working copy.
    pub check_working_copy: bool,
    /// Whether to include ignored files.
    pub no_ignore: bool,
    /// Whether to ignore externals.
    pub ignore_externals: bool,
    /// Whether to treat depth as sticky.
    pub depth_as_sticky: bool,
    /// Changelists to filter by.
    pub changelists: Option<Vec<String>>,
}

impl Default for StatusOptions {
    fn default() -> Self {
        Self {
            revision: Revision::Working,
            depth: Depth::Infinity,
            get_all: false,
            check_out_of_date: false,
            check_working_copy: true,
            no_ignore: false,
            ignore_externals: false,
            depth_as_sticky: false,
            changelists: None,
        }
    }
}

impl StatusOptions {
    /// Creates a new StatusOptions with default values.
    pub fn new() -> Self {
        Self::default()
    }

    /// Sets the revision to check status against.
    pub fn with_revision(mut self, revision: Revision) -> Self {
        self.revision = revision;
        self
    }

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

    /// Sets whether to get all entries.
    pub fn with_get_all(mut self, get_all: bool) -> Self {
        self.get_all = get_all;
        self
    }

    /// Sets whether to check out-of-date status.
    pub fn with_check_out_of_date(mut self, check_out_of_date: bool) -> Self {
        self.check_out_of_date = check_out_of_date;
        self
    }

    /// Sets whether to check the working copy.
    pub fn with_check_working_copy(mut self, check_working_copy: bool) -> Self {
        self.check_working_copy = check_working_copy;
        self
    }

    /// Sets whether to include ignored files.
    pub fn with_no_ignore(mut self, no_ignore: bool) -> Self {
        self.no_ignore = no_ignore;
        self
    }

    /// Sets whether to ignore externals.
    pub fn with_ignore_externals(mut self, ignore_externals: bool) -> Self {
        self.ignore_externals = ignore_externals;
        self
    }

    /// Sets whether to treat depth as sticky.
    pub fn with_depth_as_sticky(mut self, depth_as_sticky: bool) -> Self {
        self.depth_as_sticky = depth_as_sticky;
        self
    }

    /// Sets the changelists to filter by.
    pub fn with_changelists(mut self, changelists: Vec<String>) -> Self {
        self.changelists = Some(changelists);
        self
    }
}

/// A client context.
///
/// This is the main entry point for the client library. It holds client specific configuration and
/// callbacks
/// Client context for performing Subversion operations.
pub struct Context {
    ptr: *mut svn_client_ctx_t,
    pool: apr::Pool<'static>,
    _phantom: std::marker::PhantomData<*mut ()>,
    conflict_resolver: Option<Box<crate::conflict::ConflictResolverBaton>>,
    cancel_handler: Option<Box<dyn FnMut() -> bool + Send>>,
    auth_baton: Option<crate::auth::AuthBaton>,
    config_hash: Option<crate::config::ConfigHash>,
}
unsafe impl Send for Context {}

impl Drop for Context {
    fn drop(&mut self) {
        // Clear the conflict resolver callback first
        if self.conflict_resolver.is_some() {
            unsafe {
                (*self.ptr).conflict_func2 = None;
                (*self.ptr).conflict_baton2 = std::ptr::null_mut();
            }
        }
        // Clear cancel handler
        if self.cancel_handler.is_some() {
            unsafe {
                (*self.ptr).cancel_func = None;
                (*self.ptr).cancel_baton = std::ptr::null_mut();
            }
        }
        // Pool drop will clean up context
    }
}

impl Context {
    /// Creates a new Subversion client context.
    pub fn new() -> Result<Self, Error<'static>> {
        Self::with_config_dir(None)
    }

    /// Creates a new Subversion client context, loading configuration from
    /// the specified directory (or the default location if `None`).
    pub fn with_config_dir(config_dir: Option<&std::path::Path>) -> Result<Self, Error<'static>> {
        crate::init::initialize()?;

        let mut config_hash = crate::config::get_config_hash(config_dir)?;

        let pool = apr::Pool::new();
        let mut ctx = std::ptr::null_mut();
        let ret = unsafe {
            svn_client_create_context2(&mut ctx, config_hash.as_mut_ptr(), pool.as_mut_ptr())
        };
        Error::from_raw(ret)?;
        Ok(Context {
            ptr: ctx,
            pool,
            _phantom: std::marker::PhantomData,
            conflict_resolver: None,
            cancel_handler: None,
            auth_baton: None,
            config_hash: Some(config_hash),
        })
    }

    pub(crate) unsafe fn as_mut_ptr(&mut self) -> *mut svn_client_ctx_t {
        self.ptr
    }

    /// Set the conflict resolver for this context
    ///
    /// The resolver will be called when conflicts are encountered during
    /// operations like merge, update, or switch.
    pub fn set_conflict_resolver(
        &mut self,
        resolver: impl crate::conflict::ConflictResolver + 'static,
    ) {
        // Clear any existing resolver
        unsafe {
            (*self.ptr).conflict_func2 = None;
            (*self.ptr).conflict_baton2 = std::ptr::null_mut();
        }

        // Create new resolver baton
        let baton = Box::new(crate::conflict::ConflictResolverBaton {
            resolver: Box::new(resolver),
        });

        // Store the baton and set up the callback
        unsafe {
            let baton_ptr = Box::into_raw(baton);
            self.conflict_resolver = Some(Box::from_raw(baton_ptr));

            (*self.ptr).conflict_func2 = Some(crate::conflict::conflict_resolver_callback);
            (*self.ptr).conflict_baton2 = baton_ptr as *mut std::ffi::c_void;
        }
    }

    /// Clear the conflict resolver
    pub fn clear_conflict_resolver(&mut self) {
        unsafe {
            (*self.ptr).conflict_func2 = None;
            (*self.ptr).conflict_baton2 = std::ptr::null_mut();
        }
        self.conflict_resolver = None;
    }

    /// Set a cancel handler that will be called periodically during long operations
    ///
    /// The handler should return `true` to cancel the operation, `false` to continue.
    /// Operations will fail with an SVN_ERR_CANCELLED error if cancelled.
    pub fn set_cancel_handler<F>(&mut self, handler: F)
    where
        F: FnMut() -> bool + Send + 'static,
    {
        // Clear any existing handler
        unsafe {
            (*self.ptr).cancel_func = None;
            (*self.ptr).cancel_baton = std::ptr::null_mut();
        }

        // Store the new handler
        self.cancel_handler = Some(Box::new(handler));

        // Set up the C callback
        unsafe {
            (*self.ptr).cancel_func = Some(cancel_trampoline);
            (*self.ptr).cancel_baton = self
                .cancel_handler
                .as_mut()
                .map(|h| h.as_mut() as *mut _ as *mut std::ffi::c_void)
                .unwrap_or(std::ptr::null_mut());
        }
    }

    /// Clear the cancel handler
    pub fn clear_cancel_handler(&mut self) {
        unsafe {
            (*self.ptr).cancel_func = None;
            (*self.ptr).cancel_baton = std::ptr::null_mut();
        }
        self.cancel_handler = None;
    }

    /// Sets the authentication baton for this context (borrowed).
    pub fn set_auth<'a, 'b>(&'a mut self, auth_baton: &'b mut crate::auth::AuthBaton)
    where
        'b: 'a,
    {
        unsafe {
            (*self.ptr).auth_baton = auth_baton.as_mut_ptr();
        }
    }

    /// Sets the authentication baton for this context (borrowed, unchecked lifetime).
    ///
    /// # Safety
    ///
    /// The caller must ensure the AuthBaton outlives this Context.
    pub unsafe fn set_auth_unchecked(&mut self, auth_baton: &mut crate::auth::AuthBaton) {
        (*self.ptr).auth_baton = auth_baton.as_mut_ptr();
    }

    /// Sets the authentication baton for this context (owned).
    ///
    /// The context takes ownership of the baton, ensuring it lives as long as the context.
    pub fn set_auth_owned(&mut self, mut auth_baton: crate::auth::AuthBaton) {
        unsafe {
            (*self.ptr).auth_baton = auth_baton.as_mut_ptr();
        }
        self.auth_baton = Some(auth_baton);
    }

    /// Set the configuration hash for this context.
    ///
    /// The context takes ownership of the config hash.
    pub fn set_config(&mut self, mut config_hash: crate::config::ConfigHash) {
        unsafe {
            (*self.ptr).config = config_hash.as_mut_ptr();
        }
        self.config_hash = Some(config_hash);
    }

    /// Get a reference to the underlying pool
    pub fn pool(&self) -> &apr::Pool<'_> {
        &self.pool
    }

    /// Get the raw pointer to the context (use with caution)
    pub fn as_ptr(&self) -> *const svn_client_ctx_t {
        self.ptr
    }

    /// Checkout a working copy from url to path.
    pub fn checkout(
        &mut self,
        url: impl AsCanonicalUri,
        path: impl AsCanonicalDirent,
        options: &CheckoutOptions,
    ) -> Result<Revnum, Error<'static>> {
        let peg_revision = options.peg_revision.into();
        let revision = options.revision.into();

        // Canonicalize inputs
        let url = url.as_canonical_uri()?;
        let path = path.as_canonical_dirent()?;

        with_tmp_pool(|pool| unsafe {
            let mut revnum = 0;
            // Convert to C strings for FFI
            let url_cstr = std::ffi::CString::new(url.as_str())?;
            let path_cstr = std::ffi::CString::new(path.as_str())?;

            let err = svn_client_checkout3(
                &mut revnum,
                url_cstr.as_ptr(),
                path_cstr.as_ptr(),
                &peg_revision,
                &revision,
                options.depth.into(),
                options.ignore_externals.into(),
                options.allow_unver_obstructions.into(),
                self.ptr,
                pool.as_mut_ptr(),
            );
            Error::from_raw(err)?;
            Ok(Revnum::from_raw(revnum).unwrap())
        })
    }

    /// Updates working copy paths to a specific revision.
    pub fn update(
        &mut self,
        paths: &[&str],
        revision: Revision,
        options: &UpdateOptions,
    ) -> Result<Vec<Revnum>, Error<'static>> {
        with_tmp_pool(|pool| unsafe {
            let mut result_revs = std::ptr::null_mut();
            // Keep CStrings alive for the duration of the function
            let path_cstrings: Vec<std::ffi::CString> = paths
                .iter()
                .map(|p| crate::dirent::canonicalize_path_or_url(p).unwrap())
                .collect();
            let mut ps = apr::tables::TypedArray::new(pool, paths.len() as i32);
            for path in &path_cstrings {
                ps.push(path.as_ptr() as *mut std::ffi::c_void);
            }

            // Store converted revision to avoid use-after-free
            let rev_c: crate::svn_opt_revision_t = revision.into();

            let err = svn_client_update4(
                &mut result_revs,
                ps.as_ptr(),
                &rev_c,
                options.depth.into(),
                options.depth_is_sticky.into(),
                options.ignore_externals.into(),
                options.allow_unver_obstructions.into(),
                options.adds_as_modifications.into(),
                options.make_parents.into(),
                self.ptr,
                pool.as_mut_ptr(),
            );
            let result_revs: apr::tables::TypedArray<Revnum> =
                apr::tables::TypedArray::<Revnum>::from_ptr(result_revs);
            Error::from_raw(err)?;
            Ok(result_revs.iter().collect())
        })
    }

    /// Switches a working copy path to a different URL.
    pub fn switch(
        &mut self,
        path: impl AsCanonicalDirent,
        url: impl AsCanonicalUri,
        options: &SwitchOptions,
    ) -> Result<Revnum, Error<'static>> {
        // Canonicalize inputs
        let path = path.as_canonical_dirent()?;
        let url = url.as_canonical_uri()?;

        with_tmp_pool(|pool| unsafe {
            let mut result_rev = 0;
            // Convert to C strings for FFI
            let path_cstr = std::ffi::CString::new(path.as_str())?;
            let url_cstr = std::ffi::CString::new(url.as_str())?;

            // Store converted revisions to avoid use-after-free
            let peg_rev_c: crate::svn_opt_revision_t = options.peg_revision.into();
            let rev_c: crate::svn_opt_revision_t = options.revision.into();

            let err = svn_client_switch3(
                &mut result_rev,
                path_cstr.as_ptr(),
                url_cstr.as_ptr(),
                &peg_rev_c,
                &rev_c,
                options.depth.into(),
                options.depth_is_sticky.into(),
                options.ignore_externals.into(),
                options.allow_unver_obstructions.into(),
                options.ignore_ancestry.into(),
                self.ptr,
                pool.as_mut_ptr(),
            );
            Error::from_raw(err)?;
            Ok(Revnum::from_raw(result_rev).unwrap())
        })
    }

    /// Adds a file or directory to version control.
    pub fn add(
        &mut self,
        path: impl AsCanonicalDirent,
        options: &AddOptions,
    ) -> Result<(), Error<'static>> {
        let path = path.as_canonical_dirent()?;
        with_tmp_pool(|pool| unsafe {
            let path_cstr = std::ffi::CString::new(path.as_str())?;
            let err = svn_client_add5(
                path_cstr.as_ptr(),
                options.depth.into(),
                options.force.into(),
                options.no_ignore.into(),
                options.no_autoprops.into(),
                options.add_parents.into(),
                self.ptr,
                pool.as_mut_ptr(),
            );
            Error::from_raw(err)?;
            Ok(())
        })
    }

    /// Creates directories in version control.
    pub fn mkdir(
        &mut self,
        paths: &[&str],
        options: &mut MkdirOptions,
    ) -> Result<(), Error<'static>> {
        with_tmp_pool(|pool| unsafe {
            // Convert revprop_table if provided
            let revprop_hash = options.revprop_table.as_ref().map(|revprops| {
                let svn_strings: Vec<_> = revprops
                    .iter()
                    .map(|(k, v)| (k.as_str(), crate::string::BStr::from_bytes(v, pool)))
                    .collect();

                apr::hash::Hash::from_iter(
                    pool,
                    svn_strings
                        .iter()
                        .map(|(k, v)| (k.as_bytes(), v.as_ptr() as *mut std::ffi::c_void)),
                )
            });

            let revprop_ptr = revprop_hash
                .as_ref()
                .map_or(std::ptr::null_mut(), |h| h.as_ptr() as *mut _);

            // Keep CStrings alive for the duration of the function
            let path_cstrings: Vec<std::ffi::CString> = paths
                .iter()
                .map(|p| crate::dirent::canonicalize_path_or_url(p).unwrap())
                .collect();
            let mut ps = apr::tables::TypedArray::new(pool, paths.len() as i32);
            for path in &path_cstrings {
                ps.push(path.as_ptr() as *mut std::ffi::c_void);
            }

            // Handle commit callback
            let (callback_func, callback_baton) = if let Some(ref mut cb) = options.commit_callback
            {
                (
                    Some(crate::wrap_commit_callback2 as _),
                    cb as *mut _ as *mut std::ffi::c_void,
                )
            } else {
                (None, std::ptr::null_mut())
            };

            let err = svn_client_mkdir4(
                ps.as_ptr(),
                options.make_parents.into(),
                revprop_ptr,
                callback_func,
                callback_baton,
                self.ptr,
                pool.as_mut_ptr(),
            );
            Error::from_raw(err)?;
            Ok(())
        })
    }

    /// Deletes files or directories from version control.
    pub fn delete(
        &mut self,
        paths: &[&str],
        revprop_table: std::collections::HashMap<&str, &str>,
        options: &mut DeleteOptions,
    ) -> Result<(), Error<'static>> {
        with_tmp_pool(|pool| unsafe {
            // Convert revprops to BStr objects that live in the pool
            let svn_strings: Vec<_> = revprop_table
                .iter()
                .map(|(k, v)| (*k, crate::string::BStr::from_str(v, pool)))
                .collect();

            let rps = apr::hash::Hash::from_iter(
                pool,
                svn_strings
                    .iter()
                    .map(|(k, v)| (k.as_bytes(), v.as_ptr() as *mut std::ffi::c_void)),
            );
            // Keep CStrings alive for the duration of the function
            let path_cstrings: Vec<std::ffi::CString> = paths
                .iter()
                .map(|p| crate::dirent::canonicalize_path_or_url(p).unwrap())
                .collect();
            let mut ps = apr::tables::TypedArray::new(pool, paths.len() as i32);
            for path in &path_cstrings {
                ps.push(path.as_ptr() as *mut std::ffi::c_void);
            }
            let (callback_func, callback_baton) = if let Some(ref mut cb) = options.commit_callback
            {
                (
                    Some(crate::wrap_commit_callback2 as _),
                    cb as *mut _ as *mut std::ffi::c_void,
                )
            } else {
                (None, std::ptr::null_mut())
            };
            let err = svn_client_delete4(
                ps.as_ptr(),
                options.force.into(),
                options.keep_local.into(),
                rps.as_ptr(),
                callback_func,
                callback_baton,
                self.ptr,
                pool.as_mut_ptr(),
            );
            Error::from_raw(err)?;
            Ok(())
        })
    }

    /// Lists properties on a path.
    pub fn proplist(
        &mut self,
        target: &str,
        options: &ProplistOptions,
        receiver: &mut dyn FnMut(
            &str,
            &std::collections::HashMap<String, Vec<u8>>,
            Option<&[crate::InheritedItem]>,
        ) -> Result<(), Error<'static>>,
    ) -> Result<(), Error<'static>> {
        with_tmp_pool(|pool| {
            let target_cstr = crate::dirent::canonicalize_path_or_url(target)?;
            let changelists = options.changelists.map(|cl| {
                cl.iter()
                    .map(|cl| std::ffi::CString::new(*cl).unwrap())
                    .collect::<Vec<_>>()
            });
            let changelists = changelists.as_ref().map(|cl| {
                let mut array = apr::tables::TypedArray::<*const i8>::new(pool, 10);
                for item in cl.iter() {
                    array.push(item.as_ptr());
                }
                array
            });

            unsafe {
                let receiver = Box::into_raw(Box::new(receiver));
                // Store converted revisions to avoid use-after-free
                let peg_rev_c: crate::svn_opt_revision_t = options.peg_revision.into();
                let rev_c: crate::svn_opt_revision_t = options.revision.into();
                let err = svn_client_proplist4(
                    target_cstr.as_ptr(),
                    &peg_rev_c,
                    &rev_c,
                    options.depth.into(),
                    changelists
                        .map(|cl| cl.as_ptr())
                        .unwrap_or(std::ptr::null()),
                    options.get_target_inherited_props.into(),
                    Some(wrap_proplist_receiver2),
                    receiver as *mut std::ffi::c_void,
                    self.ptr,
                    pool.as_mut_ptr(),
                );
                // Clean up the boxed receiver
                let _ = Box::from_raw(receiver);
                Error::from_raw(err)?;
                Ok(())
            }
        })
    }

    /// Imports an unversioned path into the repository.
    pub fn import(
        &mut self,
        path: impl AsCanonicalDirent,
        url: &str,
        options: &mut ImportOptions,
    ) -> Result<(), Error<'static>> {
        let path = path.as_canonical_dirent()?;
        with_tmp_pool(|pool| {
            // Convert revprop_table if provided
            let revprop_hash = options.revprop_table.as_ref().map(|revprops| {
                let svn_strings: Vec<_> = revprops
                    .iter()
                    .map(|(k, v)| (k.as_str(), crate::string::BStr::from_str(v, pool)))
                    .collect();

                apr::hash::Hash::from_iter(
                    pool,
                    svn_strings
                        .iter()
                        .map(|(k, v)| (k.as_bytes(), v.as_ptr() as *mut std::ffi::c_void)),
                )
            });

            let revprop_ptr = revprop_hash
                .as_ref()
                .map_or(std::ptr::null_mut(), |h| unsafe { h.as_ptr() as *mut _ });

            unsafe {
                // Handle filter callback
                let (filter_func, filter_baton) = if let Some(ref mut cb) = options.filter_callback
                {
                    let boxed = Box::into_raw(Box::new(cb));
                    (
                        Some(wrap_filter_callback as _),
                        boxed as *mut std::ffi::c_void,
                    )
                } else {
                    (None, std::ptr::null_mut())
                };

                // Handle commit callback
                let (commit_func, commit_baton) = if let Some(ref mut cb) = options.commit_callback
                {
                    let boxed = Box::into_raw(Box::new(cb));
                    (
                        Some(crate::wrap_commit_callback2 as _),
                        boxed as *mut std::ffi::c_void,
                    )
                } else {
                    (None, std::ptr::null_mut())
                };

                let path_cstr = std::ffi::CString::new(path.as_str())?;
                let url_cstr = std::ffi::CString::new(url)?;
                let err = svn_client_import5(
                    path_cstr.as_ptr(),
                    url_cstr.as_ptr(),
                    options.depth.into(),
                    options.no_ignore.into(),
                    options.no_autoprops.into(),
                    options.ignore_unknown_node_types.into(),
                    revprop_ptr,
                    filter_func,
                    filter_baton,
                    commit_func,
                    commit_baton,
                    self.ptr,
                    pool.as_mut_ptr(),
                );

                // Free the boxed callbacks to prevent memory leak
                if !filter_baton.is_null() {
                    drop(Box::from_raw(
                        filter_baton
                            as *mut &mut dyn FnMut(
                                &mut bool,
                                &std::path::Path,
                                &Dirent,
                            )
                                -> Result<(), Error<'static>>,
                    ));
                }
                if !commit_baton.is_null() {
                    drop(Box::from_raw(
                        commit_baton
                            as *mut &mut dyn FnMut(
                                &crate::CommitInfo,
                            )
                                -> Result<(), Error<'static>>,
                    ));
                }

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

    /// Exports a versioned path to an unversioned path.
    ///
    /// Returns the revision that was exported, or `None` when `svn_client_export5`
    /// does not report a specific revision (e.g. when exporting a single file).
    pub fn export(
        &mut self,
        from_path_or_url: &str,
        to_path: impl AsCanonicalDirent,
        options: &ExportOptions,
    ) -> Result<Option<Revnum>, Error<'static>> {
        let native_eol: Option<&str> = options.native_eol.into();
        let native_eol = native_eol.map(|s| std::ffi::CString::new(s).unwrap());
        let mut revnum = 0;
        let to_path = to_path.as_canonical_dirent()?;
        with_tmp_pool(|tmp_pool| unsafe {
            let path_cstr = crate::dirent::to_absolute_cstring(to_path)?;
            let from_cstr = crate::dirent::canonicalize_path_or_url(from_path_or_url)?;
            // Store converted revisions to avoid use-after-free
            let peg_rev_c: crate::svn_opt_revision_t = options.peg_revision.into();
            let rev_c: crate::svn_opt_revision_t = options.revision.into();
            let err = svn_client_export5(
                &mut revnum,
                from_cstr.as_ptr(),
                path_cstr.as_ptr(),
                &peg_rev_c,
                &rev_c,
                options.overwrite as i32,
                options.ignore_externals as i32,
                options.ignore_keywords as i32,
                options.depth.into(),
                native_eol.map(|s| s.as_ptr()).unwrap_or(std::ptr::null()),
                self.ptr,
                tmp_pool.as_mut_ptr(),
            );
            Error::from_raw(err)?;
            Ok(Revnum::from_raw(revnum))
        })
    }

    /// Commits changes from a working copy to the repository.
    ///
    /// # Parameters
    ///
    /// * `targets` - Paths to commit
    /// * `options` - Commit options
    /// * `revprop_table` - Revision properties. If `log_msg_func` is None and this contains
    ///   "svn:log", that will be used as the commit message and removed from the revprops
    ///   passed to SVN.
    /// * `log_msg_func` - Optional callback to get the log message for the commit. If None,
    ///   the log message will be extracted from revprop_table["svn:log"] or default to empty.
    /// * `commit_callback` - Callback called after the commit completes
    pub fn commit(
        &mut self,
        targets: &[&str],
        options: &CommitOptions,
        mut revprop_table: std::collections::HashMap<&str, &str>,
        log_msg_func: Option<&mut dyn FnMut(&[CommitItem]) -> Result<String, Error<'static>>>,
        commit_callback: &mut dyn FnMut(&crate::CommitInfo) -> Result<(), Error<'static>>,
    ) -> Result<(), Error<'static>> {
        with_tmp_pool(|pool| {
            // Extract svn:log from revprops if present (SVN doesn't allow it in revprop_table)
            let log_message = revprop_table.remove("svn:log").map(|s| s.to_string());

            // Convert revprops to BStr objects that live in the pool
            let svn_strings: Vec<_> = revprop_table
                .iter()
                .map(|(k, v)| (*k, crate::string::BStr::from_str(v, pool)))
                .collect();

            let rps = apr::hash::Hash::from_iter(
                pool,
                svn_strings
                    .iter()
                    .map(|(k, v)| (k.as_bytes(), v.as_ptr() as *mut std::ffi::c_void)),
            );

            unsafe {
                // Create the log message callback
                // If user provided one, use it; otherwise use the extracted log message
                let mut default_log_func;
                let log_msg_callback: &mut dyn FnMut(
                    &[CommitItem],
                )
                    -> Result<String, Error<'static>> = if let Some(func) = log_msg_func {
                    func
                } else {
                    default_log_func =
                        |_items: &[CommitItem]| Ok(log_message.clone().unwrap_or_default());
                    &mut default_log_func
                };

                // Set up the log message callback on the context
                let log_msg_baton = Box::into_raw(Box::new(log_msg_callback));
                let old_log_msg_func = (*self.ptr).log_msg_func3;
                let old_log_msg_baton = (*self.ptr).log_msg_baton3;
                (*self.ptr).log_msg_func3 = Some(log_msg_trampoline);
                (*self.ptr).log_msg_baton3 = log_msg_baton as *mut std::ffi::c_void;

                // Keep CStrings alive for the duration of the function
                let target_cstrings: Vec<std::ffi::CString> = targets
                    .iter()
                    .map(|t| crate::dirent::canonicalize_path_or_url(t).unwrap())
                    .collect();
                let mut ps = apr::tables::TypedArray::new(pool, targets.len() as i32);
                for target in &target_cstrings {
                    ps.push(target.as_ptr() as *mut std::ffi::c_void);
                }

                let changelist_cstrings: Vec<std::ffi::CString> =
                    if let Some(changelists) = &options.changelists {
                        changelists
                            .iter()
                            .map(|c| std::ffi::CString::new(c.as_str()).unwrap())
                            .collect()
                    } else {
                        Vec::new()
                    };
                let mut cl = apr::tables::TypedArray::new(pool, changelist_cstrings.len() as i32);
                for changelist in &changelist_cstrings {
                    cl.push(changelist.as_ptr() as *mut std::ffi::c_void);
                }
                let commit_callback_baton = Box::into_raw(Box::new(commit_callback));
                let err = svn_client_commit6(
                    ps.as_ptr(),
                    options.depth.into(),
                    options.keep_locks.into(),
                    options.keep_changelists.into(),
                    options.commit_as_operations.into(),
                    options.include_file_externals.into(),
                    options.include_dir_externals.into(),
                    cl.as_ptr(),
                    rps.as_ptr(),
                    Some(crate::wrap_commit_callback2),
                    commit_callback_baton as *mut std::ffi::c_void,
                    self.ptr,
                    pool.as_mut_ptr(),
                );

                // Restore the old log message callback
                (*self.ptr).log_msg_func3 = old_log_msg_func;
                (*self.ptr).log_msg_baton3 = old_log_msg_baton;

                // Free the boxed callbacks to prevent memory leak
                drop(Box::from_raw(log_msg_baton));
                drop(Box::from_raw(commit_callback_baton));

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

    /// Gets the status of a working copy path.
    pub fn status(
        &mut self,
        path: &str,
        options: &StatusOptions,
        status_func: &dyn FnMut(&'_ str, &'_ Status) -> Result<(), Error<'static>>,
    ) -> Result<Option<Revnum>, Error<'static>> {
        with_tmp_pool(|pool| {
            let path_cstr = crate::dirent::canonicalize_path_or_url(path)?;
            let changelist_cstrings: Vec<std::ffi::CString> =
                if let Some(changelists) = &options.changelists {
                    changelists
                        .iter()
                        .map(|cl| std::ffi::CString::new(cl.as_str()).unwrap())
                        .collect()
                } else {
                    Vec::new()
                };
            let mut cl = apr::tables::TypedArray::new(pool, changelist_cstrings.len() as i32);
            for changelist in &changelist_cstrings {
                cl.push(changelist.as_ptr() as *mut std::ffi::c_void);
            }

            unsafe {
                let status_func = Box::into_raw(Box::new(status_func));
                let mut revnum = 0;
                // Store converted revision to avoid use-after-free
                let rev_c: crate::svn_opt_revision_t = options.revision.into();
                let err = svn_client_status6(
                    &mut revnum,
                    self.ptr,
                    path_cstr.as_ptr(),
                    &rev_c,
                    options.depth.into(),
                    options.get_all.into(),
                    options.check_out_of_date.into(),
                    options.check_working_copy.into(),
                    options.no_ignore.into(),
                    options.ignore_externals.into(),
                    options.depth_as_sticky.into(),
                    cl.as_ptr(),
                    Some(wrap_status_func),
                    status_func as *mut std::ffi::c_void,
                    pool.as_mut_ptr(),
                );
                // Clean up the boxed status_func
                let _ = Box::from_raw(status_func);
                Error::from_raw(err)?;
                Ok(Revnum::from_raw(revnum))
            }
        })
    }

    /// Retrieve log messages for a set of paths.
    ///
    /// The receiver callback will be called for each log entry found.
    /// Return an error from the callback to stop iteration early.
    pub fn log(
        &mut self,
        targets: &[&str],
        revision_ranges: &[RevisionRange],
        options: &LogOptions,
        log_entry_receiver: &dyn FnMut(&LogEntry) -> Result<(), Error<'static>>,
    ) -> Result<(), Error<'static>> {
        with_tmp_pool(|pool| unsafe {
            // Keep CStrings alive for the duration of the function
            let target_cstrings: Vec<std::ffi::CString> = targets
                .iter()
                .map(|t| crate::dirent::canonicalize_path_or_url(t).unwrap())
                .collect();
            let mut ps = apr::tables::TypedArray::new(pool, targets.len() as i32);
            for target in &target_cstrings {
                ps.push(target.as_ptr() as *mut std::ffi::c_void);
            }

            let mut rrs =
                apr::tables::TypedArray::<*mut subversion_sys::svn_opt_revision_range_t>::new(
                    pool,
                    revision_ranges.len() as i32,
                );
            for revision_range in revision_ranges {
                rrs.push(revision_range.to_c(pool));
            }

            // Keep CStrings and array alive for the duration of the function
            // According to SVN C API: NULL means "all revprops", array means "only these revprops"
            let revprop_cstrings: Vec<std::ffi::CString> = options
                .revprops
                .as_ref()
                .map(|rps| {
                    rps.iter()
                        .map(|r| std::ffi::CString::new(r.as_str()).unwrap())
                        .collect()
                })
                .unwrap_or_default();
            let mut rps_array;
            let rps_ptr = if options.revprops.is_some() {
                rps_array = apr::tables::TypedArray::new(pool, revprop_cstrings.len() as i32);
                for revprop in &revprop_cstrings {
                    rps_array.push(revprop.as_ptr() as *mut std::ffi::c_void);
                }
                rps_array.as_ptr()
            } else {
                std::ptr::null()
            };
            // Store converted revision to avoid use-after-free
            let peg_rev_c: crate::svn_opt_revision_t = options.peg_revision.into();
            let err = svn_client_log5(
                ps.as_ptr(),
                &peg_rev_c,
                rrs.as_ptr(),
                options.limit.unwrap_or(0),
                options.discover_changed_paths.into(),
                options.strict_node_history.into(),
                options.include_merged_revisions.into(),
                rps_ptr,
                Some(crate::wrap_log_entry_receiver),
                &log_entry_receiver as *const _ as *mut std::ffi::c_void,
                self.ptr,
                pool.as_mut_ptr(),
            );
            Error::from_raw(err)?;
            Ok(())
        })
    }

    /// Retrieve log messages with control flow support.
    ///
    /// The receiver can return `ControlFlow::Break(())` to stop iteration early
    /// or `ControlFlow::Continue(())` to continue processing.
    pub fn log_with_control<F>(
        &mut self,
        targets: &[&str],
        revision_ranges: &[RevisionRange],
        options: &LogOptions,
        mut receiver: F,
    ) -> Result<(), Error<'static>>
    where
        F: FnMut(&LogEntry) -> ControlFlow<()>,
    {
        self.log(
            targets,
            revision_ranges,
            options,
            &|entry| match receiver(entry) {
                ControlFlow::Continue(()) => Ok(()),
                ControlFlow::Break(()) => {
                    // Return a cancellation error to stop iteration
                    Err(Error::with_raw_status(
                        subversion_sys::svn_errno_t_SVN_ERR_CANCELLED as i32,
                        None,
                        "Log iteration stopped by user",
                    ))
                }
            },
        )
    }

    /// Retrieve log messages for revisions related to mergeinfo.
    ///
    /// This shows either merged revisions (finding_merged=true) or eligible
    /// revisions that could be merged (finding_merged=false) between a source
    /// and target.
    pub fn mergeinfo_log(
        &mut self,
        target_path_or_url: &str,
        source_path_or_url: &str,
        options: &MergeinfoLogOptions,
        log_entry_receiver: &mut dyn FnMut(&crate::LogEntry) -> Result<(), Error<'static>>,
    ) -> Result<(), Error<'static>> {
        with_tmp_pool(|pool| {
            let target_c = crate::dirent::canonicalize_path_or_url(target_path_or_url).unwrap();
            let source_c = crate::dirent::canonicalize_path_or_url(source_path_or_url).unwrap();

            // Convert revprops to C strings and create APR array
            let revprop_cstrings: Vec<std::ffi::CString> = options
                .revprops
                .iter()
                .map(|s| std::ffi::CString::new(s.as_str()).unwrap())
                .collect();
            let mut rps = apr::tables::TypedArray::new(pool, revprop_cstrings.len() as i32);
            for revprop in &revprop_cstrings {
                rps.push(revprop.as_ptr() as *mut std::ffi::c_void);
            }

            // Store converted revisions to avoid use-after-free
            let target_peg_rev_c: crate::svn_opt_revision_t = options.target_peg_revision.into();
            let source_peg_rev_c: crate::svn_opt_revision_t = options.source_peg_revision.into();
            let source_start_rev_c: crate::svn_opt_revision_t =
                options.source_start_revision.into();
            let source_end_rev_c: crate::svn_opt_revision_t = options.source_end_revision.into();

            let err = unsafe {
                subversion_sys::svn_client_mergeinfo_log2(
                    options.finding_merged as i32,
                    target_c.as_ptr(),
                    &target_peg_rev_c,
                    source_c.as_ptr(),
                    &source_peg_rev_c,
                    &source_start_rev_c,
                    &source_end_rev_c,
                    Some(crate::wrap_log_entry_receiver),
                    &log_entry_receiver as *const _ as *mut std::ffi::c_void,
                    options.discover_changed_paths as i32,
                    options.depth.into(),
                    rps.as_ptr(),
                    self.as_mut_ptr(),
                    pool.as_mut_ptr(),
                )
            };
            Error::from_raw(err)?;
            Ok(())
        })
    }

    /// Get the mergeinfo for a single target node (ignoring any subtrees).
    ///
    /// Returns mergeinfo describing the ranges which have been merged into
    /// `path_or_url` as of `peg_revision`, per the node's explicit mergeinfo
    /// or inherited mergeinfo if no explicit mergeinfo is found.
    ///
    /// If no explicit or inherited mergeinfo is found, returns `None`.
    ///
    /// # Arguments
    /// * `path_or_url` - Target path or URL
    /// * `peg_revision` - Peg revision for the target
    ///
    /// # Returns
    /// A Mergeinfo object mapping merge source URLs to rangelists,
    /// or None if no mergeinfo found.
    ///
    /// Note: Unlike most APIs which deal with mergeinfo, the keys in the returned
    /// Mergeinfo are absolute repository URLs rather than repository filesystem paths.
    ///
    /// This wraps `svn_client_mergeinfo_get_merged`.
    pub fn mergeinfo_get_merged(
        &mut self,
        path_or_url: &str,
        peg_revision: &Revision,
    ) -> Result<Option<crate::mergeinfo::Mergeinfo>, Error<'_>> {
        let pool = apr::Pool::new();
        let path_c = crate::dirent::canonicalize_path_or_url(path_or_url)?;
        let mut mergeinfo_hash: *mut apr_sys::apr_hash_t = std::ptr::null_mut();

        let peg_rev: subversion_sys::svn_opt_revision_t = (*peg_revision).into();
        let err = unsafe {
            subversion_sys::svn_client_mergeinfo_get_merged(
                &mut mergeinfo_hash,
                path_c.as_ptr(),
                &peg_rev,
                self.as_mut_ptr(),
                pool.as_mut_ptr(),
            )
        };
        Error::from_raw(err)?;

        if mergeinfo_hash.is_null() {
            return Ok(None);
        }

        unsafe {
            Ok(Some(crate::mergeinfo::Mergeinfo::from_ptr_and_pool(
                mergeinfo_hash,
                pool,
            )))
        }
    }

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

        // Clone data that needs to move to the worker thread
        let targets: Vec<String> = targets.iter().map(|s| s.to_string()).collect();
        let revision_ranges = revision_ranges.to_vec();
        let options = options.clone();

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

        let handle = std::thread::spawn(move || {
            let ctx = unsafe { &mut *(ctx_addr as *mut Context) };
            let target_refs: Vec<&str> = targets.iter().map(|s| s.as_str()).collect();

            let result = ctx.log(&target_refs, &revision_ranges, &options, &|entry| {
                let owned = OwnedLogEntry::from_log_entry(entry);
                // If the receiver is dropped, stop iteration
                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 {
                // Don't propagate cancellation errors caused by dropping the iterator
                if e.raw_apr_err() != subversion_sys::svn_errno_t_SVN_ERR_CANCELLED as i32 {
                    let _ = tx.send(Err(e));
                }
            }
        });

        LogIterator {
            rx,
            handle: Some(handle),
            _phantom: std::marker::PhantomData,
        }
    }

    /// Converts command-line arguments to a target array.
    pub fn args_to_target_array(
        &mut self,
        mut os: apr::getopt::Getopt,
        known_targets: &[&str],
        keep_last_origpath_on_truepath_collision: bool,
    ) -> Result<Vec<String>, crate::Error<'_>> {
        let pool = apr::pool::Pool::new();
        let known_targets = known_targets
            .iter()
            .map(|s| std::ffi::CString::new(*s).unwrap())
            .collect::<Vec<_>>();
        let mut targets = std::ptr::null_mut();
        let err = unsafe {
            subversion_sys::svn_client_args_to_target_array2(
                &mut targets,
                os.as_mut_ptr(),
                {
                    let mut array = apr::tables::TypedArray::<*const i8>::new(&pool, 10);
                    for s in known_targets {
                        array.push(s.as_ptr());
                    }
                    array.as_ptr()
                },
                self.ptr,
                keep_last_origpath_on_truepath_collision.into(),
                pool.as_mut_ptr(),
            )
        };
        Error::from_raw(err)?;
        let targets = unsafe { apr::tables::TypedArray::<*const i8>::from_ptr(targets) };
        Ok(targets
            .iter()
            .map(|s| unsafe { std::ffi::CStr::from_ptr(*s as *const i8) })
            .map(|s| s.to_str().unwrap().to_owned())
            .collect::<Vec<_>>())
    }

    /// Vacuums pristine copies from a working copy.
    pub fn vacuum(
        &mut self,
        path: impl AsCanonicalDirent,
        options: &VacuumOptions,
    ) -> Result<(), Error<'static>> {
        let path = crate::dirent::to_absolute_cstring(path)?;
        with_tmp_pool(|pool| unsafe {
            let err = svn_client_vacuum(
                path.as_ptr(),
                options.remove_unversioned_items.into(),
                options.remove_ignored_items.into(),
                options.fix_recorded_timestamps.into(),
                options.vacuum_pristines.into(),
                options.include_externals.into(),
                self.ptr,
                pool.as_mut_ptr(),
            );
            Error::from_raw(err)
        })
    }

    /// Cleans up a working copy.
    pub fn cleanup(
        &mut self,
        path: impl AsCanonicalDirent,
        options: &CleanupOptions,
    ) -> Result<(), Error<'static>> {
        let path = crate::dirent::to_absolute_cstring(path)?;
        with_tmp_pool(|pool| unsafe {
            let err = svn_client_cleanup2(
                path.as_ptr(),
                options.break_locks.into(),
                options.fix_recorded_timestamps.into(),
                options.clear_dav_cache.into(),
                options.vacuum_pristines.into(),
                options.include_externals.into(),
                self.ptr,
                pool.as_mut_ptr(),
            );
            Error::from_raw(err)
        })
    }

    /// Gets conflict information for a path.
    pub fn conflict_get(
        &mut self,
        local_abspath: impl AsCanonicalDirent,
    ) -> Result<Conflict, Error<'static>> {
        let pool = apr::Pool::new();
        let local_abspath = local_abspath.as_canonical_dirent()?;
        let mut conflict: *mut subversion_sys::svn_client_conflict_t = std::ptr::null_mut();
        with_tmp_pool(|tmp_pool| unsafe {
            let path_cstr = crate::dirent::to_absolute_cstring(local_abspath)?;
            let err = svn_client_conflict_get(
                &mut conflict,
                path_cstr.as_ptr(),
                self.ptr,
                pool.as_mut_ptr(), // Use persistent pool for conflict allocation
                tmp_pool.as_mut_ptr(), // Use tmp_pool only for scratch
            );
            Error::from_raw(err)?;
            Ok(Conflict::from_ptr_and_pool(conflict, pool))
        })
    }

    /// Walk all conflicts within the specified depth of a path.
    ///
    /// Calls the provided closure for each conflict found.
    pub fn conflict_walk<F>(
        &mut self,
        local_abspath: impl AsCanonicalDirent,
        depth: crate::Depth,
        mut callback: F,
    ) -> Result<(), Error<'static>>
    where
        F: FnMut(Conflict) -> Result<(), Error<'static>>,
    {
        let local_abspath = local_abspath.as_canonical_dirent()?;
        let path_cstr = crate::dirent::to_absolute_cstring(local_abspath)?;

        struct WalkBaton<'a, F: FnMut(Conflict) -> Result<(), Error<'static>>> {
            callback: &'a mut F,
            error: Option<Error<'static>>,
        }

        unsafe extern "C" fn walk_func<F: FnMut(Conflict) -> Result<(), Error<'static>>>(
            baton: *mut std::ffi::c_void,
            conflict: *mut subversion_sys::svn_client_conflict_t,
            scratch_pool: *mut apr_sys::apr_pool_t,
        ) -> *mut subversion_sys::svn_error_t {
            let baton = &mut *(baton as *mut WalkBaton<F>);
            // The conflict is valid for the duration of this callback
            let pool = apr::Pool::from_raw(scratch_pool);

            let conflict_obj = Conflict {
                ptr: conflict,
                pool,
                _phantom: std::marker::PhantomData,
            };
            match (baton.callback)(conflict_obj) {
                Ok(()) => std::ptr::null_mut(),
                Err(e) => {
                    baton.error = Some(e);
                    std::ptr::null_mut()
                }
            }
        }

        let mut baton = WalkBaton {
            callback: &mut callback,
            error: None,
        };

        with_tmp_pool(|pool| unsafe {
            let err = subversion_sys::svn_client_conflict_walk(
                path_cstr.as_ptr(),
                depth.into(),
                Some(walk_func::<F>),
                &mut baton as *mut WalkBaton<F> as *mut std::ffi::c_void,
                self.ptr,
                pool.as_mut_ptr(),
            );
            Error::from_raw(err)?;
            if let Some(e) = baton.error {
                return Err(e);
            }
            Ok(())
        })
    }

    /// Outputs the contents of a file.
    pub fn cat(
        &mut self,
        path_or_url: &str,
        stream: &mut dyn std::io::Write,
        options: &CatOptions,
    ) -> Result<HashMap<String, Vec<u8>>, Error<'_>> {
        let path_or_url = crate::dirent::canonicalize_path_or_url(path_or_url).unwrap();
        let mut s = crate::io::wrap_write(stream)?;
        with_tmp_pool(|result_pool| {
            with_tmp_pool(|scratch_pool| unsafe {
                let mut props: *mut apr::hash::apr_hash_t = std::ptr::null_mut();
                // Store converted revisions to avoid use-after-free
                let peg_rev_c: crate::svn_opt_revision_t = options.peg_revision.into();
                let rev_c: crate::svn_opt_revision_t = options.revision.into();
                let err = subversion_sys::svn_client_cat3(
                    &mut props,
                    s.as_mut_ptr(),
                    path_or_url.as_ptr(),
                    &peg_rev_c,
                    &rev_c,
                    options.expand_keywords.into(),
                    self.as_mut_ptr(),
                    result_pool.as_mut_ptr(),
                    scratch_pool.as_mut_ptr(),
                );
                Error::from_raw(err)?;
                let prop_hash = crate::props::PropHash::from_ptr(props);
                Ok(prop_hash.to_hashmap())
            })
        })
    }

    /// Locks paths in the repository.
    pub fn lock(
        &mut self,
        targets: &[&str],
        comment: &str,
        steal_lock: bool,
    ) -> Result<(), Error<'static>> {
        with_tmp_pool(|pool| {
            let targets = targets
                .iter()
                .map(|s| std::ffi::CString::new(*s).unwrap())
                .collect::<Vec<_>>();
            let mut targets_array = apr::tables::TypedArray::<*const i8>::new(pool, 10);
            for target in targets.iter() {
                targets_array.push(target.as_ptr());
            }
            let comment = std::ffi::CString::new(comment).unwrap();
            unsafe {
                let err = subversion_sys::svn_client_lock(
                    targets_array.as_ptr(),
                    comment.as_ptr(),
                    steal_lock.into(),
                    self.as_mut_ptr(),
                    pool.as_mut_ptr(),
                );
                Error::from_raw(err)?;
                Ok(())
            }
        })
    }

    /// Unlocks paths in the repository.
    pub fn unlock(&mut self, targets: &[&str], break_lock: bool) -> Result<(), Error<'static>> {
        with_tmp_pool(|pool| {
            let targets = targets
                .iter()
                .map(|s| std::ffi::CString::new(*s).unwrap())
                .collect::<Vec<_>>();
            let mut targets_array = apr::tables::TypedArray::<*const i8>::new(pool, 10);
            for target in targets.iter() {
                targets_array.push(target.as_ptr());
            }
            unsafe {
                let err = subversion_sys::svn_client_unlock(
                    targets_array.as_ptr(),
                    break_lock.into(),
                    self.as_mut_ptr(),
                    pool.as_mut_ptr(),
                );
                Error::from_raw(err)?;
                Ok(())
            }
        })
    }

    /// Gets the root of the working copy.
    pub fn get_wc_root(
        &mut self,
        path: impl AsCanonicalDirent,
    ) -> Result<std::path::PathBuf, Error<'static>> {
        let path = path.as_canonical_dirent()?;
        let mut wc_root: *const i8 = std::ptr::null();
        with_tmp_pool(|tmp_pool| unsafe {
            let path_cstr = crate::dirent::to_absolute_cstring(path)?;
            let err = subversion_sys::svn_client_get_wc_root(
                &mut wc_root,
                path_cstr.as_ptr(),
                self.as_mut_ptr(),
                tmp_pool.as_mut_ptr(),
                tmp_pool.as_mut_ptr(),
            );
            Error::from_raw(err)?;
            Ok(std::ffi::CStr::from_ptr(wc_root).to_str().unwrap().into())
        })
    }

    /// Gets the minimum and maximum revisions in a working copy.
    pub fn min_max_revisions(
        &mut self,
        local_abspath: impl AsCanonicalDirent,
        committed: bool,
    ) -> Result<(Revnum, Revnum), Error<'static>> {
        let _scratch_pool = apr::pool::Pool::new();
        let local_abspath = local_abspath.as_canonical_dirent()?;
        let mut min_revision: subversion_sys::svn_revnum_t = 0;
        let mut max_revision: subversion_sys::svn_revnum_t = 0;
        with_tmp_pool(|tmp_pool| unsafe {
            let path_cstr = crate::dirent::to_absolute_cstring(local_abspath)?;
            let err = subversion_sys::svn_client_min_max_revisions(
                &mut min_revision,
                &mut max_revision,
                path_cstr.as_ptr(),
                committed as i32,
                self.as_mut_ptr(),
                tmp_pool.as_mut_ptr(),
            );
            Error::from_raw(err)?;
            Ok((
                Revnum::from_raw(min_revision).unwrap(),
                Revnum::from_raw(max_revision).unwrap(),
            ))
        })
    }

    /// Gets the URL for a working copy path.
    pub fn url_from_path(&mut self, path: impl AsCanonicalUri) -> Result<String, Error<'static>> {
        // Canonicalize input
        let path = path.as_canonical_uri()?;

        with_tmp_pool(|pool| unsafe {
            let mut url: *const i8 = std::ptr::null();
            let path_cstr = std::ffi::CString::new(path.as_str())?;

            let err = subversion_sys::svn_client_url_from_path2(
                &mut url,
                path_cstr.as_ptr(),
                self.as_mut_ptr(),
                pool.as_mut_ptr(),
                pool.as_mut_ptr(),
            );
            Error::from_raw(err)?;
            Ok(std::ffi::CStr::from_ptr(url).to_str().unwrap().into())
        })
    }

    /// Gets the repository root URL and UUID for a path or URL.
    pub fn get_repos_root(
        &mut self,
        path_or_url: &str,
    ) -> Result<(String, String), Error<'static>> {
        let path_or_url = crate::dirent::canonicalize_path_or_url(path_or_url).unwrap();
        with_tmp_pool(|pool| unsafe {
            let mut repos_root: *const i8 = std::ptr::null();
            let mut repos_uuid: *const i8 = std::ptr::null();
            let err = subversion_sys::svn_client_get_repos_root(
                &mut repos_root,
                &mut repos_uuid,
                path_or_url.as_ptr(),
                self.as_mut_ptr(),
                pool.as_mut_ptr(),
                pool.as_mut_ptr(),
            );
            Error::from_raw(err)?;
            Ok((
                std::ffi::CStr::from_ptr(repos_root)
                    .to_str()
                    .unwrap()
                    .into(),
                std::ffi::CStr::from_ptr(repos_uuid)
                    .to_str()
                    .unwrap()
                    .into(),
            ))
        })
    }

    #[cfg(feature = "ra")]
    /// Opens a raw repository access session.
    pub fn open_raw_session(
        &mut self,
        url: &str,
        wri_path: &std::path::Path,
    ) -> Result<crate::ra::Session<'_>, Error<'_>> {
        let url = std::ffi::CString::new(url).unwrap();
        let wri_path = crate::dirent::to_absolute_cstring(wri_path)?;
        let pool = apr::Pool::new();
        unsafe {
            let scratch_pool = Pool::default();
            let mut session: *mut subversion_sys::svn_ra_session_t = std::ptr::null_mut();
            let err = subversion_sys::svn_client_open_ra_session2(
                &mut session,
                url.as_ptr(),
                wri_path.as_ptr(),
                self.as_mut_ptr(),
                pool.as_mut_ptr(),
                scratch_pool.as_mut_ptr(),
            );
            Error::from_raw(err)?;
            Ok(crate::ra::Session::from_ptr_and_pool(session, pool))
        }
    }

    /// Gets information about a path or URL.
    pub fn info(
        &mut self,
        abspath_or_url: &str,
        options: &InfoOptions,
        receiver: &dyn FnMut(&std::path::Path, &Info) -> Result<(), Error<'static>>,
    ) -> Result<(), Error<'static>> {
        // svn_client_info4 requires absolute paths (unlike info3 which accepts relative)
        validate_absolute_path_or_url(abspath_or_url)?;

        with_tmp_pool(|pool| {
            // Canonicalize so svn sees a valid dirent (e.g. a Windows verbatim
            // `\\?\C:\...` path would otherwise trip svn_dirent_is_absolute).
            let abspath_or_url = crate::dirent::canonicalize_path_or_url(abspath_or_url)?;
            let changelists = options.changelists.as_ref().map(|cl| {
                cl.iter()
                    .map(|cl| std::ffi::CString::new(cl.as_str()).unwrap())
                    .collect::<Vec<_>>()
            });
            let changelists = changelists.as_ref().map(|cl| {
                let mut array = apr::tables::TypedArray::<*const i8>::new(pool, 10);
                for item in cl.iter() {
                    array.push(item.as_ptr());
                }
                array
            });
            let mut receiver = receiver;
            let receiver = &mut receiver as *mut _ as *mut std::ffi::c_void;
            // Store converted revisions to avoid use-after-free
            let peg_rev_c: crate::svn_opt_revision_t = options.peg_revision.into();
            let rev_c: crate::svn_opt_revision_t = options.revision.into();
            unsafe {
                let err = subversion_sys::svn_client_info4(
                    abspath_or_url.as_ptr(),
                    &peg_rev_c,
                    &rev_c,
                    options.depth.into(),
                    options.fetch_excluded as i32,
                    options.fetch_actual_only as i32,
                    options.include_externals as i32,
                    changelists.map_or(std::ptr::null(), |cl| cl.as_ptr()),
                    Some(wrap_info_receiver2),
                    receiver,
                    self.as_mut_ptr(),
                    pool.as_mut_ptr(),
                );
                Error::from_raw(err)?;
                Ok(())
            }
        })
    }

    /// Blame/annotate a file, showing revision information for each line
    pub fn blame(
        &mut self,
        path_or_url: &str,
        options: &BlameOptions,
        receiver: &mut dyn FnMut(BlameInfo) -> Result<(), Error<'static>>,
    ) -> Result<(Revnum, Revnum), Error<'static>> {
        with_tmp_pool(|pool| {
            let path_or_url = crate::dirent::canonicalize_path_or_url(path_or_url).unwrap();

            // Convert diff options to C strings
            // Create diff file options struct
            let diff_file_options =
                unsafe { subversion_sys::svn_diff_file_options_create(pool.as_mut_ptr()) };

            if !options.diff_options.is_empty() {
                let diff_options_cstrings: Vec<std::ffi::CString> = options
                    .diff_options
                    .iter()
                    .map(|opt| std::ffi::CString::new(opt.as_str()).unwrap())
                    .collect();

                // Create APR array of diff options for parsing
                let mut diff_opts_array = apr::tables::TypedArray::<*const i8>::new(pool, 10);
                for opt in diff_options_cstrings.iter() {
                    diff_opts_array.push(opt.as_ptr());
                }

                // Parse the options into the diff_file_options struct
                let parse_err = unsafe {
                    subversion_sys::svn_diff_file_options_parse(
                        diff_file_options,
                        diff_opts_array.as_ptr(),
                        pool.as_mut_ptr(),
                    )
                };
                Error::from_raw(parse_err)?;
            }

            // Output parameters for resolved revision numbers
            let mut start_revnum: subversion_sys::svn_revnum_t = 0;
            let mut end_revnum: subversion_sys::svn_revnum_t = 0;

            // Wrap the receiver callback
            let receiver_baton = receiver as *mut _ as *mut std::ffi::c_void;

            // Store converted revisions to avoid use-after-free
            let peg_rev_c: crate::svn_opt_revision_t = options.peg_revision.into();
            let start_rev_c: crate::svn_opt_revision_t = options.start_revision.into();
            let end_rev_c: crate::svn_opt_revision_t = options.end_revision.into();

            unsafe {
                let err = subversion_sys::svn_client_blame6(
                    &mut start_revnum,
                    &mut end_revnum,
                    path_or_url.as_ptr(),
                    &peg_rev_c,
                    &start_rev_c,
                    &end_rev_c,
                    diff_file_options,
                    options.ignore_mime_type as subversion_sys::svn_boolean_t,
                    options.include_merged_revisions as subversion_sys::svn_boolean_t,
                    Some(blame_receiver_wrapper),
                    receiver_baton,
                    self.as_mut_ptr(),
                    pool.as_mut_ptr(),
                );
                Error::from_raw(err)?;
                Ok((
                    Revnum::from_raw(start_revnum).unwrap(),
                    Revnum::from_raw(end_revnum).unwrap(),
                ))
            }
        })
    }

    /// Copy or move a file or directory
    pub fn copy(
        &mut self,
        sources: &[(&str, Option<Revision>)],
        dst_path: &str,
        options: &mut CopyOptions,
    ) -> Result<(), Error<'static>> {
        with_tmp_pool(|pool| {
            // Keep CStrings alive for the duration of the function
            let path_cstrings: Vec<std::ffi::CString> = sources
                .iter()
                .map(|(path, _)| crate::dirent::canonicalize_path_or_url(path).unwrap())
                .collect();

            let sources_array = sources
                .iter()
                .zip(path_cstrings.iter())
                .map(|((_, rev), path_c)| {
                    let src: *mut subversion_sys::svn_client_copy_source_t = pool.calloc();
                    let revision: *mut subversion_sys::svn_opt_revision_t = pool.calloc();
                    let peg_revision: *mut subversion_sys::svn_opt_revision_t = pool.calloc();
                    unsafe {
                        (*src).path = path_c.as_ptr();
                        *revision = rev
                            .as_ref()
                            .map(|r| (*r).into())
                            .unwrap_or(Revision::Head.into());
                        (*src).revision = revision;
                        *peg_revision = Revision::Head.into();
                        (*src).peg_revision = peg_revision;
                    }
                    src
                })
                .collect::<Vec<_>>();

            let mut sources_apr_array = apr::tables::TypedArray::<
                *const subversion_sys::svn_client_copy_source_t,
            >::new(pool, sources_array.len() as i32);
            for src in sources_array.iter() {
                sources_apr_array.push(*src as *const _);
            }

            let dst_c = crate::dirent::canonicalize_path_or_url(dst_path)?;

            // Handle externals_to_pin
            let externals_hash = options.externals_to_pin.as_ref().map(|ext| {
                let svn_strings: Vec<_> = ext
                    .iter()
                    .map(|(k, v)| (k.as_str(), crate::string::BStr::from_str(v, pool)))
                    .collect();
                apr::hash::Hash::from_iter(
                    pool,
                    svn_strings
                        .iter()
                        .map(|(k, v)| (k.as_bytes(), v.as_ptr() as *mut std::ffi::c_void)),
                )
            });

            // Handle revprop_table
            let revprop_hash = options.revprop_table.as_ref().map(|rp| {
                let svn_strings: Vec<_> = rp
                    .iter()
                    .map(|(k, v)| (k.as_str(), crate::string::BStr::from_str(v, pool)))
                    .collect();
                apr::hash::Hash::from_iter(
                    pool,
                    svn_strings
                        .iter()
                        .map(|(k, v)| (k.as_bytes(), v.as_ptr() as *mut std::ffi::c_void)),
                )
            });

            // Handle commit_callback
            let (callback_func, callback_baton) = if let Some(ref mut cb) = options.commit_callback
            {
                (
                    Some(crate::wrap_commit_callback2 as _),
                    cb as *mut _ as *mut std::ffi::c_void,
                )
            } else {
                (None, std::ptr::null_mut())
            };

            let err = unsafe {
                subversion_sys::svn_client_copy7(
                    sources_apr_array.as_ptr(),
                    dst_c.as_ptr(),
                    options.copy_as_child as i32,
                    options.make_parents as i32,
                    options.ignore_externals as i32,
                    options.metadata_only as i32,
                    options.pin_externals as i32,
                    externals_hash
                        .as_ref()
                        .map_or(std::ptr::null_mut(), |h| h.as_ptr()),
                    revprop_hash
                        .as_ref()
                        .map_or(std::ptr::null_mut(), |h| h.as_ptr()),
                    callback_func,
                    callback_baton,
                    self.as_mut_ptr(),
                    pool.as_mut_ptr(),
                )
            };
            svn_result(err)
        })
    }

    /// Create directories (supports multiple paths)
    pub fn mkdir_multiple(
        &mut self,
        paths: &[&str],
        options: &mut MkdirOptions,
    ) -> Result<(), Error<'static>> {
        with_tmp_pool(|pool| {
            let paths_c: Vec<_> = paths
                .iter()
                .map(|p| crate::dirent::canonicalize_path_or_url(p).unwrap())
                .collect();
            let mut paths_array = apr::tables::TypedArray::<*const i8>::new(pool, 0);
            for path in paths_c.iter() {
                paths_array.push(path.as_ptr());
            }

            // Convert revprop_table if provided
            let revprop_hash = options.revprop_table.as_ref().map(|revprops| {
                let svn_strings: Vec<_> = revprops
                    .iter()
                    .map(|(k, v)| (k.as_str(), crate::string::BStr::from_bytes(v, pool)))
                    .collect();

                apr::hash::Hash::from_iter(
                    pool,
                    svn_strings
                        .iter()
                        .map(|(k, v)| (k.as_bytes(), v.as_ptr() as *mut std::ffi::c_void)),
                )
            });

            let revprop_ptr = revprop_hash
                .as_ref()
                .map_or(std::ptr::null_mut(), |h| unsafe { h.as_ptr() as *mut _ });

            // Handle commit callback
            let (callback_func, callback_baton) = if let Some(ref mut cb) = options.commit_callback
            {
                (
                    Some(crate::wrap_commit_callback2 as _),
                    cb as *mut _ as *mut std::ffi::c_void,
                )
            } else {
                (None, std::ptr::null_mut())
            };

            let err = unsafe {
                subversion_sys::svn_client_mkdir4(
                    paths_array.as_ptr(),
                    options.make_parents as i32,
                    revprop_ptr,
                    callback_func,
                    callback_baton,
                    self.as_mut_ptr(),
                    pool.as_mut_ptr(),
                )
            };
            svn_result(err)
        })
    }

    /// Get property value
    pub fn propget(
        &mut self,
        propname: &str,
        target: &str,
        options: &PropGetOptions,
        actual_revnum: Option<&mut Revnum>,
    ) -> Result<std::collections::HashMap<String, Vec<u8>>, Error<'_>> {
        with_tmp_pool(|pool| {
            let propname_c = std::ffi::CString::new(propname).unwrap();
            let target_c = crate::dirent::canonicalize_path_or_url(target)?;

            // Convert changelists if provided
            let (changelists_array, list_cstrings) = if let Some(lists) = &options.changelists {
                let mut array = apr::tables::TypedArray::<*const i8>::new(pool, lists.len() as i32);
                let cstrings: Vec<_> = lists
                    .iter()
                    .map(|l| std::ffi::CString::new(l.as_str()).unwrap())
                    .collect();
                for cstring in &cstrings {
                    array.push(cstring.as_ptr());
                }
                (unsafe { array.as_ptr() }, Some(cstrings))
            } else {
                (std::ptr::null(), None)
            };

            let mut props = std::ptr::null_mut();
            let mut actual_rev = actual_revnum.as_ref().map_or(0, |r| r.0);

            // Store converted revisions to avoid use-after-free
            let peg_rev_c: crate::svn_opt_revision_t = options.peg_revision.into();
            let rev_c: crate::svn_opt_revision_t = options.revision.into();

            let err = unsafe {
                subversion_sys::svn_client_propget5(
                    &mut props,
                    std::ptr::null_mut(), // inherited_props
                    propname_c.as_ptr(),
                    target_c.as_ptr(),
                    &peg_rev_c,
                    &rev_c,
                    if actual_revnum.is_some() {
                        &mut actual_rev
                    } else {
                        std::ptr::null_mut()
                    },
                    options.depth.into(),
                    changelists_array,
                    self.as_mut_ptr(),
                    pool.as_mut_ptr(),
                    pool.as_mut_ptr(),
                )
            };

            // Keep list_cstrings alive until after the call
            drop(list_cstrings);

            svn_result(err)?;

            if let Some(revnum) = actual_revnum {
                *revnum = Revnum(actual_rev);
            }

            if props.is_null() {
                return Ok(std::collections::HashMap::new());
            }

            // Convert apr hash to Rust HashMap
            let prop_hash = unsafe { crate::props::PropHash::from_ptr(props) };
            Ok(prop_hash.to_hashmap())
        })
    }

    /// Get property value with inherited properties.
    ///
    /// This is like `propget()` but also returns inherited properties from parent paths.
    /// Returns a tuple of (properties, inherited_properties).
    pub fn propget_with_inherited(
        &mut self,
        propname: &str,
        target: &str,
        options: &PropGetOptions,
        actual_revnum: Option<&mut Revnum>,
    ) -> Result<
        (
            std::collections::HashMap<String, Vec<u8>>,
            Vec<InheritedPropItem>,
        ),
        Error<'_>,
    > {
        with_tmp_pool(|pool| {
            let propname_c = std::ffi::CString::new(propname).unwrap();
            let target_c = crate::dirent::canonicalize_path_or_url(target).unwrap();

            // Convert changelists if provided
            let (changelists_array, list_cstrings) = if let Some(lists) = &options.changelists {
                let mut array = apr::tables::TypedArray::<*const i8>::new(pool, lists.len() as i32);
                let cstrings: Vec<_> = lists
                    .iter()
                    .map(|l| std::ffi::CString::new(l.as_str()).unwrap())
                    .collect();
                for cstring in &cstrings {
                    array.push(cstring.as_ptr());
                }
                (unsafe { array.as_ptr() }, Some(cstrings))
            } else {
                (std::ptr::null(), None)
            };

            let mut props = std::ptr::null_mut();
            let mut inherited_props = std::ptr::null_mut();
            let mut actual_rev = actual_revnum.as_ref().map_or(0, |r| r.0);

            let err = unsafe {
                subversion_sys::svn_client_propget5(
                    &mut props,
                    &mut inherited_props,
                    propname_c.as_ptr(),
                    target_c.as_ptr(),
                    &options.peg_revision.into(),
                    &options.revision.into(),
                    if actual_revnum.is_some() {
                        &mut actual_rev
                    } else {
                        std::ptr::null_mut()
                    },
                    options.depth.into(),
                    changelists_array,
                    self.as_mut_ptr(),
                    pool.as_mut_ptr(),
                    pool.as_mut_ptr(),
                )
            };

            // Keep list_cstrings alive until after the call
            drop(list_cstrings);

            svn_result(err)?;

            if let Some(revnum) = actual_revnum {
                *revnum = Revnum(actual_rev);
            }

            // Convert props hash to Rust HashMap
            let properties = if props.is_null() {
                std::collections::HashMap::new()
            } else {
                let prop_hash = unsafe { crate::props::PropHash::from_ptr(props) };
                prop_hash.to_hashmap()
            };

            // Convert inherited_props array to Vec<InheritedPropItem>
            let inherited = if inherited_props.is_null() {
                Vec::new()
            } else {
                let array = unsafe {
                    apr::tables::TypedArray::<*mut subversion_sys::svn_prop_inherited_item_t>::from_ptr(inherited_props)
                };
                let mut result = Vec::new();
                for item_ptr in array.iter() {
                    if item_ptr.is_null() {
                        continue;
                    }
                    unsafe {
                        let item = *item_ptr;
                        let path = if item.path_or_url.is_null() {
                            String::new()
                        } else {
                            std::ffi::CStr::from_ptr(item.path_or_url)
                                .to_string_lossy()
                                .into_owned()
                        };
                        let props = if item.prop_hash.is_null() {
                            std::collections::HashMap::new()
                        } else {
                            let prop_hash = crate::props::PropHash::from_ptr(item.prop_hash);
                            prop_hash.to_hashmap()
                        };
                        result.push(InheritedPropItem {
                            path_or_url: path,
                            properties: props,
                        });
                    }
                }
                result
            };

            Ok((properties, inherited))
        })
    }

    /// Set property value
    pub fn propset(
        &mut self,
        propname: &str,
        propval: Option<&[u8]>,
        target: &str,
        options: &PropSetOptions,
    ) -> Result<(), Error<'static>> {
        with_tmp_pool(|pool| {
            let propname_c = std::ffi::CString::new(propname).unwrap();
            let target_c = crate::dirent::canonicalize_path_or_url(target).unwrap();

            // propset_local expects an array of targets
            let mut targets_array = apr::tables::TypedArray::<*const i8>::new(pool, 0);
            targets_array.push(target_c.as_ptr());

            let propval_ptr = propval.map(|val| crate::svn_string_ncreate(val, pool));

            // Convert changelists if provided
            let (changelists_array, list_cstrings) = if let Some(lists) = &options.changelists {
                let mut array = apr::tables::TypedArray::<*const i8>::new(pool, lists.len() as i32);
                let cstrings: Vec<_> = lists
                    .iter()
                    .map(|l| std::ffi::CString::new(l.as_str()).unwrap())
                    .collect();
                for cstring in &cstrings {
                    array.push(cstring.as_ptr());
                }
                (unsafe { array.as_ptr() }, Some(cstrings))
            } else {
                (std::ptr::null(), None)
            };

            let err = unsafe {
                subversion_sys::svn_client_propset_local(
                    propname_c.as_ptr(),
                    propval_ptr
                        .map(|p| p as *const _)
                        .unwrap_or(std::ptr::null()),
                    targets_array.as_ptr(),
                    options.depth.into(),
                    options.skip_checks as i32,
                    changelists_array,
                    self.as_mut_ptr(),
                    pool.as_mut_ptr(),
                )
            };

            // Keep list_cstrings alive until after the call
            drop(list_cstrings);

            svn_result(err)
        })
    }

    /// Sets a property on a remote URL in the repository.
    ///
    /// This sets a property directly on a repository URL, creating a commit.
    /// For working copy property setting, use `propset()` instead.
    pub fn propset_remote(
        &mut self,
        propname: &str,
        propval: Option<&[u8]>,
        url: &str,
        options: &mut PropSetRemoteOptions,
    ) -> Result<(), Error<'static>> {
        with_tmp_pool(|pool| {
            let propname_c = std::ffi::CString::new(propname).unwrap();
            let url_c = std::ffi::CString::new(url).unwrap();

            let propval_ptr = propval.map(|val| crate::svn_string_ncreate(val, pool));

            // Handle revprop_table
            let revprop_hash = options.revprop_table.as_ref().map(|rp| {
                let svn_strings: Vec<_> = rp
                    .iter()
                    .map(|(k, v)| (k.as_str(), crate::string::BStr::from_str(v, pool)))
                    .collect();
                apr::hash::Hash::from_iter(
                    pool,
                    svn_strings
                        .iter()
                        .map(|(k, v)| (k.as_bytes(), v.as_ptr() as *mut std::ffi::c_void)),
                )
            });

            unsafe {
                let (callback_func, callback_baton) = if let Some(ref mut cb) =
                    options.commit_callback
                {
                    (
                        Some(crate::wrap_commit_callback2 as unsafe extern "C" fn(_, _, _) -> _),
                        Box::into_raw(Box::new(cb)) as *mut std::ffi::c_void,
                    )
                } else {
                    (None, std::ptr::null_mut())
                };

                let err = subversion_sys::svn_client_propset_remote(
                    propname_c.as_ptr(),
                    propval_ptr
                        .map(|p| p as *const _)
                        .unwrap_or(std::ptr::null()),
                    url_c.as_ptr(),
                    options.skip_checks as i32,
                    options.base_revision_for_url.0,
                    revprop_hash
                        .as_ref()
                        .map_or(std::ptr::null(), |h| h.as_ptr()),
                    callback_func,
                    callback_baton,
                    self.as_mut_ptr(),
                    pool.as_mut_ptr(),
                );
                svn_result(err)
            }
        })
    }

    /// List properties with callback
    pub fn proplist_all(
        &mut self,
        target: &str,
        options: &ProplistOptions,
        receiver: &mut dyn FnMut(
            &str,
            std::collections::HashMap<String, Vec<u8>>,
        ) -> Result<(), Error<'static>>,
    ) -> Result<(), Error<'static>> {
        with_tmp_pool(|pool| {
            let target_c = crate::dirent::canonicalize_path_or_url(target).unwrap();

            let changelists = options.changelists.map(|cl| {
                let mut array = apr::tables::TypedArray::<*const i8>::new(pool, 0);
                for item in cl.iter() {
                    let item_c = std::ffi::CString::new(*item).unwrap();
                    array.push(item_c.as_ptr());
                }
                array
            });

            extern "C" fn proplist_receiver(
                baton: *mut std::ffi::c_void,
                path: *const i8,
                props: *mut apr_sys::apr_hash_t,
                _inherited_props: *mut apr_sys::apr_array_header_t,
                _scratch_pool: *mut apr_sys::apr_pool_t,
            ) -> *mut subversion_sys::svn_error_t {
                let receiver = unsafe {
                    &mut *(baton
                        as *mut &mut dyn FnMut(
                            &str,
                            std::collections::HashMap<String, Vec<u8>>,
                        )
                            -> Result<(), Error<'static>>)
                };
                let path_str = unsafe { std::ffi::CStr::from_ptr(path).to_str().unwrap() };

                // Convert props hash
                let prop_hash_wrapper = unsafe { crate::props::PropHash::from_ptr(props) };
                let prop_hash = prop_hash_wrapper.to_hashmap();

                match receiver(path_str, prop_hash) {
                    Ok(()) => std::ptr::null_mut(),
                    Err(mut e) => unsafe { e.detach() },
                }
            }

            let receiver_ptr = receiver as *mut _ as *mut std::ffi::c_void;

            let err = unsafe {
                subversion_sys::svn_client_proplist4(
                    target_c.as_ptr(),
                    &options.peg_revision.into(),
                    &options.revision.into(),
                    options.depth.into(),
                    changelists.map_or(std::ptr::null(), |cl| cl.as_ptr()),
                    options.get_target_inherited_props as i32,
                    Some(proplist_receiver),
                    receiver_ptr,
                    self.as_mut_ptr(),
                    pool.as_mut_ptr(),
                )
            };
            svn_result(err)
        })
    }

    /// Get differences between two paths/revisions
    pub fn diff(
        &mut self,
        path_or_url1: &str,
        revision1: &Revision,
        path_or_url2: &str,
        revision2: &Revision,
        relative_to_dir: Option<&str>,
        outstream: &mut crate::io::Stream,
        errstream: &mut crate::io::Stream,
        options: &DiffOptions,
    ) -> Result<(), Error<'static>> {
        with_tmp_pool(|pool| {
            let path1_c = crate::dirent::canonicalize_path_or_url(path_or_url1).unwrap();
            let path2_c = crate::dirent::canonicalize_path_or_url(path_or_url2).unwrap();
            let header_encoding_c =
                std::ffi::CString::new(options.header_encoding.as_str()).unwrap();

            let diff_options_c: Vec<_> = options
                .diff_options
                .iter()
                .map(|o| std::ffi::CString::new(o.as_str()).unwrap())
                .collect();
            let mut diff_options_array = apr::tables::TypedArray::<*const i8>::new(pool, 0);
            for opt in diff_options_c.iter() {
                diff_options_array.push(opt.as_ptr());
            }

            let relative_to_dir_c = relative_to_dir
                .map(crate::dirent::to_absolute_cstring)
                .transpose()?;

            // Convert changelists if provided
            let (changelists_array, list_cstrings) = if let Some(lists) = &options.changelists {
                let mut array = apr::tables::TypedArray::<*const i8>::new(pool, lists.len() as i32);
                let cstrings: Vec<_> = lists
                    .iter()
                    .map(|l| std::ffi::CString::new(l.as_str()).unwrap())
                    .collect();
                for cstring in &cstrings {
                    array.push(cstring.as_ptr());
                }
                (unsafe { array.as_ptr() }, Some(cstrings))
            } else {
                (std::ptr::null(), None)
            };

            let err = unsafe {
                subversion_sys::svn_client_diff7(
                    diff_options_array.as_ptr(),
                    path1_c.as_ptr(),
                    &(*revision1).into(),
                    path2_c.as_ptr(),
                    &(*revision2).into(),
                    relative_to_dir_c
                        .as_ref()
                        .map_or(std::ptr::null(), |c| c.as_ptr()),
                    options.depth.into(),
                    options.ignore_ancestry as i32,
                    options.no_diff_added as i32,
                    options.no_diff_deleted as i32,
                    options.show_copies_as_adds as i32,
                    options.ignore_content_type as i32,
                    options.ignore_properties as i32,
                    options.properties_only as i32,
                    options.use_git_diff_format as i32,
                    options.pretty_print_mergeinfo as i32,
                    header_encoding_c.as_ptr(),
                    outstream.as_mut_ptr(),
                    errstream.as_mut_ptr(),
                    changelists_array,
                    self.as_mut_ptr(),
                    pool.as_mut_ptr(),
                )
            };

            // Keep list_cstrings alive until after the call
            drop(list_cstrings);

            svn_result(err)
        })
    }

    /// Produces diff output comparing a path at two revisions using a peg revision.
    ///
    /// This is similar to `diff()` but uses a single path with a peg revision to
    /// identify the object, then compares it at two different operative revisions.
    pub fn diff_peg(
        &mut self,
        path_or_url: &str,
        peg_revision: &Revision,
        start_revision: &Revision,
        end_revision: &Revision,
        relative_to_dir: Option<&str>,
        outstream: &mut crate::io::Stream,
        errstream: &mut crate::io::Stream,
        options: &DiffOptions,
    ) -> Result<(), Error<'static>> {
        with_tmp_pool(|pool| {
            let path_c = crate::dirent::canonicalize_path_or_url(path_or_url).unwrap();
            let header_encoding_c =
                std::ffi::CString::new(options.header_encoding.as_str()).unwrap();
            let relative_to_dir_c = relative_to_dir
                .map(crate::dirent::to_absolute_cstring)
                .transpose()?;

            // Create diff file options struct
            let diff_file_options =
                unsafe { subversion_sys::svn_diff_file_options_create(pool.as_mut_ptr()) };

            if !options.diff_options.is_empty() {
                let diff_options_cstrings: Vec<std::ffi::CString> = options
                    .diff_options
                    .iter()
                    .map(|opt| std::ffi::CString::new(opt.as_str()).unwrap())
                    .collect();

                // Create APR array of diff options for parsing
                let mut diff_opts_array = apr::tables::TypedArray::<*const i8>::new(pool, 10);
                for opt in diff_options_cstrings.iter() {
                    diff_opts_array.push(opt.as_ptr());
                }

                // Parse the options into the diff_file_options struct
                let parse_err = unsafe {
                    subversion_sys::svn_diff_file_options_parse(
                        diff_file_options,
                        diff_opts_array.as_ptr(),
                        pool.as_mut_ptr(),
                    )
                };
                Error::from_raw(parse_err)?;
            }

            let diff_options_array = unsafe {
                subversion_sys::svn_cstring_split(
                    std::ptr::null(),
                    std::ptr::null(),
                    0,
                    pool.as_mut_ptr(),
                )
            };

            // Convert changelists if provided
            let (changelists_array, list_cstrings) = if let Some(lists) = &options.changelists {
                let mut array = apr::tables::TypedArray::<*const i8>::new(pool, lists.len() as i32);
                let cstrings: Vec<_> = lists
                    .iter()
                    .map(|l| std::ffi::CString::new(l.as_str()).unwrap())
                    .collect();
                for cstring in &cstrings {
                    array.push(cstring.as_ptr());
                }
                (unsafe { array.as_ptr() }, Some(cstrings))
            } else {
                (std::ptr::null(), None)
            };

            let err = unsafe {
                subversion_sys::svn_client_diff_peg7(
                    diff_options_array,
                    path_c.as_ptr(),
                    &(*peg_revision).into(),
                    &(*start_revision).into(),
                    &(*end_revision).into(),
                    relative_to_dir_c
                        .as_ref()
                        .map_or(std::ptr::null(), |c| c.as_ptr()),
                    options.depth.into(),
                    options.ignore_ancestry as i32,
                    options.no_diff_added as i32,
                    options.no_diff_deleted as i32,
                    options.show_copies_as_adds as i32,
                    options.ignore_content_type as i32,
                    options.ignore_properties as i32,
                    options.properties_only as i32,
                    options.use_git_diff_format as i32,
                    options.pretty_print_mergeinfo as i32,
                    header_encoding_c.as_ptr(),
                    outstream.as_mut_ptr(),
                    errstream.as_mut_ptr(),
                    changelists_array,
                    self.as_mut_ptr(),
                    pool.as_mut_ptr(),
                )
            };

            // Keep list_cstrings alive until after the call
            drop(list_cstrings);

            svn_result(err)
        })
    }

    /// Produces a summary of differences between two paths.
    ///
    /// This is like `diff()` but returns a summary of changes (added, modified, deleted)
    /// instead of generating full diff output.
    pub fn diff_summarize(
        &mut self,
        path_or_url1: &str,
        revision1: &Revision,
        path_or_url2: &str,
        revision2: &Revision,
        options: &DiffSummarizeOptions,
        summarize_func: &mut dyn FnMut(DiffSummary) -> Result<(), Error<'static>>,
    ) -> Result<(), Error<'static>> {
        with_tmp_pool(|pool| {
            let path1_c = crate::dirent::canonicalize_path_or_url(path_or_url1).unwrap();
            let path2_c = crate::dirent::canonicalize_path_or_url(path_or_url2).unwrap();

            // Build changelists array if provided
            let (changelists_array, _list_cstrings) = if let Some(lists) = &options.changelists {
                let mut array = apr::tables::TypedArray::<*const i8>::new(pool, lists.len() as i32);
                let cstrings: Vec<_> = lists
                    .iter()
                    .map(|l| std::ffi::CString::new(l.as_str()).unwrap())
                    .collect();
                for cstring in &cstrings {
                    array.push(cstring.as_ptr());
                }
                (unsafe { array.as_ptr() }, Some(cstrings))
            } else {
                (std::ptr::null(), None)
            };

            extern "C" fn summarize_callback(
                diff: *const subversion_sys::svn_client_diff_summarize_t,
                baton: *mut std::ffi::c_void,
                _pool: *mut apr_sys::apr_pool_t,
            ) -> *mut subversion_sys::svn_error_t {
                unsafe {
                    let callback = &mut *(baton
                        as *mut &mut dyn FnMut(DiffSummary) -> Result<(), Error<'static>>);
                    let summary = DiffSummary::from_raw(diff);
                    match callback(summary) {
                        Ok(()) => std::ptr::null_mut(),
                        Err(mut e) => e.detach(),
                    }
                }
            }

            let callback_baton = &summarize_func as *const _ as *mut std::ffi::c_void;

            let err = unsafe {
                subversion_sys::svn_client_diff_summarize2(
                    path1_c.as_ptr(),
                    &(*revision1).into(),
                    path2_c.as_ptr(),
                    &(*revision2).into(),
                    options.depth.into(),
                    options.ignore_ancestry as i32,
                    changelists_array,
                    Some(summarize_callback as unsafe extern "C" fn(_, _, _) -> _),
                    callback_baton,
                    self.as_mut_ptr(),
                    pool.as_mut_ptr(),
                )
            };

            svn_result(err)
        })
    }

    /// Produces a summary of differences using a peg revision.
    ///
    /// This is like `diff_peg()` but returns a summary of changes instead of
    /// generating full diff output.
    pub fn diff_summarize_peg(
        &mut self,
        path_or_url: &str,
        peg_revision: &Revision,
        start_revision: &Revision,
        end_revision: &Revision,
        options: &DiffSummarizeOptions,
        summarize_func: &mut dyn FnMut(DiffSummary) -> Result<(), Error<'static>>,
    ) -> Result<(), Error<'static>> {
        with_tmp_pool(|pool| {
            let path_c = crate::dirent::canonicalize_path_or_url(path_or_url).unwrap();

            // Convert changelists if provided
            let (changelists_array, list_cstrings) = if let Some(lists) = &options.changelists {
                let mut array = apr::tables::TypedArray::<*const i8>::new(pool, lists.len() as i32);
                let cstrings: Vec<_> = lists
                    .iter()
                    .map(|l| std::ffi::CString::new(l.as_str()).unwrap())
                    .collect();
                for cstring in &cstrings {
                    array.push(cstring.as_ptr());
                }
                (unsafe { array.as_ptr() }, Some(cstrings))
            } else {
                (std::ptr::null(), None)
            };

            extern "C" fn summarize_callback(
                diff: *const subversion_sys::svn_client_diff_summarize_t,
                baton: *mut std::ffi::c_void,
                _pool: *mut apr_sys::apr_pool_t,
            ) -> *mut subversion_sys::svn_error_t {
                unsafe {
                    let callback = &mut *(baton
                        as *mut &mut dyn FnMut(DiffSummary) -> Result<(), Error<'static>>);
                    let summary = DiffSummary::from_raw(diff);
                    match callback(summary) {
                        Ok(()) => std::ptr::null_mut(),
                        Err(mut e) => e.detach(),
                    }
                }
            }

            let callback_baton = &summarize_func as *const _ as *mut std::ffi::c_void;

            let err = unsafe {
                subversion_sys::svn_client_diff_summarize_peg2(
                    path_c.as_ptr(),
                    &(*peg_revision).into(),
                    &(*start_revision).into(),
                    &(*end_revision).into(),
                    options.depth.into(),
                    options.ignore_ancestry as i32,
                    changelists_array,
                    Some(summarize_callback as unsafe extern "C" fn(_, _, _) -> _),
                    callback_baton,
                    self.as_mut_ptr(),
                    pool.as_mut_ptr(),
                )
            };

            // Keep list_cstrings alive until after the call
            drop(list_cstrings);

            svn_result(err)
        })
    }

    /// List directory contents
    pub fn list(
        &mut self,
        path_or_url: &str,
        options: &ListOptions,
        list_func: &mut dyn FnMut(
            &str,
            &crate::DirEntry,
            Option<&crate::Lock>,
        ) -> Result<(), Error<'static>>,
    ) -> Result<(), Error<'static>> {
        with_tmp_pool(|pool| {
            let path_or_url_c = crate::dirent::canonicalize_path_or_url(path_or_url).unwrap();

            // Keep CStrings alive for the duration of the function
            let pattern_cstrings: Vec<std::ffi::CString> = options
                .patterns
                .as_ref()
                .map(|pats| {
                    pats.iter()
                        .map(|p| std::ffi::CString::new(p.as_str()).unwrap())
                        .collect()
                })
                .unwrap_or_default();

            let patterns = if !pattern_cstrings.is_empty() {
                let mut array = apr::tables::TypedArray::<*const i8>::new(pool, 0);
                for pattern_c in pattern_cstrings.iter() {
                    array.push(pattern_c.as_ptr());
                }
                Some(array)
            } else if options.patterns.is_some() {
                Some(apr::tables::TypedArray::<*const i8>::new(pool, 0))
            } else {
                None
            };

            extern "C" fn list_receiver(
                baton: *mut std::ffi::c_void,
                path: *const i8,
                dirent: *const subversion_sys::svn_dirent_t,
                lock: *const subversion_sys::svn_lock_t,
                _abs_path: *const i8,
                _external_parent_url: *const i8,
                _external_target: *const i8,
                scratch_pool: *mut apr_sys::apr_pool_t,
            ) -> *mut subversion_sys::svn_error_t {
                let list_func = unsafe {
                    &mut *(baton
                        as *mut &mut dyn FnMut(
                            &str,
                            &crate::DirEntry,
                            Option<&crate::Lock>,
                        )
                            -> Result<(), Error<'static>>)
                };
                let path_str = unsafe { std::ffi::CStr::from_ptr(path).to_str().unwrap() };
                let dirent = crate::DirEntry::from_raw(dirent as *mut _);
                let lock = if lock.is_null() {
                    None
                } else {
                    let pool_handle = unsafe { apr::PoolHandle::from_borrowed_raw(scratch_pool) };
                    Some(crate::Lock::from_raw(lock as *mut _, pool_handle))
                };

                match list_func(path_str, &dirent, lock.as_ref()) {
                    Ok(()) => std::ptr::null_mut(),
                    Err(mut e) => unsafe { e.detach() },
                }
            }

            let list_func_ptr = &list_func as *const _ as *mut std::ffi::c_void;

            let err = unsafe {
                subversion_sys::svn_client_list4(
                    path_or_url_c.as_ptr(),
                    &options.peg_revision.into(),
                    &options.revision.into(),
                    patterns.map_or(std::ptr::null(), |p| p.as_ptr()),
                    options.depth.into(),
                    options.dirent_fields,
                    options.fetch_locks as i32,
                    options.include_externals as i32,
                    Some(list_receiver),
                    list_func_ptr,
                    self.as_mut_ptr(),
                    pool.as_mut_ptr(),
                )
            };
            svn_result(err)
        })
    }

    /// Resolve conflicts
    pub fn resolve(
        &mut self,
        path: &str,
        depth: Depth,
        conflict_choice: crate::ConflictChoice,
    ) -> Result<(), Error<'static>> {
        with_tmp_pool(|pool| {
            let path_c = crate::dirent::canonicalize_path_or_url(path).unwrap();

            let err = unsafe {
                subversion_sys::svn_client_resolve(
                    path_c.as_ptr(),
                    depth.into(),
                    conflict_choice.into(),
                    self.as_mut_ptr(),
                    pool.as_mut_ptr(),
                )
            };
            svn_result(err)
        })
    }
}

// Note: Context now requires a pool parameter, so no Default impl

/// Builder for diff operations
/// Builder for creating diff operations with various options.
pub struct DiffBuilder<'a> {
    ctx: &'a mut Context,
    path1: String,
    revision1: Revision,
    path2: String,
    revision2: Revision,
    depth: Depth,
    diff_options: Vec<String>,
    relative_to_dir: Option<String>,
    ignore_ancestry: bool,
    no_diff_added: bool,
    no_diff_deleted: bool,
    show_copies_as_adds: bool,
    ignore_content_type: bool,
    ignore_properties: bool,
    properties_only: bool,
    use_git_diff_format: bool,
    header_encoding: String,
    changelists: Option<Vec<String>>,
}

impl<'a> DiffBuilder<'a> {
    /// Creates a new DiffBuilder for comparing two paths/revisions.
    pub fn new(
        ctx: &'a mut Context,
        path1: impl Into<String>,
        revision1: Revision,
        path2: impl Into<String>,
        revision2: Revision,
    ) -> Self {
        Self {
            ctx,
            path1: path1.into(),
            revision1,
            path2: path2.into(),
            revision2,
            depth: Depth::Infinity,
            diff_options: Vec::new(),
            relative_to_dir: None,
            ignore_ancestry: false,
            no_diff_added: false,
            no_diff_deleted: false,
            show_copies_as_adds: false,
            ignore_content_type: false,
            ignore_properties: false,
            properties_only: false,
            use_git_diff_format: false,
            header_encoding: "UTF-8".to_string(),
            changelists: None,
        }
    }

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

    /// Sets additional options to pass to the diff command.
    pub fn diff_options(mut self, options: Vec<String>) -> Self {
        self.diff_options = options;
        self
    }

    /// Sets the directory to make paths relative to.
    pub fn relative_to_dir(mut self, dir: impl Into<String>) -> Self {
        self.relative_to_dir = Some(dir.into());
        self
    }

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

    /// Sets whether to omit diffs for added files.
    pub fn no_diff_added(mut self, no_diff: bool) -> Self {
        self.no_diff_added = no_diff;
        self
    }

    /// Sets whether to omit diffs for deleted files.
    pub fn no_diff_deleted(mut self, no_diff: bool) -> Self {
        self.no_diff_deleted = no_diff;
        self
    }

    /// Sets whether to show copies as additions.
    pub fn show_copies_as_adds(mut self, show: bool) -> Self {
        self.show_copies_as_adds = show;
        self
    }

    /// Sets whether to ignore content type differences.
    pub fn ignore_content_type(mut self, ignore: bool) -> Self {
        self.ignore_content_type = ignore;
        self
    }

    /// Sets whether to ignore property differences.
    pub fn ignore_properties(mut self, ignore: bool) -> Self {
        self.ignore_properties = ignore;
        self
    }

    /// Sets whether to show only property differences.
    pub fn properties_only(mut self, only: bool) -> Self {
        self.properties_only = only;
        self
    }

    /// Sets whether to use Git diff format.
    pub fn use_git_diff_format(mut self, use_git: bool) -> Self {
        self.use_git_diff_format = use_git;
        self
    }

    /// Sets the encoding for diff headers.
    pub fn header_encoding(mut self, encoding: impl Into<String>) -> Self {
        self.header_encoding = encoding.into();
        self
    }

    /// Sets the changelists to include in the diff.
    pub fn changelists(mut self, lists: Vec<String>) -> Self {
        self.changelists = Some(lists);
        self
    }

    /// Executes the diff operation.
    pub fn execute(
        self,
        outstream: &mut crate::io::Stream,
        errstream: &mut crate::io::Stream,
    ) -> Result<(), Error<'static>> {
        let mut options = DiffOptions::new()
            .with_diff_options(self.diff_options)
            .with_depth(self.depth)
            .with_ignore_ancestry(self.ignore_ancestry)
            .with_no_diff_added(self.no_diff_added)
            .with_no_diff_deleted(self.no_diff_deleted)
            .with_show_copies_as_adds(self.show_copies_as_adds)
            .with_ignore_content_type(self.ignore_content_type)
            .with_ignore_properties(self.ignore_properties)
            .with_properties_only(self.properties_only)
            .with_use_git_diff_format(self.use_git_diff_format)
            .with_header_encoding(self.header_encoding);

        if let Some(cl) = self.changelists {
            options = options.with_changelists(cl);
        }

        self.ctx.diff(
            &self.path1,
            &self.revision1,
            &self.path2,
            &self.revision2,
            self.relative_to_dir.as_deref(),
            outstream,
            errstream,
            &options,
        )
    }
}

/// Builder for list operations
/// Builder for listing directory contents from the repository.
pub struct ListBuilder<'a> {
    ctx: &'a mut Context,
    path_or_url: String,
    peg_revision: Revision,
    revision: Revision,
    patterns: Option<Vec<String>>,
    depth: Depth,
    dirent_fields: u32,
    fetch_locks: bool,
    include_externals: bool,
}

impl<'a> ListBuilder<'a> {
    /// Creates a new ListBuilder for listing directory entries.
    pub fn new(ctx: &'a mut Context, path_or_url: impl Into<String>) -> Self {
        Self {
            ctx,
            path_or_url: path_or_url.into(),
            peg_revision: Revision::Head,
            revision: Revision::Head,
            patterns: None,
            depth: Depth::Infinity,
            dirent_fields: subversion_sys::SVN_DIRENT_KIND
                | subversion_sys::SVN_DIRENT_SIZE
                | subversion_sys::SVN_DIRENT_HAS_PROPS
                | subversion_sys::SVN_DIRENT_CREATED_REV
                | subversion_sys::SVN_DIRENT_TIME
                | subversion_sys::SVN_DIRENT_LAST_AUTHOR,
            fetch_locks: false,
            include_externals: false,
        }
    }

    /// Sets the peg revision for the path.
    pub fn peg_revision(mut self, rev: Revision) -> Self {
        self.peg_revision = rev;
        self
    }

    /// Sets the revision to list.
    pub fn revision(mut self, rev: Revision) -> Self {
        self.revision = rev;
        self
    }

    /// Sets the glob patterns to filter entries.
    pub fn patterns(mut self, patterns: Vec<String>) -> Self {
        self.patterns = Some(patterns);
        self
    }

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

    /// Sets which dirent fields to retrieve.
    pub fn dirent_fields(mut self, fields: u32) -> Self {
        self.dirent_fields = fields;
        self
    }

    /// Sets whether to fetch lock information.
    pub fn fetch_locks(mut self, fetch: bool) -> Self {
        self.fetch_locks = fetch;
        self
    }

    /// Sets whether to include externals.
    pub fn include_externals(mut self, include: bool) -> Self {
        self.include_externals = include;
        self
    }

    /// Executes the list operation.
    pub fn execute(
        self,
        list_func: &mut dyn FnMut(
            &str,
            &crate::DirEntry,
            Option<&crate::Lock>,
        ) -> Result<(), Error<'static>>,
    ) -> Result<(), Error<'static>> {
        let options = ListOptions {
            peg_revision: self.peg_revision,
            revision: self.revision,
            patterns: self.patterns,
            depth: self.depth,
            dirent_fields: self.dirent_fields,
            fetch_locks: self.fetch_locks,
            include_externals: self.include_externals,
        };

        self.ctx.list(&self.path_or_url, &options, list_func)
    }
}

/// Builder for copy operations
/// Builder for copying versioned items in the repository or working copy.
pub struct CopyBuilder<'a> {
    ctx: &'a mut Context,
    sources: Vec<(String, Option<Revision>)>,
    dst_path: String,
    copy_as_child: bool,
    make_parents: bool,
    ignore_externals: bool,
    metadata_only: bool,
    pin_externals: bool,
}

impl<'a> CopyBuilder<'a> {
    /// Creates a new CopyBuilder for copying versioned items.
    pub fn new(ctx: &'a mut Context, dst_path: impl Into<String>) -> Self {
        Self {
            ctx,
            sources: Vec::new(),
            dst_path: dst_path.into(),
            copy_as_child: false,
            make_parents: false,
            ignore_externals: false,
            metadata_only: false,
            pin_externals: false,
        }
    }

    /// Adds a source path to copy from.
    pub fn add_source(mut self, path: impl Into<String>, revision: Option<Revision>) -> Self {
        self.sources.push((path.into(), revision));
        self
    }

    /// Sets whether to copy as a child of the destination.
    pub fn copy_as_child(mut self, as_child: bool) -> Self {
        self.copy_as_child = as_child;
        self
    }

    /// Sets whether to create parent directories.
    pub fn make_parents(mut self, make: bool) -> Self {
        self.make_parents = make;
        self
    }

    /// Sets whether to ignore externals.
    pub fn ignore_externals(mut self, ignore: bool) -> Self {
        self.ignore_externals = ignore;
        self
    }

    /// Sets whether to copy metadata only.
    pub fn metadata_only(mut self, only: bool) -> Self {
        self.metadata_only = only;
        self
    }

    /// Sets whether to pin externals.
    pub fn pin_externals(mut self, pin: bool) -> Self {
        self.pin_externals = pin;
        self
    }

    /// Executes the copy operation.
    pub fn execute(self) -> Result<(), Error<'static>> {
        let sources: Vec<(&str, Option<Revision>)> = self
            .sources
            .iter()
            .map(|(path, rev)| (path.as_str(), *rev))
            .collect();

        let mut options = CopyOptions::new()
            .with_copy_as_child(self.copy_as_child)
            .with_make_parents(self.make_parents)
            .with_ignore_externals(self.ignore_externals)
            .with_metadata_only(self.metadata_only)
            .with_pin_externals(self.pin_externals);

        self.ctx.copy(&sources, &self.dst_path, &mut options)
    }
}

/// Builder for info operations
/// Builder for retrieving information about versioned items.
pub struct InfoBuilder<'a> {
    ctx: &'a mut Context,
    abspath_or_url: String,
    peg_revision: Revision,
    revision: Revision,
    depth: Depth,
    fetch_excluded: bool,
    fetch_actual_only: bool,
    include_externals: bool,
    changelists: Option<Vec<String>>,
}

impl<'a> InfoBuilder<'a> {
    /// Creates a new InfoBuilder for retrieving information about versioned items.
    pub fn new(ctx: &'a mut Context, abspath_or_url: impl Into<String>) -> Self {
        Self {
            ctx,
            abspath_or_url: abspath_or_url.into(),
            peg_revision: Revision::Head,
            revision: Revision::Head,
            depth: Depth::Infinity,
            fetch_excluded: false,
            fetch_actual_only: false,
            include_externals: false,
            changelists: None,
        }
    }

    /// Sets the peg revision for the path.
    pub fn peg_revision(mut self, rev: Revision) -> Self {
        self.peg_revision = rev;
        self
    }

    /// Sets the revision to get info for.
    pub fn revision(mut self, rev: Revision) -> Self {
        self.revision = rev;
        self
    }

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

    /// Sets whether to fetch excluded items.
    pub fn fetch_excluded(mut self, fetch: bool) -> Self {
        self.fetch_excluded = fetch;
        self
    }

    /// Sets whether to fetch actual nodes only.
    pub fn fetch_actual_only(mut self, fetch: bool) -> Self {
        self.fetch_actual_only = fetch;
        self
    }

    /// Sets whether to include externals.
    pub fn include_externals(mut self, include: bool) -> Self {
        self.include_externals = include;
        self
    }

    /// Sets the changelists to filter by.
    pub fn changelists(mut self, lists: Vec<String>) -> Self {
        self.changelists = Some(lists);
        self
    }

    /// Executes the info operation.
    pub fn execute(
        self,
        receiver: &dyn FnMut(&std::path::Path, &Info) -> Result<(), Error<'static>>,
    ) -> Result<(), Error<'static>> {
        let options = InfoOptions {
            peg_revision: self.peg_revision,
            revision: self.revision,
            depth: self.depth,
            fetch_excluded: self.fetch_excluded,
            fetch_actual_only: self.fetch_actual_only,
            include_externals: self.include_externals,
            changelists: self.changelists,
        };

        self.ctx.info(&self.abspath_or_url, &options, receiver)
    }
}

/// Builder for commit operations
/// Builder for creating commit operations with various options.
pub struct CommitBuilder<'a> {
    ctx: &'a mut Context,
    targets: Vec<String>,
    depth: Depth,
    keep_locks: bool,
    keep_changelists: bool,
    commit_as_operations: bool,
    include_file_externals: bool,
    include_dir_externals: bool,
    changelists: Option<Vec<String>>,
    revprop_table: std::collections::HashMap<String, String>,
}

impl<'a> CommitBuilder<'a> {
    /// Creates a new CommitBuilder for committing changes.
    pub fn new(ctx: &'a mut Context) -> Self {
        Self {
            ctx,
            targets: Vec::new(),
            depth: Depth::Infinity,
            keep_locks: false,
            keep_changelists: false,
            commit_as_operations: true,
            include_file_externals: false,
            include_dir_externals: false,
            changelists: None,
            revprop_table: std::collections::HashMap::new(),
        }
    }

    /// Adds a target path to commit.
    pub fn add_target(mut self, target: impl Into<String>) -> Self {
        self.targets.push(target.into());
        self
    }

    /// Sets the target paths to commit.
    pub fn targets(mut self, targets: Vec<String>) -> Self {
        self.targets = targets;
        self
    }

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

    /// Sets whether to keep locks after commit.
    pub fn keep_locks(mut self, keep: bool) -> Self {
        self.keep_locks = keep;
        self
    }

    /// Sets whether to keep changelists after commit.
    pub fn keep_changelists(mut self, keep: bool) -> Self {
        self.keep_changelists = keep;
        self
    }

    /// Sets whether to commit as operations.
    pub fn commit_as_operations(mut self, as_ops: bool) -> Self {
        self.commit_as_operations = as_ops;
        self
    }

    /// Sets whether to include file externals.
    pub fn include_file_externals(mut self, include: bool) -> Self {
        self.include_file_externals = include;
        self
    }

    /// Sets whether to include directory externals.
    pub fn include_dir_externals(mut self, include: bool) -> Self {
        self.include_dir_externals = include;
        self
    }

    /// Sets the changelists to commit.
    pub fn changelists(mut self, lists: Vec<String>) -> Self {
        self.changelists = Some(lists);
        self
    }

    /// Adds a revision property.
    pub fn add_revprop(mut self, name: impl Into<String>, value: impl Into<String>) -> Self {
        self.revprop_table.insert(name.into(), value.into());
        self
    }

    /// Sets all revision properties.
    pub fn revprops(mut self, props: std::collections::HashMap<String, String>) -> Self {
        self.revprop_table = props;
        self
    }

    /// Executes the commit operation.
    ///
    /// # Parameters
    ///
    /// * `log_msg_func` - Optional callback to get the log message for the commit. If None,
    ///   the log message will be extracted from revprops["svn:log"] or default to empty.
    /// * `commit_callback` - Callback called after the commit completes
    pub fn execute(
        self,
        log_msg_func: Option<&mut dyn FnMut(&[CommitItem]) -> Result<String, Error<'static>>>,
        commit_callback: &mut dyn FnMut(&crate::CommitInfo) -> Result<(), Error<'static>>,
    ) -> Result<(), Error<'static>> {
        let targets_ref: Vec<&str> = self.targets.iter().map(|s| s.as_str()).collect();
        let revprop_ref: std::collections::HashMap<&str, &str> = self
            .revprop_table
            .iter()
            .map(|(k, v)| (k.as_str(), v.as_str()))
            .collect();

        let changelists_vec = self
            .changelists
            .as_ref()
            .map(|cl| cl.iter().map(|s| s.to_string()).collect::<Vec<_>>());
        let options = CommitOptions {
            depth: self.depth,
            keep_locks: self.keep_locks,
            keep_changelists: self.keep_changelists,
            commit_as_operations: self.commit_as_operations,
            include_file_externals: self.include_file_externals,
            include_dir_externals: self.include_dir_externals,
            changelists: changelists_vec,
        };

        self.ctx.commit(
            &targets_ref,
            &options,
            revprop_ref,
            log_msg_func,
            commit_callback,
        )
    }
}

/// Builder for log operations
/// Builder for retrieving log messages from the repository.
pub struct LogBuilder<'a> {
    ctx: &'a mut Context,
    targets: Vec<String>,
    peg_revision: Revision,
    revision_ranges: Vec<RevisionRange>,
    limit: i32,
    discover_changed_paths: bool,
    strict_node_history: bool,
    include_merged_revisions: bool,
    revprops: Option<Vec<String>>,
}

impl<'a> LogBuilder<'a> {
    /// Creates a new LogBuilder for retrieving log messages.
    pub fn new(ctx: &'a mut Context) -> Self {
        Self {
            ctx,
            targets: Vec::new(),
            peg_revision: Revision::Head,
            revision_ranges: vec![RevisionRange::new(
                Revision::Number(Revnum(1)),
                Revision::Head,
            )],
            limit: 0, // no limit
            discover_changed_paths: false,
            strict_node_history: true,
            include_merged_revisions: false,
            revprops: None,
        }
    }

    /// Adds a target path for the log.
    pub fn add_target(mut self, target: impl Into<String>) -> Self {
        self.targets.push(target.into());
        self
    }

    /// Sets the target paths for the log.
    pub fn targets(mut self, targets: Vec<String>) -> Self {
        self.targets = targets;
        self
    }

    /// Sets the peg revision.
    pub fn peg_revision(mut self, rev: Revision) -> Self {
        self.peg_revision = rev;
        self
    }

    /// Adds a revision range to retrieve.
    pub fn add_revision_range(mut self, start: Revision, end: Revision) -> Self {
        self.revision_ranges.push(RevisionRange::new(start, end));
        self
    }

    /// Sets the revision ranges to retrieve.
    pub fn revision_ranges(mut self, ranges: Vec<RevisionRange>) -> Self {
        self.revision_ranges = ranges;
        self
    }

    /// Sets the maximum number of log entries to retrieve.
    pub fn limit(mut self, limit: i32) -> Self {
        self.limit = limit;
        self
    }

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

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

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

    /// Adds a revision property to retrieve.
    pub fn add_revprop(mut self, prop: impl Into<String>) -> Self {
        if let Some(ref mut revprops) = self.revprops {
            revprops.push(prop.into());
        } else {
            self.revprops = Some(vec![prop.into()]);
        }
        self
    }

    /// Sets the revision properties to retrieve (None = all, Some(vec) = only specified).
    pub fn revprops(mut self, props: Option<Vec<String>>) -> Self {
        self.revprops = props;
        self
    }

    /// Executes the log operation.
    pub fn execute(
        self,
        log_entry_receiver: &dyn FnMut(&LogEntry) -> Result<(), Error<'static>>,
    ) -> Result<(), Error<'static>> {
        let targets_ref: Vec<&str> = self.targets.iter().map(|s| s.as_str()).collect();

        let mut options = LogOptions::new()
            .with_peg_revision(self.peg_revision)
            .with_discover_changed_paths(self.discover_changed_paths)
            .with_strict_node_history(self.strict_node_history)
            .with_include_merged_revisions(self.include_merged_revisions)
            .with_revprops(self.revprops);

        if self.limit != 0 {
            options = options.with_limit(self.limit);
        }

        self.ctx.log(
            &targets_ref,
            &self.revision_ranges,
            &options,
            log_entry_receiver,
        )
    }
}

/// Builder for update operations
/// Builder for updating working copy items to a different revision.
pub struct UpdateBuilder<'a> {
    ctx: &'a mut Context,
    paths: Vec<String>,
    revision: Revision,
    depth: Depth,
    depth_is_sticky: bool,
    ignore_externals: bool,
    allow_unver_obstructions: bool,
    adds_as_modifications: bool,
    make_parents: bool,
}

impl<'a> UpdateBuilder<'a> {
    /// Creates a new UpdateBuilder for updating working copies.
    pub fn new(ctx: &'a mut Context) -> Self {
        Self {
            ctx,
            paths: Vec::new(),
            revision: Revision::Head,
            depth: Depth::Infinity,
            depth_is_sticky: false,
            ignore_externals: false,
            allow_unver_obstructions: false,
            adds_as_modifications: false,
            make_parents: false,
        }
    }

    /// Adds a path to update.
    pub fn add_path(mut self, path: impl Into<String>) -> Self {
        self.paths.push(path.into());
        self
    }

    /// Sets the paths to update.
    pub fn paths(mut self, paths: Vec<String>) -> Self {
        self.paths = paths;
        self
    }

    /// Sets the revision to update to.
    pub fn revision(mut self, rev: Revision) -> Self {
        self.revision = rev;
        self
    }

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

    /// Sets whether the depth is sticky.
    pub fn depth_is_sticky(mut self, sticky: bool) -> Self {
        self.depth_is_sticky = sticky;
        self
    }

    /// Sets whether to ignore externals.
    pub fn ignore_externals(mut self, ignore: bool) -> Self {
        self.ignore_externals = ignore;
        self
    }

    /// Sets whether to allow unversioned obstructions.
    pub fn allow_unver_obstructions(mut self, allow: bool) -> Self {
        self.allow_unver_obstructions = allow;
        self
    }

    /// Sets whether to treat adds as modifications.
    pub fn adds_as_modification(mut self, as_mod: bool) -> Self {
        self.adds_as_modifications = as_mod;
        self
    }

    /// Sets whether to create parent directories.
    pub fn make_parents(mut self, make: bool) -> Self {
        self.make_parents = make;
        self
    }

    /// Executes the update operation.
    pub fn execute(self) -> Result<Vec<Revnum>, Error<'static>> {
        let paths_ref: Vec<&str> = self.paths.iter().map(|s| s.as_str()).collect();
        let options = UpdateOptions {
            depth: self.depth,
            depth_is_sticky: self.depth_is_sticky,
            ignore_externals: self.ignore_externals,
            allow_unver_obstructions: self.allow_unver_obstructions,
            adds_as_modifications: self.adds_as_modifications,
            make_parents: self.make_parents,
        };

        self.ctx.update(&paths_ref, self.revision, &options)
    }
}

/// Builder for mkdir operations
/// Builder for creating directories in the repository or working copy.
pub struct MkdirBuilder<'a> {
    ctx: &'a mut Context,
    paths: Vec<String>,
    make_parents: bool,
    revprop_table: std::collections::HashMap<String, Vec<u8>>,
}

impl<'a> MkdirBuilder<'a> {
    /// Creates a new MkdirBuilder for creating directories.
    pub fn new(ctx: &'a mut Context) -> Self {
        Self {
            ctx,
            paths: Vec::new(),
            make_parents: false,
            revprop_table: std::collections::HashMap::new(),
        }
    }

    /// Adds a directory path to create.
    pub fn add_path(mut self, path: impl Into<String>) -> Self {
        self.paths.push(path.into());
        self
    }

    /// Sets the directory paths to create.
    pub fn paths(mut self, paths: Vec<String>) -> Self {
        self.paths = paths;
        self
    }

    /// Sets whether to create parent directories.
    pub fn make_parents(mut self, make: bool) -> Self {
        self.make_parents = make;
        self
    }

    /// Adds a revision property.
    pub fn add_revprop(mut self, name: impl Into<String>, value: Vec<u8>) -> Self {
        self.revprop_table.insert(name.into(), value);
        self
    }

    /// Sets all revision properties.
    pub fn revprops(mut self, props: std::collections::HashMap<String, Vec<u8>>) -> Self {
        self.revprop_table = props;
        self
    }

    /// Executes the mkdir operation.
    pub fn execute(
        self,
        commit_callback: &mut dyn FnMut(&crate::CommitInfo) -> Result<(), Error<'static>>,
    ) -> Result<(), Error<'static>> {
        let paths_ref: Vec<&str> = self.paths.iter().map(|s| s.as_str()).collect();
        let revprop_table_owned: std::collections::HashMap<String, Vec<u8>> =
            self.revprop_table.into_iter().collect();

        let mut options = MkdirOptions::new()
            .with_make_parents(self.make_parents)
            .with_commit_callback(commit_callback);

        if !revprop_table_owned.is_empty() {
            options = options.with_revprop_table(revprop_table_owned);
        }

        self.ctx.mkdir(&paths_ref, &mut options)
    }
}

impl Context {
    /// Create a builder for diff operations
    pub fn diff_builder<'a>(
        &'a mut self,
        path1: impl Into<String>,
        revision1: Revision,
        path2: impl Into<String>,
        revision2: Revision,
    ) -> DiffBuilder<'a> {
        DiffBuilder::new(self, path1, revision1, path2, revision2)
    }

    /// Create a builder for list operations
    pub fn list_builder<'a>(&'a mut self, path_or_url: impl Into<String>) -> ListBuilder<'a> {
        ListBuilder::new(self, path_or_url)
    }

    /// Create a builder for copy operations
    pub fn copy_builder<'a>(&'a mut self, dst_path: impl Into<String>) -> CopyBuilder<'a> {
        CopyBuilder::new(self, dst_path)
    }

    /// Revert changes in the working copy
    ///
    /// This wraps svn_client_revert4 to revert local modifications.
    pub fn revert(
        &mut self,
        paths: &[&str],
        options: &RevertOptions,
    ) -> Result<(), Error<'static>> {
        let pool = Pool::new();

        // Convert paths to APR array
        let mut paths_array = apr::tables::TypedArray::<*const i8>::new(&pool, paths.len() as i32);
        let path_cstrings: Vec<_> = paths
            .iter()
            .map(|p| crate::dirent::canonicalize_path_or_url(p).unwrap())
            .collect();
        for cstring in &path_cstrings {
            paths_array.push(cstring.as_ptr());
        }

        // Convert changelists if provided
        let (changelists_array, list_cstrings) = if let Some(lists) = &options.changelists {
            let mut array = apr::tables::TypedArray::<*const i8>::new(&pool, lists.len() as i32);
            let cstrings: Vec<_> = lists
                .iter()
                .map(|l| std::ffi::CString::new(l.as_str()).unwrap())
                .collect();
            for cstring in &cstrings {
                array.push(cstring.as_ptr());
            }
            (unsafe { array.as_ptr() }, Some(cstrings))
        } else {
            (std::ptr::null(), None)
        };

        let err = unsafe {
            subversion_sys::svn_client_revert4(
                paths_array.as_ptr(),
                options.depth.into(),
                changelists_array,
                options.clear_changelists as i32,
                options.metadata_only as i32,
                options.added_keep_local as i32,
                self.ptr,
                pool.as_mut_ptr(),
            )
        };

        // Keep list_cstrings alive until after the call
        drop(list_cstrings);

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

    /// Mark conflicts as resolved
    ///
    /// This wraps svn_client_resolved to mark conflicts as resolved.
    pub fn resolved(
        &mut self,
        path: impl AsCanonicalDirent,
        recursive: bool,
    ) -> Result<(), Error<'static>> {
        let pool = Pool::new();
        let path = crate::dirent::to_absolute_cstring(path)?;

        let err = unsafe {
            subversion_sys::svn_client_resolved(
                path.as_ptr(),
                recursive as i32,
                self.ptr,
                pool.as_mut_ptr(),
            )
        };

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

    /// Add paths to a changelist
    ///
    /// This wraps svn_client_add_to_changelist to add paths to a changelist.
    pub fn add_to_changelist(
        &mut self,
        targets: &[&str],
        changelist: &str,
        depth: Depth,
        changelists: Option<&[&str]>,
    ) -> Result<(), Error<'static>> {
        let pool = Pool::new();
        let changelist = std::ffi::CString::new(changelist).unwrap();

        // Convert targets to APR array
        let mut targets_array =
            apr::tables::TypedArray::<*const i8>::new(&pool, targets.len() as i32);
        let target_cstrings: Vec<_> = targets
            .iter()
            .map(|t| crate::dirent::canonicalize_path_or_url(t).unwrap())
            .collect();
        for cstring in &target_cstrings {
            targets_array.push(cstring.as_ptr());
        }

        // Convert changelists if provided
        let changelists_array = if let Some(lists) = changelists {
            let mut array = apr::tables::TypedArray::<*const i8>::new(&pool, lists.len() as i32);
            let list_cstrings: Vec<_> = lists
                .iter()
                .map(|l| std::ffi::CString::new(*l).unwrap())
                .collect();
            for cstring in &list_cstrings {
                array.push(cstring.as_ptr());
            }
            unsafe { array.as_ptr() }
        } else {
            std::ptr::null()
        };

        let err = unsafe {
            subversion_sys::svn_client_add_to_changelist(
                targets_array.as_ptr(),
                changelist.as_ptr(),
                depth.into(),
                changelists_array,
                self.ptr,
                pool.as_mut_ptr(),
            )
        };

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

    /// Remove paths from changelists
    ///
    /// This wraps svn_client_remove_from_changelists to remove paths from changelists.
    pub fn remove_from_changelists(
        &mut self,
        targets: &[&str],
        depth: Depth,
        changelists: Option<&[&str]>,
    ) -> Result<(), Error<'static>> {
        let pool = Pool::new();

        // Convert targets to APR array
        let mut targets_array =
            apr::tables::TypedArray::<*const i8>::new(&pool, targets.len() as i32);
        let target_cstrings: Vec<_> = targets
            .iter()
            .map(|t| crate::dirent::canonicalize_path_or_url(t).unwrap())
            .collect();
        for cstring in &target_cstrings {
            targets_array.push(cstring.as_ptr());
        }

        // Convert changelists if provided
        let changelists_array = if let Some(lists) = changelists {
            let mut array = apr::tables::TypedArray::<*const i8>::new(&pool, lists.len() as i32);
            let list_cstrings: Vec<_> = lists
                .iter()
                .map(|l| std::ffi::CString::new(*l).unwrap())
                .collect();
            for cstring in &list_cstrings {
                array.push(cstring.as_ptr());
            }
            unsafe { array.as_ptr() }
        } else {
            std::ptr::null()
        };

        let err = unsafe {
            subversion_sys::svn_client_remove_from_changelists(
                targets_array.as_ptr(),
                depth.into(),
                changelists_array,
                self.ptr,
                pool.as_mut_ptr(),
            )
        };

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

    /// Get paths belonging to changelists
    ///
    /// Crawl the working copy starting at `path` to discover paths that belong
    /// to one of the specified changelists. If `changelists` is None, discover
    /// paths with any changelist. The callback is invoked for each path found.
    pub fn get_changelists(
        &mut self,
        path: &str,
        depth: Depth,
        changelists: Option<&[&str]>,
        receiver: &mut dyn FnMut(&str, &str) -> Result<(), Error<'static>>,
    ) -> Result<(), Error<'static>> {
        let pool = Pool::new();
        let path_cstr = crate::dirent::canonicalize_path_or_url(path).unwrap();

        // Convert changelists if provided - must keep cstrings alive until after the call
        let cstrings: Vec<_> = changelists
            .map(|lists| {
                lists
                    .iter()
                    .map(|l| std::ffi::CString::new(*l).unwrap())
                    .collect::<Vec<_>>()
            })
            .unwrap_or_default();

        let changelists_array = if changelists.is_some() {
            let mut array = apr::tables::TypedArray::<*const i8>::new(&pool, cstrings.len() as i32);
            for cstring in &cstrings {
                array.push(cstring.as_ptr());
            }
            unsafe { array.as_ptr() }
        } else {
            std::ptr::null()
        };

        let callback_baton = &receiver as *const _ as *mut std::ffi::c_void;

        let err = unsafe {
            subversion_sys::svn_client_get_changelists(
                path_cstr.as_ptr(),
                changelists_array,
                depth.into(),
                Some(wrap_changelist_receiver),
                callback_baton,
                self.ptr,
                pool.as_mut_ptr(),
            )
        };

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

    /// Merge changes between two sources
    ///
    /// This wraps svn_client_merge5 for merging between two sources.
    pub fn merge_sources(
        &mut self,
        source1: &str,
        revision1: &Revision,
        source2: &str,
        revision2: &Revision,
        target_wcpath: &str,
        depth: Depth,
        options: &MergeSourcesOptions,
    ) -> Result<(), Error<'static>> {
        let pool = Pool::new();
        let source1 = crate::dirent::canonicalize_path_or_url(source1).unwrap();
        let source2 = crate::dirent::canonicalize_path_or_url(source2).unwrap();
        let target_wcpath = crate::dirent::to_absolute_cstring(target_wcpath)?;

        // Convert merge_options to APR array if provided
        let merge_opts_array = options.merge_options.as_ref().map(|opts| {
            let mut array = apr::tables::TypedArray::<*const i8>::new(&pool, opts.len() as i32);
            let cstrings: Vec<_> = opts
                .iter()
                .map(|opt| std::ffi::CString::new(opt.as_str()).unwrap())
                .collect();
            for cstring in &cstrings {
                array.push(cstring.as_ptr());
            }
            (array, cstrings)
        });

        let merge_opts_ptr = merge_opts_array
            .as_ref()
            .map_or(std::ptr::null(), |(arr, _)| unsafe { arr.as_ptr() });

        let err = unsafe {
            subversion_sys::svn_client_merge5(
                source1.as_ptr(),
                &(*revision1).into(),
                source2.as_ptr(),
                &(*revision2).into(),
                target_wcpath.as_ptr(),
                depth.into(),
                options.ignore_mergeinfo as i32,
                options.diff_ignore_ancestry as i32,
                options.force_delete as i32,
                options.record_only as i32,
                options.dry_run as i32,
                options.allow_mixed_rev as i32,
                merge_opts_ptr,
                self.ptr,
                pool.as_mut_ptr(),
            )
        };

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

    /// Merge changes from a source into a working copy
    ///
    /// This wraps svn_client_merge_peg5 for the common case of merging from a branch.
    /// Use this for typical merge operations like merging a branch into trunk.
    ///
    /// # Arguments
    /// * `source` - URL or path to merge from
    /// * `ranges_to_merge` - Revision ranges to merge (empty for automatic merge)
    /// * `peg_revision` - Peg revision for the source
    /// * `target_wcpath` - Working copy path to merge into
    /// * `depth` - Depth for the merge operation
    /// * `options` - Merge options
    pub fn merge_peg(
        &mut self,
        source: &str,
        ranges_to_merge: &[crate::mergeinfo::MergeRange],
        peg_revision: &Revision,
        target_wcpath: &str,
        depth: Depth,
        options: &MergeSourcesOptions,
    ) -> Result<(), Error<'static>> {
        let pool = Pool::new();
        let source = crate::dirent::canonicalize_path_or_url(source)?;
        let target_wcpath = crate::dirent::to_absolute_cstring(target_wcpath)?;

        // Convert revision ranges to APR array
        let ranges_array = if ranges_to_merge.is_empty() {
            std::ptr::null()
        } else {
            unsafe {
                let mut array =
                    apr::tables::TypedArray::<*mut subversion_sys::svn_merge_range_t>::new(
                        &pool,
                        ranges_to_merge.len() as i32,
                    );
                for range in ranges_to_merge {
                    let range_ptr: *mut subversion_sys::svn_merge_range_t = pool.calloc();
                    (*range_ptr).start = range.start.0;
                    (*range_ptr).end = range.end.0;
                    (*range_ptr).inheritable = range.inheritable as i32;
                    array.push(range_ptr);
                }
                array.as_ptr()
            }
        };

        // Convert merge_options to APR array if provided
        let merge_opts_array = options.merge_options.as_ref().map(|opts| {
            let mut array = apr::tables::TypedArray::<*const i8>::new(&pool, opts.len() as i32);
            let cstrings: Vec<_> = opts
                .iter()
                .map(|opt| std::ffi::CString::new(opt.as_str()).unwrap())
                .collect();
            for cstring in &cstrings {
                array.push(cstring.as_ptr());
            }
            (array, cstrings)
        });

        let merge_opts_ptr = merge_opts_array
            .as_ref()
            .map_or(std::ptr::null(), |(arr, _)| unsafe { arr.as_ptr() });

        let peg_rev_c: subversion_sys::svn_opt_revision_t = (*peg_revision).into();
        let err = unsafe {
            subversion_sys::svn_client_merge_peg5(
                source.as_ptr(),
                ranges_array,
                &peg_rev_c,
                target_wcpath.as_ptr(),
                depth.into(),
                options.ignore_mergeinfo as i32,
                options.diff_ignore_ancestry as i32,
                options.force_delete as i32,
                options.record_only as i32,
                options.dry_run as i32,
                options.allow_mixed_rev as i32,
                merge_opts_ptr,
                self.ptr,
                pool.as_mut_ptr(),
            )
        };

        svn_result(err)
    }

    /// Merge changes between two sources into a working copy path.
    ///
    /// Merges the differences between `source1` at `revision1` and `source2` at
    /// `revision2` into the working copy path `target_wcpath`.
    ///
    /// This is useful for cherry-picking specific changes or merging between
    /// different branches without using peg revisions.
    ///
    /// # Arguments
    /// * `source1` - First source URL or working copy path
    /// * `revision1` - Revision for first source
    /// * `source2` - Second source URL or working copy path
    /// * `revision2` - Revision for second source
    /// * `target_wcpath` - Target working copy path
    /// * `depth` - How deep to merge
    /// * `options` - Merge options controlling the merge behavior
    ///
    /// This wraps `svn_client_merge5`.
    pub fn merge(
        &mut self,
        source1: &str,
        revision1: &Revision,
        source2: &str,
        revision2: &Revision,
        target_wcpath: &str,
        depth: Depth,
        options: &MergeSourcesOptions,
    ) -> Result<(), Error<'static>> {
        let pool = Pool::new();
        let source1 = crate::dirent::canonicalize_path_or_url(source1)?;
        let source2 = crate::dirent::canonicalize_path_or_url(source2)?;
        let target_wcpath = crate::dirent::to_absolute_cstring(target_wcpath)?;

        // Convert merge_options to APR array if provided
        let merge_opts_array = options.merge_options.as_ref().map(|opts| {
            let mut array = apr::tables::TypedArray::<*const i8>::new(&pool, opts.len() as i32);
            let cstrings: Vec<_> = opts
                .iter()
                .map(|opt| std::ffi::CString::new(opt.as_str()).unwrap())
                .collect();
            for cstring in &cstrings {
                array.push(cstring.as_ptr());
            }
            (array, cstrings)
        });

        let merge_opts_ptr = merge_opts_array
            .as_ref()
            .map_or(std::ptr::null(), |(arr, _)| unsafe { arr.as_ptr() });

        let rev1: subversion_sys::svn_opt_revision_t = (*revision1).into();
        let rev2: subversion_sys::svn_opt_revision_t = (*revision2).into();

        let err = unsafe {
            subversion_sys::svn_client_merge5(
                source1.as_ptr(),
                &rev1,
                source2.as_ptr(),
                &rev2,
                target_wcpath.as_ptr(),
                depth.into(),
                options.ignore_mergeinfo as i32,
                options.diff_ignore_ancestry as i32,
                options.force_delete as i32,
                options.record_only as i32,
                options.dry_run as i32,
                options.allow_mixed_rev as i32,
                merge_opts_ptr,
                self.ptr,
                pool.as_mut_ptr(),
            )
        };

        svn_result(err)
    }

    /// Get information about the state of merging between two branches.
    ///
    /// Returns information about the merge relationship between
    /// source and target, including whether a reintegration merge is needed.
    ///
    /// # Arguments
    /// * `source_path_or_url` - Source path or URL
    /// * `source_revision` - Source revision
    /// * `target_path_or_url` - Target path or URL (WC or repository)
    /// * `target_revision` - Target revision
    ///
    /// # Returns
    /// A tuple containing:
    /// - needs_reintegration: true if next merge should be a reintegration
    /// - yca_url/yca_rev: Youngest common ancestor location
    /// - base_url/base_rev: Base chosen for 3-way merge
    /// - right_url/right_rev: Right-hand side of source diff
    /// - target_url/target_rev: Target location
    /// - repos_root_url: Repository root URL
    ///
    /// This wraps `svn_client_get_merging_summary`.
    ///
    /// # Example
    /// ```no_run
    /// # use subversion::client::Context;
    /// # use subversion::Revision;
    /// # let mut ctx = Context::new().unwrap();
    /// let summary = ctx.get_merging_summary(
    ///     "http://svn.example.com/repos/trunk",
    ///     &Revision::Head,
    ///     "/path/to/wc",
    ///     &Revision::Working,
    /// ).unwrap();
    /// if summary.0 {
    ///     println!("Reintegration merge needed!");
    /// }
    /// ```
    pub fn get_merging_summary(
        &mut self,
        source_path_or_url: &str,
        source_revision: &Revision,
        target_path_or_url: &str,
        target_revision: &Revision,
    ) -> Result<
        (
            bool,
            String,
            i64,
            String,
            i64,
            String,
            i64,
            String,
            i64,
            String,
        ),
        Error<'_>,
    > {
        let result_pool = apr::Pool::new();
        let scratch_pool = apr::Pool::new();

        let source_c = crate::dirent::canonicalize_path_or_url(source_path_or_url)?;
        let target_c = crate::dirent::canonicalize_path_or_url(target_path_or_url)?;

        let source_rev: subversion_sys::svn_opt_revision_t = (*source_revision).into();
        let target_rev: subversion_sys::svn_opt_revision_t = (*target_revision).into();

        let mut needs_reintegration: i32 = 0;
        let mut yca_url: *const i8 = std::ptr::null();
        let mut yca_rev: subversion_sys::svn_revnum_t = 0;
        let mut base_url: *const i8 = std::ptr::null();
        let mut base_rev: subversion_sys::svn_revnum_t = 0;
        let mut right_url: *const i8 = std::ptr::null();
        let mut right_rev: subversion_sys::svn_revnum_t = 0;
        let mut target_url: *const i8 = std::ptr::null();
        let mut target_rev_out: subversion_sys::svn_revnum_t = 0;
        let mut repos_root_url: *const i8 = std::ptr::null();

        let err = unsafe {
            subversion_sys::svn_client_get_merging_summary(
                &mut needs_reintegration,
                &mut yca_url,
                &mut yca_rev,
                &mut base_url,
                &mut base_rev,
                &mut right_url,
                &mut right_rev,
                &mut target_url,
                &mut target_rev_out,
                &mut repos_root_url,
                source_c.as_ptr(),
                &source_rev,
                target_c.as_ptr(),
                &target_rev,
                self.ptr,
                result_pool.as_mut_ptr(),
                scratch_pool.as_mut_ptr(),
            )
        };

        Error::from_raw(err)?;

        unsafe {
            let yca_url_str = if yca_url.is_null() {
                String::new()
            } else {
                std::ffi::CStr::from_ptr(yca_url)
                    .to_string_lossy()
                    .into_owned()
            };

            let base_url_str = if base_url.is_null() {
                String::new()
            } else {
                std::ffi::CStr::from_ptr(base_url)
                    .to_string_lossy()
                    .into_owned()
            };

            let right_url_str = if right_url.is_null() {
                String::new()
            } else {
                std::ffi::CStr::from_ptr(right_url)
                    .to_string_lossy()
                    .into_owned()
            };

            let target_url_str = if target_url.is_null() {
                String::new()
            } else {
                std::ffi::CStr::from_ptr(target_url)
                    .to_string_lossy()
                    .into_owned()
            };

            let repos_root_url_str = if repos_root_url.is_null() {
                String::new()
            } else {
                std::ffi::CStr::from_ptr(repos_root_url)
                    .to_string_lossy()
                    .into_owned()
            };

            Ok((
                needs_reintegration != 0,
                yca_url_str,
                yca_rev.into(),
                base_url_str,
                base_rev.into(),
                right_url_str,
                right_rev.into(),
                target_url_str,
                target_rev_out.into(),
                repos_root_url_str,
            ))
        }
    }

    /// Move (rename) a file or directory
    ///
    /// This wraps svn_client_move7 to perform a versioned move operation.
    pub fn move_path(
        &mut self,
        src_paths: &[&str],
        dst_path: &str,
        options: &mut MoveOptions,
    ) -> Result<(), Error<'static>> {
        let pool = Pool::new();

        // Convert source paths to APR array
        let mut src_paths_array =
            apr::tables::TypedArray::<*const i8>::new(&pool, src_paths.len() as i32);
        let src_cstrings: Vec<_> = src_paths
            .iter()
            .map(|p| crate::dirent::canonicalize_path_or_url(p).unwrap())
            .collect();
        for cstring in &src_cstrings {
            src_paths_array.push(cstring.as_ptr());
        }

        let dst_path = crate::dirent::canonicalize_path_or_url(dst_path)?;

        // Convert revprop table if provided
        let mut revprop_hash = std::ptr::null_mut();
        if let Some(ref revprops) = options.revprop_table {
            let mut hash = apr::hash::Hash::new(&pool);
            for (key, value) in revprops {
                let key_cstring = std::ffi::CString::new(key.as_str()).unwrap();
                let svn_string = crate::string::BStr::from_bytes(value, &pool);
                unsafe {
                    hash.insert(
                        key_cstring.as_bytes(),
                        svn_string.as_ptr() as *mut std::ffi::c_void,
                    );
                }
            }
            unsafe {
                revprop_hash = hash.as_mut_ptr();
            }
        }

        // Handle commit callback
        let (callback_func, callback_baton) = if let Some(ref mut cb) = options.commit_callback {
            (
                Some(crate::wrap_commit_callback2 as _),
                cb as *mut _ as *mut std::ffi::c_void,
            )
        } else {
            (None, std::ptr::null_mut())
        };

        let err = unsafe {
            subversion_sys::svn_client_move7(
                src_paths_array.as_ptr(),
                dst_path.as_ptr(),
                options.move_as_child as i32,
                options.make_parents as i32,
                options.allow_mixed_revisions as i32,
                options.metadata_only as i32,
                revprop_hash,
                callback_func,
                callback_baton,
                self.ptr,
                pool.as_mut_ptr(),
            )
        };

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

    /// Create a builder for info operations
    pub fn info_builder<'a>(&'a mut self, abspath_or_url: impl Into<String>) -> InfoBuilder<'a> {
        InfoBuilder::new(self, abspath_or_url)
    }

    /// Create a builder for commit operations
    pub fn commit_builder<'a>(&'a mut self) -> CommitBuilder<'a> {
        CommitBuilder::new(self)
    }

    /// Create a builder for log operations
    pub fn log_builder<'a>(&'a mut self) -> LogBuilder<'a> {
        LogBuilder::new(self)
    }

    /// Create a builder for update operations
    pub fn update_builder<'a>(&'a mut self) -> UpdateBuilder<'a> {
        UpdateBuilder::new(self)
    }

    /// Create a builder for mkdir operations
    pub fn mkdir_builder<'a>(&'a mut self) -> MkdirBuilder<'a> {
        MkdirBuilder::new(self)
    }

    /// Apply a patch to a working copy
    ///
    /// Applies a unified diff patch from `patch_path` to the working copy at `wc_dir_path`.
    pub fn patch(
        &mut self,
        patch_path: &std::path::Path,
        wc_dir_path: &std::path::Path,
        options: &mut PatchOptions,
    ) -> Result<(), Error<'static>> {
        let pool = apr::Pool::new();
        let patch_path_cstr = crate::dirent::to_absolute_cstring(patch_path)?;
        let wc_dir_path_cstr = crate::dirent::to_absolute_cstring(wc_dir_path)?;

        // Create C-compatible callback wrapper for patch_func
        extern "C" fn c_patch_callback(
            baton: *mut std::ffi::c_void,
            filtered: *mut i32,
            canon_path: *const i8,
            patch_abspath: *const i8,
            reject_abspath: *const i8,
            _pool: *mut apr_sys::apr_pool_t,
        ) -> *mut subversion_sys::svn_error_t {
            unsafe {
                let callback = &mut *(baton
                    as *mut &mut dyn FnMut(
                        &mut bool,
                        &str,
                        &std::path::Path,
                        &std::path::Path,
                    ) -> Result<(), Error<'static>>);

                let canon_path_str = std::ffi::CStr::from_ptr(canon_path).to_str().unwrap_or("");
                let patch_path = std::path::Path::new(
                    std::ffi::CStr::from_ptr(patch_abspath)
                        .to_str()
                        .unwrap_or(""),
                );
                let reject_path = std::path::Path::new(
                    std::ffi::CStr::from_ptr(reject_abspath)
                        .to_str()
                        .unwrap_or(""),
                );

                let mut filtered_bool = *filtered != 0;
                match callback(&mut filtered_bool, canon_path_str, patch_path, reject_path) {
                    Ok(()) => {
                        *filtered = filtered_bool as i32;
                        std::ptr::null_mut()
                    }
                    Err(err) => err.into_raw(),
                }
            }
        }

        let (callback_func, callback_baton) = if let Some(ref mut cb) = options.patch_func {
            (
                Some(c_patch_callback as _),
                cb as *mut _ as *mut std::ffi::c_void,
            )
        } else {
            (None, std::ptr::null_mut())
        };

        unsafe {
            let err = subversion_sys::svn_client_patch(
                patch_path_cstr.as_ptr(),
                wc_dir_path_cstr.as_ptr(),
                options.dry_run as i32,
                options.strip_count,
                options.reverse as i32,
                options.ignore_whitespace as i32,
                options.remove_tempfiles as i32,
                callback_func,
                callback_baton,
                self.as_mut_ptr(),
                pool.as_mut_ptr(),
            );
            Error::from_raw(err)
        }
    }

    /// Perform a reintegrate merge
    ///
    /// Merges all eligible revisions from the source URL to the working copy.
    pub fn merge_reintegrate(
        &mut self,
        source_url: &str,
        source_peg_revision: &Revision,
        target_wcpath: &std::path::Path,
        dry_run: bool,
        merge_options: &[&str],
    ) -> Result<(), Error<'static>> {
        let pool = apr::Pool::new();
        let source_url_cstr = std::ffi::CString::new(source_url)?;
        let target_wcpath_cstr = crate::dirent::to_absolute_cstring(target_wcpath)?;

        // Convert merge options to APR array
        let merge_options_array = unsafe {
            let array = apr_sys::apr_array_make(
                pool.as_mut_ptr(),
                merge_options.len() as i32,
                std::mem::size_of::<*const std::os::raw::c_char>() as i32,
            );

            for option in merge_options {
                let option_ptr = pool.pstrdup(option);
                let slot = apr_sys::apr_array_push(array) as *mut *const std::os::raw::c_char;
                *slot = option_ptr;
            }

            array
        };

        unsafe {
            let err = subversion_sys::svn_client_merge_reintegrate(
                source_url_cstr.as_ptr(),
                &(*source_peg_revision).into(),
                target_wcpath_cstr.as_ptr(),
                dry_run as i32,
                merge_options_array,
                self.as_mut_ptr(),
                pool.as_mut_ptr(),
            );
            Error::from_raw(err)
        }
    }

    /// Get the UUID of a repository from a URL
    /// Returns the UUID string identifying the repository
    pub fn uuid_from_url(&mut self, url: &str) -> Result<String, Error<'static>> {
        let url_cstr = std::ffi::CString::new(url)?;
        let pool = apr::Pool::new();
        let mut uuid_ptr = std::ptr::null();

        let ret = unsafe {
            subversion_sys::svn_client_uuid_from_url(
                &mut uuid_ptr,
                url_cstr.as_ptr(),
                self.ptr,
                pool.as_mut_ptr(),
            )
        };

        Error::from_raw(ret)?;

        if uuid_ptr.is_null() {
            return Err(Error::from_message("Failed to get repository UUID"));
        }

        let uuid_str = unsafe {
            std::ffi::CStr::from_ptr(uuid_ptr)
                .to_string_lossy()
                .into_owned()
        };

        Ok(uuid_str)
    }

    /// Get the UUID of a repository from a working copy path
    /// Returns the UUID string of the repository the working copy is connected to
    pub fn uuid_from_path(&mut self, path: &std::path::Path) -> Result<String, Error<'static>> {
        let path_cstr = crate::dirent::to_absolute_cstring(path)?;
        let pool = apr::Pool::new();
        let mut uuid_ptr = std::ptr::null();

        let ret = unsafe {
            subversion_sys::svn_client_uuid_from_path2(
                &mut uuid_ptr,
                path_cstr.as_ptr(),
                self.ptr,
                pool.as_mut_ptr(),
                pool.as_mut_ptr(), // scratch pool
            )
        };

        Error::from_raw(ret)?;

        if uuid_ptr.is_null() {
            return Err(Error::from_message("Failed to get repository UUID"));
        }

        let uuid_str = unsafe {
            std::ffi::CStr::from_ptr(uuid_ptr)
                .to_string_lossy()
                .into_owned()
        };

        Ok(uuid_str)
    }

    /// Relocate a working copy to a new repository URL
    ///
    /// Updates all references from `from_prefix` to `to_prefix` in the working copy.
    pub fn relocate(
        &mut self,
        wcroot_path: &std::path::Path,
        from_prefix: &str,
        to_prefix: &str,
        ignore_externals: bool,
    ) -> Result<(), Error<'static>> {
        let pool = apr::Pool::new();
        let wcroot_path_cstr = crate::dirent::to_absolute_cstring(wcroot_path)?;
        let from_prefix_cstr = std::ffi::CString::new(from_prefix)?;
        let to_prefix_cstr = std::ffi::CString::new(to_prefix)?;

        unsafe {
            let err = subversion_sys::svn_client_relocate2(
                wcroot_path_cstr.as_ptr(),
                from_prefix_cstr.as_ptr(),
                to_prefix_cstr.as_ptr(),
                ignore_externals as i32,
                self.as_mut_ptr(),
                pool.as_mut_ptr(),
            );
            Error::from_raw(err)
        }
    }

    /// Set a revision property on a repository
    ///
    /// # Errors
    ///
    /// Returns an error if:
    /// - The property name is invalid
    /// - The URL is invalid
    /// - The repository operation fails
    pub fn revprop_set(
        &mut self,
        propname: &str,
        propval: Option<&[u8]>,
        url: &str,
        options: &RevpropSetOptions,
    ) -> Result<(), Error<'static>> {
        // Validate property name
        if !crate::props::name_is_valid(propname) {
            return Err(Error::from_message(&format!(
                "Invalid property name: '{}'",
                propname
            )));
        }

        let pool = apr::Pool::new();
        let propname_cstr = std::ffi::CString::new(propname)?;
        let url_cstr = std::ffi::CString::new(url)?;

        let propval_svn = if let Some(val) = propval {
            unsafe {
                let svn_str = apr_sys::apr_palloc(
                    pool.as_mut_ptr(),
                    std::mem::size_of::<subversion_sys::svn_string_t>(),
                ) as *mut subversion_sys::svn_string_t;
                (*svn_str).data = val.as_ptr() as *const std::os::raw::c_char;
                (*svn_str).len = val.len();
                svn_str
            }
        } else {
            std::ptr::null_mut()
        };

        let original_propval_svn = if let Some(ref val) = options.original_propval {
            unsafe {
                let svn_str = apr_sys::apr_palloc(
                    pool.as_mut_ptr(),
                    std::mem::size_of::<subversion_sys::svn_string_t>(),
                ) as *mut subversion_sys::svn_string_t;
                (*svn_str).data = val.as_ptr() as *const std::os::raw::c_char;
                (*svn_str).len = val.len();
                svn_str
            }
        } else {
            std::ptr::null_mut()
        };

        unsafe {
            let err = subversion_sys::svn_client_revprop_set2(
                propname_cstr.as_ptr(),
                propval_svn,
                original_propval_svn,
                url_cstr.as_ptr(),
                &options.revision.into(),
                std::ptr::null_mut(), // base_revision_for_url - not used
                options.force as i32,
                self.as_mut_ptr(),
                pool.as_mut_ptr(),
            );
            Error::from_raw(err)
        }
    }

    /// Get a revision property from a repository
    ///
    /// # Arguments
    ///
    /// * `propname` - The name of the property to retrieve
    /// * `url` - The repository URL
    /// * `revision` - The revision to get the property from
    ///
    /// # Returns
    ///
    /// Returns `Ok(Some(value))` if the property exists, `Ok(None)` if it doesn't exist,
    /// or an error if the operation fails.
    ///
    /// # Errors
    ///
    /// Returns an error if:
    /// - The property name is invalid
    /// - The URL is invalid
    /// - The repository operation fails
    pub fn revprop_get(
        &mut self,
        propname: &str,
        url: &str,
        revision: &Revision,
    ) -> Result<Option<Vec<u8>>, Error<'_>> {
        // Validate property name
        if !crate::props::name_is_valid(propname) {
            return Err(Error::from_message(&format!(
                "Invalid property name: '{}'",
                propname
            )));
        }

        let pool = apr::Pool::new();
        let propname_cstr = std::ffi::CString::new(propname)?;
        let url_cstr = std::ffi::CString::new(url)?;
        let mut propval: *mut subversion_sys::svn_string_t = std::ptr::null_mut();
        let mut actual_rev = 0;

        unsafe {
            let err = subversion_sys::svn_client_revprop_get(
                propname_cstr.as_ptr(),
                &mut propval,
                url_cstr.as_ptr(),
                &(*revision).into(),
                &mut actual_rev,
                self.as_mut_ptr(),
                pool.as_mut_ptr(),
            );
            Error::from_raw(err)?;

            if propval.is_null() {
                Ok(None)
            } else {
                let len = (*propval).len;
                let data = (*propval).data as *const u8;
                Ok(Some(std::slice::from_raw_parts(data, len).to_vec()))
            }
        }
    }

    /// List revision properties on a repository
    ///
    /// # Arguments
    ///
    /// * `url` - The repository URL
    /// * `revision` - The revision to list properties from
    ///
    /// # Returns
    ///
    /// Returns a `HashMap` mapping property names to their values.
    ///
    /// # Errors
    ///
    /// Returns an error if:
    /// - The URL is invalid
    /// - The repository operation fails
    pub fn revprop_list(
        &mut self,
        url: &str,
        revision: &Revision,
    ) -> Result<std::collections::HashMap<String, Vec<u8>>, Error<'_>> {
        let pool = apr::Pool::new();
        let url_cstr = std::ffi::CString::new(url)?;
        let mut props_hash = std::ptr::null_mut();
        let mut actual_rev = 0;

        unsafe {
            let err = subversion_sys::svn_client_revprop_list(
                &mut props_hash,
                url_cstr.as_ptr(),
                &(*revision).into(),
                &mut actual_rev,
                self.as_mut_ptr(),
                pool.as_mut_ptr(),
            );
            Error::from_raw(err)?;

            let result = if !props_hash.is_null() {
                let prop_hash = crate::props::PropHash::from_ptr(props_hash);
                prop_hash.to_hashmap()
            } else {
                std::collections::HashMap::new()
            };

            Ok(result)
        }
    }

    /// Suggest possible merge sources for a repository path
    pub fn suggest_merge_sources(
        &mut self,
        path_or_url: &str,
        peg_revision: &Revision,
    ) -> Result<Vec<String>, Error<'_>> {
        let pool = apr::Pool::new();
        let path_or_url_cstr = crate::dirent::canonicalize_path_or_url(path_or_url)?;
        let mut sources_array = std::ptr::null_mut();

        unsafe {
            let err = subversion_sys::svn_client_suggest_merge_sources(
                &mut sources_array,
                path_or_url_cstr.as_ptr(),
                &(*peg_revision).into(),
                self.as_mut_ptr(),
                pool.as_mut_ptr(),
            );
            Error::from_raw(err)?;

            let mut result = Vec::new();
            if !sources_array.is_null() {
                let array_len = (*sources_array).nelts as usize;
                let array_data = (*sources_array).elts as *const *const std::os::raw::c_char;

                for i in 0..array_len {
                    let source_ptr = *array_data.add(i);
                    if !source_ptr.is_null() {
                        let source_str = std::ffi::CStr::from_ptr(source_ptr);
                        result.push(source_str.to_string_lossy().into_owned());
                    }
                }
            }

            Ok(result)
        }
    }

    /// Upgrade a working copy to a newer format
    pub fn upgrade(&mut self, wcroot_path: &std::path::Path) -> Result<(), Error<'static>> {
        let pool = apr::Pool::new();
        let wcroot_path_cstr = crate::dirent::to_absolute_cstring(wcroot_path)?;

        unsafe {
            let err = subversion_sys::svn_client_upgrade(
                wcroot_path_cstr.as_ptr(),
                self.as_mut_ptr(),
                pool.as_mut_ptr(),
            );
            Error::from_raw(err)
        }
    }
}

/// Status information for a working copy item.
pub struct Status {
    ptr: *const subversion_sys::svn_client_status_t,
    _pool: apr::PoolHandle<'static>,
}

impl Status {
    /// Returns the node kind (file, directory, etc.).
    pub fn kind(&self) -> crate::NodeKind {
        unsafe { (*self.ptr).kind.into() }
    }

    /// Returns the local absolute path of the item.
    pub fn local_abspath(&self) -> &str {
        unsafe {
            std::ffi::CStr::from_ptr((*self.ptr).local_abspath)
                .to_str()
                .unwrap()
        }
    }

    /// Returns the local absolute path in the native OS style.
    ///
    /// On Unix this is the same as [`local_abspath()`](Self::local_abspath).
    /// On Windows, SVN stores paths with forward slashes internally;
    /// this method converts them to native backslash style.
    pub fn local_abspath_native(&self) -> std::path::PathBuf {
        unsafe {
            let pool = apr::Pool::new();
            let local = subversion_sys::svn_dirent_local_style(
                (*self.ptr).local_abspath,
                pool.as_mut_ptr(),
            );
            std::path::PathBuf::from(std::ffi::CStr::from_ptr(local).to_str().unwrap())
        }
    }

    /// Returns the file size.
    pub fn filesize(&self) -> i64 {
        unsafe { (*self.ptr).filesize }
    }

    /// Returns whether the item is versioned.
    pub fn versioned(&self) -> bool {
        unsafe { (*self.ptr).versioned != 0 }
    }

    /// Returns whether the item is conflicted.
    pub fn conflicted(&self) -> bool {
        unsafe { (*self.ptr).conflicted != 0 }
    }

    /// Returns the node status.
    pub fn node_status(&self) -> crate::StatusKind {
        unsafe { (*self.ptr).node_status.into() }
    }

    /// Returns the text status.
    pub fn text_status(&self) -> crate::StatusKind {
        unsafe { (*self.ptr).text_status.into() }
    }

    /// Returns the property status.
    pub fn prop_status(&self) -> crate::StatusKind {
        unsafe { (*self.ptr).prop_status.into() }
    }

    /// Returns whether the working copy is locked.
    pub fn wc_is_locked(&self) -> bool {
        unsafe { (*self.ptr).wc_is_locked != 0 }
    }

    /// Returns whether the item was copied.
    pub fn copied(&self) -> bool {
        unsafe { (*self.ptr).copied != 0 }
    }

    /// Returns the repository root URL.
    pub fn repos_root_url(&self) -> &str {
        unsafe {
            std::ffi::CStr::from_ptr((*self.ptr).repos_root_url)
                .to_str()
                .unwrap()
        }
    }

    /// Returns the repository UUID.
    pub fn repos_uuid(&self) -> &str {
        unsafe {
            std::ffi::CStr::from_ptr((*self.ptr).repos_uuid)
                .to_str()
                .unwrap()
        }
    }

    /// Returns the repository relative path.
    pub fn repos_relpath(&self) -> &str {
        unsafe {
            std::ffi::CStr::from_ptr((*self.ptr).repos_relpath)
                .to_str()
                .unwrap()
        }
    }

    /// Returns the revision number.
    pub fn revision(&self) -> Revnum {
        Revnum::from_raw(unsafe { (*self.ptr).revision }).unwrap()
    }

    /// Returns the last changed revision.
    pub fn changed_rev(&self) -> Revnum {
        Revnum::from_raw(unsafe { (*self.ptr).changed_rev }).unwrap()
    }

    /// Returns the last changed date.
    pub fn changed_date(&self) -> apr::time::Time {
        unsafe { apr::time::Time::from((*self.ptr).changed_date) }
    }

    /// Returns the last changed author.
    pub fn changed_author(&self) -> &str {
        unsafe {
            std::ffi::CStr::from_ptr((*self.ptr).changed_author)
                .to_str()
                .unwrap()
        }
    }

    /// Returns whether the item is switched.
    pub fn switched(&self) -> bool {
        unsafe { (*self.ptr).switched != 0 }
    }

    /// Returns whether the item is a file external.
    pub fn file_external(&self) -> bool {
        unsafe { (*self.ptr).file_external != 0 }
    }

    /// Returns the lock information if the item is locked.
    pub fn lock(&self) -> Option<crate::Lock<'_>> {
        let lock_ptr = unsafe { (*self.ptr).lock };
        if lock_ptr.is_null() {
            None
        } else {
            let pool_handle =
                unsafe { apr::PoolHandle::from_borrowed_raw(self._pool.as_mut_ptr()) };
            Some(crate::Lock::from_raw(lock_ptr, pool_handle))
        }
    }

    /// Returns the changelist name if the item is in a changelist.
    pub fn changelist(&self) -> Option<&str> {
        unsafe {
            if (*self.ptr).changelist.is_null() {
                None
            } else {
                Some(
                    std::ffi::CStr::from_ptr((*self.ptr).changelist)
                        .to_str()
                        .unwrap(),
                )
            }
        }
    }

    /// Returns the depth of the item.
    pub fn depth(&self) -> crate::Depth {
        unsafe { (*self.ptr).depth.into() }
    }

    /// Returns the out-of-date node kind.
    pub fn ood_kind(&self) -> crate::NodeKind {
        unsafe { (*self.ptr).ood_kind.into() }
    }

    /// Returns the repository node status.
    pub fn repos_node_status(&self) -> crate::StatusKind {
        unsafe { (*self.ptr).repos_node_status.into() }
    }

    /// Returns the repository text status.
    pub fn repos_text_status(&self) -> crate::StatusKind {
        unsafe { (*self.ptr).repos_text_status.into() }
    }

    /// Returns the repository property status.
    pub fn repos_prop_status(&self) -> crate::StatusKind {
        unsafe { (*self.ptr).repos_prop_status.into() }
    }

    /// Returns the repository lock information.
    pub fn repos_lock(&self) -> Option<crate::Lock<'_>> {
        let lock_ptr = unsafe { (*self.ptr).repos_lock };
        if lock_ptr.is_null() {
            None
        } else {
            let pool_handle =
                unsafe { apr::PoolHandle::from_borrowed_raw(self._pool.as_mut_ptr()) };
            Some(crate::Lock::from_raw(lock_ptr, pool_handle))
        }
    }

    /// Returns the out-of-date changed revision.
    pub fn ood_changed_rev(&self) -> Option<Revnum> {
        Revnum::from_raw(unsafe { (*self.ptr).ood_changed_rev })
    }

    /// Returns the out-of-date changed author.
    pub fn ood_changed_author(&self) -> Option<&str> {
        unsafe {
            if (*self.ptr).ood_changed_author.is_null() {
                None
            } else {
                Some(
                    std::ffi::CStr::from_ptr((*self.ptr).ood_changed_author)
                        .to_str()
                        .unwrap(),
                )
            }
        }
    }

    /// Returns the out-of-date changed date.
    pub fn ood_changed_date(&self) -> apr::time::Time {
        unsafe { apr::time::Time::from((*self.ptr).ood_changed_date) }
    }

    /// Returns the absolute path the item was moved from.
    pub fn moved_from_abspath(&self) -> Option<&str> {
        unsafe {
            if (*self.ptr).moved_from_abspath.is_null() {
                None
            } else {
                Some(
                    std::ffi::CStr::from_ptr((*self.ptr).moved_from_abspath)
                        .to_str()
                        .unwrap(),
                )
            }
        }
    }

    /// Returns the absolute path the item was moved to.
    pub fn moved_to_abspath(&self) -> Option<&str> {
        unsafe {
            if (*self.ptr).moved_to_abspath.is_null() {
                None
            } else {
                Some(
                    std::ffi::CStr::from_ptr((*self.ptr).moved_to_abspath)
                        .to_str()
                        .unwrap(),
                )
            }
        }
    }
}

/// Conflict handle with RAII cleanup
/// Represents a conflict in the working copy.
pub struct Conflict {
    ptr: *mut subversion_sys::svn_client_conflict_t,
    pool: apr::Pool<'static>,
    _phantom: std::marker::PhantomData<*mut ()>, // !Send + !Sync
}

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

impl Conflict {
    /// Get a reference to the underlying pool
    pub fn pool(&self) -> &apr::Pool<'_> {
        &self.pool
    }

    /// Get the raw pointer to the conflict (use with caution)
    pub fn as_ptr(&self) -> *const subversion_sys::svn_client_conflict_t {
        self.ptr
    }

    /// Get the mutable raw pointer to the conflict (use with caution)
    pub fn as_mut_ptr(&mut self) -> *mut subversion_sys::svn_client_conflict_t {
        self.ptr
    }
    pub(crate) unsafe fn from_ptr_and_pool(
        ptr: *mut subversion_sys::svn_client_conflict_t,
        pool: apr::Pool<'static>,
    ) -> Self {
        Self {
            ptr,
            pool,
            _phantom: std::marker::PhantomData,
        }
    }

    /// Gets the description of a property conflict.
    pub fn prop_get_description(&mut self) -> Result<String, Error<'static>> {
        let pool = apr::pool::Pool::new();
        let mut description: *const i8 = std::ptr::null_mut();
        let err = unsafe {
            subversion_sys::svn_client_conflict_prop_get_description(
                &mut description,
                self.ptr,
                pool.as_mut_ptr(),
                pool.as_mut_ptr(),
            )
        };

        Error::from_raw(err)?;
        Ok(unsafe { std::ffi::CStr::from_ptr(description) }
            .to_str()
            .unwrap()
            .to_owned())
    }

    /// Resolve a text conflict by option ID
    pub fn text_resolve_by_id(
        &mut self,
        choice: crate::TextConflictChoice,
        ctx: &mut Context,
    ) -> Result<(), Error<'static>> {
        let scratch_pool = apr::pool::Pool::new();
        unsafe {
            let err = subversion_sys::svn_client_conflict_text_resolve_by_id(
                self.ptr,
                choice.into(),
                ctx.as_mut_ptr(),
                scratch_pool.as_mut_ptr(),
            );
            svn_result(err)
        }
    }

    /// Resolve a property conflict by option ID
    pub fn prop_resolve_by_id(
        &mut self,
        propname: &str,
        choice: crate::TextConflictChoice,
        ctx: &mut Context,
    ) -> Result<(), Error<'static>> {
        let scratch_pool = apr::pool::Pool::new();
        let propname_c = std::ffi::CString::new(propname)?;
        unsafe {
            let err = subversion_sys::svn_client_conflict_prop_resolve_by_id(
                self.ptr,
                propname_c.as_ptr(),
                choice.into(),
                ctx.as_mut_ptr(),
                scratch_pool.as_mut_ptr(),
            );
            svn_result(err)
        }
    }

    /// Resolve a property conflict with a specific option
    ///
    /// This is useful when you've customized an option (e.g., with set_merged_propval)
    /// and want to resolve using that customized option.
    pub fn prop_resolve_with_option(
        &mut self,
        propname: &str,
        option: &ConflictOption,
        ctx: &mut Context,
    ) -> Result<(), Error<'static>> {
        let scratch_pool = apr::pool::Pool::new();
        let propname_c = std::ffi::CString::new(propname)?;
        unsafe {
            let err = subversion_sys::svn_client_conflict_prop_resolve(
                self.ptr,
                propname_c.as_ptr(),
                option.ptr,
                ctx.as_mut_ptr(),
                scratch_pool.as_mut_ptr(),
            );
            svn_result(err)
        }
    }

    /// Resolve a text conflict with a specific option
    ///
    /// This is useful when you've customized an option
    /// and want to resolve using that customized option.
    pub fn text_resolve(
        &mut self,
        option: &ConflictOption,
        ctx: &mut Context,
    ) -> Result<(), Error<'static>> {
        let scratch_pool = apr::pool::Pool::new();
        unsafe {
            let err = subversion_sys::svn_client_conflict_text_resolve(
                self.ptr,
                option.ptr,
                ctx.as_mut_ptr(),
                scratch_pool.as_mut_ptr(),
            );
            svn_result(err)
        }
    }

    /// Resolve a tree conflict by option ID
    pub fn tree_resolve_by_id(
        &mut self,
        choice: crate::TreeConflictChoice,
        ctx: &mut Context,
    ) -> Result<(), Error<'static>> {
        let scratch_pool = apr::pool::Pool::new();
        unsafe {
            let err = subversion_sys::svn_client_conflict_tree_resolve_by_id(
                self.ptr,
                choice.into(),
                ctx.as_mut_ptr(),
                scratch_pool.as_mut_ptr(),
            );
            svn_result(err)
        }
    }

    /// Resolve a tree conflict with a specific option
    ///
    /// This is useful when you've customized an option (e.g., with set_moved_to_abspath)
    /// and want to resolve using that customized option.
    pub fn tree_resolve(
        &mut self,
        option: &ConflictOption,
        ctx: &mut Context,
    ) -> Result<(), Error<'static>> {
        let scratch_pool = apr::pool::Pool::new();
        unsafe {
            let err = subversion_sys::svn_client_conflict_tree_resolve(
                self.ptr,
                option.ptr,
                ctx.as_mut_ptr(),
                scratch_pool.as_mut_ptr(),
            );
            svn_result(err)
        }
    }

    /// Get whether the conflict has text, property, or tree conflicts
    pub fn get_conflicted(&self) -> Result<(bool, Vec<String>, bool), Error<'_>> {
        let pool = apr::pool::Pool::new();
        let mut text_conflicted: subversion_sys::svn_boolean_t = 0;
        let mut props_conflicted: *mut apr_sys::apr_array_header_t = std::ptr::null_mut();
        let mut tree_conflicted: subversion_sys::svn_boolean_t = 0;

        unsafe {
            let err = subversion_sys::svn_client_conflict_get_conflicted(
                &mut text_conflicted,
                &mut props_conflicted,
                &mut tree_conflicted,
                self.ptr,
                pool.as_mut_ptr(),
                pool.as_mut_ptr(),
            );
            svn_result(err)?;

            let has_text_conflict = text_conflicted != 0;
            let has_tree_conflict = tree_conflicted != 0;

            let mut prop_conflicts = Vec::new();
            if !props_conflicted.is_null() {
                // Property conflicts array contains property names
                let array =
                    apr::tables::TypedArray::<*const std::ffi::c_char>::from_ptr(props_conflicted);
                for cstr_ptr in array.iter() {
                    let cstr = std::ffi::CStr::from_ptr(cstr_ptr);
                    let propname = cstr.to_string_lossy().into_owned();
                    prop_conflicts.push(propname);
                }
            }

            Ok((has_text_conflict, prop_conflicts, has_tree_conflict))
        }
    }

    /// Get the operation that caused the conflict
    pub fn get_operation(&self) -> Result<crate::conflict::ConflictAction, Error<'static>> {
        unsafe {
            let operation = subversion_sys::svn_client_conflict_get_operation(self.ptr);
            Ok(operation.into())
        }
    }

    /// Get whether the conflict involves a binary file
    pub fn text_get_mime_type(&self) -> Result<Option<String>, Error<'_>> {
        unsafe {
            let mime_type = subversion_sys::svn_client_conflict_text_get_mime_type(self.ptr);
            if mime_type.is_null() {
                Ok(None)
            } else {
                Ok(Some(
                    std::ffi::CStr::from_ptr(mime_type)
                        .to_string_lossy()
                        .into_owned(),
                ))
            }
        }
    }

    /// Get the absolute path of the conflicted item
    pub fn get_local_abspath(&self) -> &str {
        unsafe {
            let abspath = subversion_sys::svn_client_conflict_get_local_abspath(self.ptr);
            std::ffi::CStr::from_ptr(abspath).to_str().unwrap()
        }
    }

    /// Get the incoming change type
    pub fn get_incoming_change(&self) -> crate::conflict::ConflictAction {
        unsafe { subversion_sys::svn_client_conflict_get_incoming_change(self.ptr).into() }
    }

    /// Get the local change type
    pub fn get_local_change(&self) -> crate::conflict::ConflictAction {
        unsafe { subversion_sys::svn_client_conflict_get_local_change(self.ptr).into() }
    }

    /// Get repository root URL and UUID
    pub fn get_repos_info(&self) -> Result<(Option<String>, Option<String>), Error<'_>> {
        let pool = apr::pool::Pool::new();
        let mut repos_root_url: *const i8 = std::ptr::null();
        let mut repos_uuid: *const i8 = std::ptr::null();

        unsafe {
            let err = subversion_sys::svn_client_conflict_get_repos_info(
                &mut repos_root_url,
                &mut repos_uuid,
                self.ptr,
                pool.as_mut_ptr(),
                pool.as_mut_ptr(),
            );
            svn_result(err)?;

            let root_url = if repos_root_url.is_null() {
                None
            } else {
                Some(
                    std::ffi::CStr::from_ptr(repos_root_url)
                        .to_str()
                        .unwrap()
                        .to_owned(),
                )
            };

            let uuid = if repos_uuid.is_null() {
                None
            } else {
                Some(
                    std::ffi::CStr::from_ptr(repos_uuid)
                        .to_str()
                        .unwrap()
                        .to_owned(),
                )
            };

            Ok((root_url, uuid))
        }
    }

    /// Get the recommended option ID for resolving this conflict
    pub fn get_recommended_option_id(&self) -> crate::ClientConflictOptionId {
        unsafe { subversion_sys::svn_client_conflict_get_recommended_option_id(self.ptr).into() }
    }

    /// Get available resolution options for a text conflict
    pub fn text_get_resolution_options(
        &self,
        ctx: &mut Context,
    ) -> Result<Vec<ConflictOption>, Error<'_>> {
        let pool = apr::pool::Pool::new();
        let mut options: *mut apr_sys::apr_array_header_t = std::ptr::null_mut();

        unsafe {
            let err = subversion_sys::svn_client_conflict_text_get_resolution_options(
                &mut options,
                self.ptr,
                ctx.as_mut_ptr(),
                self.pool.as_mut_ptr(),
                pool.as_mut_ptr(),
            );
            svn_result(err)?;

            let mut result = Vec::new();
            if !options.is_null() {
                let array = apr::tables::TypedArray::<
                    *mut subversion_sys::svn_client_conflict_option_t,
                >::from_ptr(options);
                for option_ptr in array.iter() {
                    result.push(ConflictOption {
                        ptr: option_ptr,
                        merged_value_pool: None,
                    });
                }
            }
            Ok(result)
        }
    }

    /// Get available resolution options for a property conflict
    pub fn prop_get_resolution_options(
        &self,
        ctx: &mut Context,
    ) -> Result<Vec<ConflictOption>, Error<'_>> {
        let pool = apr::pool::Pool::new();
        let mut options: *mut apr_sys::apr_array_header_t = std::ptr::null_mut();

        unsafe {
            let err = subversion_sys::svn_client_conflict_prop_get_resolution_options(
                &mut options,
                self.ptr,
                ctx.as_mut_ptr(),
                self.pool.as_mut_ptr(),
                pool.as_mut_ptr(),
            );
            svn_result(err)?;

            let mut result = Vec::new();
            if !options.is_null() {
                let array = apr::tables::TypedArray::<
                    *mut subversion_sys::svn_client_conflict_option_t,
                >::from_ptr(options);
                for option_ptr in array.iter() {
                    result.push(ConflictOption {
                        ptr: option_ptr,
                        merged_value_pool: None,
                    });
                }
            }
            Ok(result)
        }
    }

    /// Get available resolution options for a tree conflict
    pub fn tree_get_resolution_options(
        &self,
        ctx: &mut Context,
    ) -> Result<Vec<ConflictOption>, Error<'_>> {
        let pool = apr::pool::Pool::new();
        let mut options: *mut apr_sys::apr_array_header_t = std::ptr::null_mut();

        unsafe {
            let err = subversion_sys::svn_client_conflict_tree_get_resolution_options(
                &mut options,
                self.ptr,
                ctx.as_mut_ptr(),
                self.pool.as_mut_ptr(),
                pool.as_mut_ptr(),
            );
            svn_result(err)?;

            let mut result = Vec::new();
            if !options.is_null() {
                let array = apr::tables::TypedArray::<
                    *mut subversion_sys::svn_client_conflict_option_t,
                >::from_ptr(options);
                for option_ptr in array.iter() {
                    result.push(ConflictOption {
                        ptr: option_ptr,
                        merged_value_pool: None,
                    });
                }
            }
            Ok(result)
        }
    }

    /// Get description of a tree conflict
    ///
    /// Returns a tuple of (incoming_change_description, local_change_description)
    pub fn tree_get_description(
        &self,
        ctx: &mut Context,
    ) -> Result<(String, String), Error<'static>> {
        let pool = apr::pool::Pool::new();
        let mut incoming_desc: *const i8 = std::ptr::null();
        let mut local_desc: *const i8 = std::ptr::null();

        unsafe {
            let err = subversion_sys::svn_client_conflict_tree_get_description(
                &mut incoming_desc,
                &mut local_desc,
                self.ptr,
                ctx.as_mut_ptr(),
                self.pool.as_mut_ptr(),
                pool.as_mut_ptr(),
            );
            svn_result(err)?;

            let incoming = std::ffi::CStr::from_ptr(incoming_desc)
                .to_str()
                .unwrap()
                .to_owned();
            let local = std::ffi::CStr::from_ptr(local_desc)
                .to_str()
                .unwrap()
                .to_owned();

            Ok((incoming, local))
        }
    }

    /// Get details for a tree conflict from the repository.
    ///
    /// This function contacts the repository to gather more information about
    /// a tree conflict. Call this before getting resolution options for better
    /// recommendations.
    pub fn tree_get_details(&mut self, ctx: &mut Context) -> Result<(), Error<'static>> {
        let pool = apr::pool::Pool::new();
        unsafe {
            let err = subversion_sys::svn_client_conflict_tree_get_details(
                self.ptr,
                ctx.as_mut_ptr(),
                pool.as_mut_ptr(),
            );
            svn_result(err)
        }
    }

    /// Get the victim node kind of a tree conflict.
    pub fn tree_get_victim_node_kind(&self) -> crate::NodeKind {
        unsafe { subversion_sys::svn_client_conflict_tree_get_victim_node_kind(self.ptr).into() }
    }

    /// Get the incoming old repository location.
    ///
    /// Returns (repos_relpath, revision, node_kind).
    pub fn get_incoming_old_repos_location(
        &self,
    ) -> Result<(Option<String>, crate::Revnum, crate::NodeKind), Error<'_>> {
        let pool = apr::pool::Pool::new();
        let mut repos_relpath: *const i8 = std::ptr::null();
        let mut revision: subversion_sys::svn_revnum_t = -1;
        let mut node_kind: subversion_sys::svn_node_kind_t = 0;

        unsafe {
            let err = subversion_sys::svn_client_conflict_get_incoming_old_repos_location(
                &mut repos_relpath,
                &mut revision,
                &mut node_kind,
                self.ptr,
                pool.as_mut_ptr(),
                pool.as_mut_ptr(),
            );
            svn_result(err)?;

            let path = if repos_relpath.is_null() {
                None
            } else {
                Some(
                    std::ffi::CStr::from_ptr(repos_relpath)
                        .to_str()
                        .unwrap()
                        .to_owned(),
                )
            };

            Ok((path, crate::Revnum(revision), node_kind.into()))
        }
    }

    /// Get the incoming new repository location.
    ///
    /// Returns (repos_relpath, revision, node_kind).
    pub fn get_incoming_new_repos_location(
        &self,
    ) -> Result<(Option<String>, crate::Revnum, crate::NodeKind), Error<'_>> {
        let pool = apr::pool::Pool::new();
        let mut repos_relpath: *const i8 = std::ptr::null();
        let mut revision: subversion_sys::svn_revnum_t = -1;
        let mut node_kind: subversion_sys::svn_node_kind_t = 0;

        unsafe {
            let err = subversion_sys::svn_client_conflict_get_incoming_new_repos_location(
                &mut repos_relpath,
                &mut revision,
                &mut node_kind,
                self.ptr,
                pool.as_mut_ptr(),
                pool.as_mut_ptr(),
            );
            svn_result(err)?;

            let path = if repos_relpath.is_null() {
                None
            } else {
                Some(
                    std::ffi::CStr::from_ptr(repos_relpath)
                        .to_str()
                        .unwrap()
                        .to_owned(),
                )
            };

            Ok((path, crate::Revnum(revision), node_kind.into()))
        }
    }

    /// Get the current resolution for a text conflict.
    pub fn text_get_resolution(&self) -> crate::ClientConflictOptionId {
        unsafe { subversion_sys::svn_client_conflict_text_get_resolution(self.ptr).into() }
    }

    /// Get the current resolution for a property conflict.
    pub fn prop_get_resolution(
        &self,
        propname: &str,
    ) -> Result<crate::ClientConflictOptionId, Error<'static>> {
        let propname_c = std::ffi::CString::new(propname)?;
        unsafe {
            Ok(subversion_sys::svn_client_conflict_prop_get_resolution(
                self.ptr,
                propname_c.as_ptr(),
            )
            .into())
        }
    }

    /// Get the current resolution for a tree conflict.
    pub fn tree_get_resolution(&self) -> crate::ClientConflictOptionId {
        unsafe { subversion_sys::svn_client_conflict_tree_get_resolution(self.ptr).into() }
    }

    /// Get the reject file path for a property conflict.
    pub fn prop_get_reject_abspath(&self) -> Option<String> {
        unsafe {
            let path = subversion_sys::svn_client_conflict_prop_get_reject_abspath(self.ptr);
            if path.is_null() {
                None
            } else {
                Some(std::ffi::CStr::from_ptr(path).to_str().unwrap().to_owned())
            }
        }
    }

    /// Get the file paths containing the different versions of the conflicted text.
    ///
    /// Returns (base_abspath, working_abspath, incoming_old_abspath, incoming_new_abspath).
    /// Any of these may be None if that version is not available.
    pub fn text_get_contents(
        &self,
    ) -> Result<
        (
            Option<String>,
            Option<String>,
            Option<String>,
            Option<String>,
        ),
        Error<'_>,
    > {
        let pool = apr::pool::Pool::new();
        let mut base: *const i8 = std::ptr::null();
        let mut working: *const i8 = std::ptr::null();
        let mut incoming_old: *const i8 = std::ptr::null();
        let mut incoming_new: *const i8 = std::ptr::null();

        unsafe {
            let err = subversion_sys::svn_client_conflict_text_get_contents(
                &mut base,
                &mut working,
                &mut incoming_old,
                &mut incoming_new,
                self.ptr,
                pool.as_mut_ptr(),
                pool.as_mut_ptr(),
            );
            svn_result(err)?;

            let to_opt_string = |ptr: *const i8| {
                if ptr.is_null() {
                    None
                } else {
                    Some(std::ffi::CStr::from_ptr(ptr).to_str().unwrap().to_owned())
                }
            };

            Ok((
                to_opt_string(base),
                to_opt_string(working),
                to_opt_string(incoming_old),
                to_opt_string(incoming_new),
            ))
        }
    }

    /// Get the property values for a property conflict.
    ///
    /// Returns (base_propval, working_propval, incoming_old_propval, incoming_new_propval).
    pub fn prop_get_propvals(
        &self,
        propname: &str,
    ) -> Result<
        (
            Option<Vec<u8>>,
            Option<Vec<u8>>,
            Option<Vec<u8>>,
            Option<Vec<u8>>,
        ),
        Error<'_>,
    > {
        let pool = apr::pool::Pool::new();
        let propname_c = std::ffi::CString::new(propname)?;
        let mut base: *const subversion_sys::svn_string_t = std::ptr::null();
        let mut working: *const subversion_sys::svn_string_t = std::ptr::null();
        let mut incoming_old: *const subversion_sys::svn_string_t = std::ptr::null();
        let mut incoming_new: *const subversion_sys::svn_string_t = std::ptr::null();

        unsafe {
            let err = subversion_sys::svn_client_conflict_prop_get_propvals(
                &mut base,
                &mut working,
                &mut incoming_old,
                &mut incoming_new,
                self.ptr,
                propname_c.as_ptr(),
                pool.as_mut_ptr(),
            );
            svn_result(err)?;

            let to_opt_vec = |ptr: *const subversion_sys::svn_string_t| {
                if ptr.is_null() {
                    None
                } else {
                    let s = &*ptr;
                    if s.data.is_null() {
                        None
                    } else {
                        Some(std::slice::from_raw_parts(s.data as *const u8, s.len).to_vec())
                    }
                }
            };

            Ok((
                to_opt_vec(base),
                to_opt_vec(working),
                to_opt_vec(incoming_old),
                to_opt_vec(incoming_new),
            ))
        }
    }
}

/// Represents a conflict resolution option
pub struct ConflictOption {
    ptr: *mut subversion_sys::svn_client_conflict_option_t,
    // Pool to keep merged propval alive until resolution
    merged_value_pool: Option<apr::Pool<'static>>,
}

impl ConflictOption {
    /// Get the option ID
    pub fn get_id(&self) -> crate::ClientConflictOptionId {
        unsafe { subversion_sys::svn_client_conflict_option_get_id(self.ptr).into() }
    }

    /// Get the human-readable label for this option
    pub fn get_label(&self) -> String {
        let pool = apr::pool::Pool::new();
        unsafe {
            let label =
                subversion_sys::svn_client_conflict_option_get_label(self.ptr, pool.as_mut_ptr());
            std::ffi::CStr::from_ptr(label).to_str().unwrap().to_owned()
        }
    }

    /// Get the human-readable description for this option
    pub fn get_description(&self) -> String {
        let pool = apr::pool::Pool::new();
        unsafe {
            let desc = subversion_sys::svn_client_conflict_option_get_description(
                self.ptr,
                pool.as_mut_ptr(),
            );
            std::ffi::CStr::from_ptr(desc).to_str().unwrap().to_owned()
        }
    }

    /// Set the merged property value for this conflict option
    /// Used when manually merging property conflicts.
    ///
    /// The value will be kept alive until the option is dropped or this method is called again.
    pub fn set_merged_propval(&mut self, merged_propval: Option<&[u8]>) {
        // Create a new pool to store the merged value
        // This replaces any previous pool
        let pool = apr::pool::Pool::new();

        unsafe {
            let svn_string = merged_propval.map(|val| {
                subversion_sys::svn_string_ncreate(
                    val.as_ptr() as *const i8,
                    val.len(),
                    pool.as_mut_ptr(),
                )
            });
            subversion_sys::svn_client_conflict_option_set_merged_propval(
                self.ptr,
                svn_string.unwrap_or(std::ptr::null_mut()),
            );
        }

        // Store the pool to keep the value alive
        self.merged_value_pool = Some(pool);
    }

    /// Get the list of possible moved-to repository relative paths for tree conflict resolution
    pub fn get_moved_to_repos_relpath_candidates(&self) -> Result<Vec<String>, Error<'_>> {
        let pool = apr::pool::Pool::new();
        let mut candidates: *mut apr_sys::apr_array_header_t = std::ptr::null_mut();

        unsafe {
            let err =
                subversion_sys::svn_client_conflict_option_get_moved_to_repos_relpath_candidates2(
                    &mut candidates,
                    self.ptr,
                    pool.as_mut_ptr(),
                    pool.as_mut_ptr(),
                );
            svn_result(err)?;

            let mut result = Vec::new();
            if !candidates.is_null() {
                let array =
                    apr::tables::TypedArray::<*const std::ffi::c_char>::from_ptr(candidates);
                for cstr_ptr in array.iter() {
                    let cstr = std::ffi::CStr::from_ptr(cstr_ptr);
                    result.push(cstr.to_string_lossy().into_owned());
                }
            }
            Ok(result)
        }
    }

    /// Get the list of possible moved-to absolute paths for tree conflict resolution
    pub fn get_moved_to_abspath_candidates(&self) -> Result<Vec<std::path::PathBuf>, Error<'_>> {
        let pool = apr::pool::Pool::new();
        let mut candidates: *mut apr_sys::apr_array_header_t = std::ptr::null_mut();

        unsafe {
            let err = subversion_sys::svn_client_conflict_option_get_moved_to_abspath_candidates2(
                &mut candidates,
                self.ptr,
                pool.as_mut_ptr(),
                pool.as_mut_ptr(),
            );
            svn_result(err)?;

            let mut result = Vec::new();
            if !candidates.is_null() {
                let array =
                    apr::tables::TypedArray::<*const std::ffi::c_char>::from_ptr(candidates);
                for cstr_ptr in array.iter() {
                    let cstr = std::ffi::CStr::from_ptr(cstr_ptr);
                    result.push(std::path::PathBuf::from(cstr.to_str()?));
                }
            }
            Ok(result)
        }
    }

    /// Set the moved-to repository relative path for tree conflict resolution by index
    ///
    /// The index refers to a candidate from get_moved_to_repos_relpath_candidates()
    pub fn set_moved_to_repos_relpath(
        &mut self,
        preferred_move_target_idx: i32,
        ctx: &mut Context,
    ) -> Result<(), Error<'static>> {
        let pool = apr::pool::Pool::new();

        unsafe {
            let err = subversion_sys::svn_client_conflict_option_set_moved_to_repos_relpath2(
                self.ptr,
                preferred_move_target_idx,
                ctx.as_mut_ptr(),
                pool.as_mut_ptr(),
            );
            svn_result(err)
        }
    }

    /// Set the moved-to absolute path for tree conflict resolution by index
    ///
    /// The index refers to a candidate from get_moved_to_abspath_candidates()
    pub fn set_moved_to_abspath(
        &mut self,
        preferred_move_target_idx: i32,
        ctx: &mut Context,
    ) -> Result<(), Error<'static>> {
        let pool = apr::pool::Pool::new();

        unsafe {
            let err = subversion_sys::svn_client_conflict_option_set_moved_to_abspath2(
                self.ptr,
                preferred_move_target_idx,
                ctx.as_mut_ptr(),
                pool.as_mut_ptr(),
            );
            svn_result(err)
        }
    }

    /// Find a conflict option by its ID
    pub fn find_by_id(
        options: &[ConflictOption],
        option_id: crate::ClientConflictOptionId,
    ) -> Option<&ConflictOption> {
        options.iter().find(|opt| opt.get_id() == option_id)
    }
}

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

    // Builder-setter coverage for the option structs. Each test chains every
    // setter with a non-default value and asserts the corresponding field, so
    // a setter that returns Default::default() instead of mutating `self` is
    // caught. Revision is not PartialEq, so it is checked with matches!.

    #[test]
    fn test_switch_options_builder() {
        let o = SwitchOptions::new()
            .with_peg_revision(Revision::Number(Revnum(1)))
            .with_revision(Revision::Number(Revnum(2)))
            .with_depth(Depth::Files)
            .with_depth_is_sticky(true)
            .with_ignore_externals(true)
            .with_allow_unver_obstructions(true)
            .with_ignore_ancestry(true);
        assert!(matches!(o.peg_revision, Revision::Number(Revnum(1))));
        assert!(matches!(o.revision, Revision::Number(Revnum(2))));
        assert_eq!(o.depth, Depth::Files);
        assert!(o.depth_is_sticky);
        assert!(o.ignore_externals);
        assert!(o.allow_unver_obstructions);
        assert!(o.ignore_ancestry);
    }

    #[test]
    fn test_add_options_builder() {
        let o = AddOptions::new()
            .with_depth(Depth::Files)
            .with_force(true)
            .with_no_ignore(true)
            .with_no_autoprops(true)
            .with_add_parents(true);
        assert_eq!(o.depth, Depth::Files);
        assert!(o.force);
        assert!(o.no_ignore);
        assert!(o.no_autoprops);
        assert!(o.add_parents);
    }

    #[test]
    fn test_merge_sources_options_builder() {
        let o = MergeSourcesOptions::new()
            .with_ignore_mergeinfo(true)
            .with_diff_ignore_ancestry(true)
            .with_force_delete(true)
            .with_record_only(true)
            .with_dry_run(true)
            .with_allow_mixed_rev(true)
            .with_merge_options(vec!["-x".to_string()]);
        assert!(o.ignore_mergeinfo);
        assert!(o.diff_ignore_ancestry);
        assert!(o.force_delete);
        assert!(o.record_only);
        assert!(o.dry_run);
        assert!(o.allow_mixed_rev);
        assert_eq!(o.merge_options.as_deref(), Some(&["-x".to_string()][..]));
    }

    #[test]
    fn test_diff_summarize_options_builder() {
        let o = DiffSummarizeOptions::new()
            .with_depth(Depth::Files)
            .with_ignore_ancestry(true)
            .with_changelists(vec!["cl".to_string()]);
        assert_eq!(o.depth, Depth::Files);
        assert!(o.ignore_ancestry);
        assert_eq!(o.changelists.as_deref(), Some(&["cl".to_string()][..]));
    }

    #[test]
    fn test_revert_options_builder() {
        let o = RevertOptions::new()
            .with_depth(Depth::Files)
            .with_changelists(vec!["cl".to_string()])
            .with_clear_changelists(true)
            .with_added_keep_local(true);
        assert_eq!(o.depth, Depth::Files);
        assert_eq!(o.changelists.as_deref(), Some(&["cl".to_string()][..]));
        assert!(o.clear_changelists);
        assert!(o.added_keep_local);
    }

    #[test]
    fn test_revprop_set_options_builder() {
        let o = RevpropSetOptions::new()
            .with_revision(Revision::Number(Revnum(3)))
            .with_original_propval(b"old".to_vec())
            .with_force(true);
        assert!(matches!(o.revision, Revision::Number(Revnum(3))));
        assert_eq!(o.original_propval.as_deref(), Some(&b"old"[..]));
        assert!(o.force);
    }

    #[test]
    fn test_prop_set_options_builder() {
        let o = PropSetOptions::new()
            .with_depth(Depth::Files)
            .with_skip_checks(true)
            .with_base_revision_for_url(Revnum(5))
            .with_changelists(vec!["cl".to_string()]);
        assert_eq!(o.depth, Depth::Files);
        assert!(o.skip_checks);
        assert_eq!(o.base_revision_for_url, Revnum(5));
        assert_eq!(o.changelists.as_deref(), Some(&["cl".to_string()][..]));
    }

    #[test]
    fn test_prop_get_options_builder() {
        let o = PropGetOptions::new()
            .with_peg_revision(Revision::Number(Revnum(1)))
            .with_revision(Revision::Number(Revnum(2)))
            .with_depth(Depth::Files)
            .with_changelists(vec!["cl".to_string()]);
        assert!(matches!(o.peg_revision, Revision::Number(Revnum(1))));
        assert!(matches!(o.revision, Revision::Number(Revnum(2))));
        assert_eq!(o.depth, Depth::Files);
        assert_eq!(o.changelists.as_deref(), Some(&["cl".to_string()][..]));
    }

    #[test]
    fn test_patch_options_builder() {
        let o = PatchOptions::new()
            .with_dry_run(true)
            .with_strip_count(3)
            .with_reverse(true)
            .with_ignore_whitespace(true)
            .with_remove_tempfiles(true);
        assert!(o.dry_run);
        assert_eq!(o.strip_count, 3);
        assert!(o.reverse);
        assert!(o.ignore_whitespace);
        assert!(o.remove_tempfiles);
    }

    #[test]
    fn test_diff_options_builder() {
        let o = DiffOptions::new()
            .with_diff_options(vec!["-x".to_string()])
            .with_depth(Depth::Files)
            .with_ignore_ancestry(true)
            .with_no_diff_added(true)
            .with_no_diff_deleted(true)
            .with_show_copies_as_adds(true)
            .with_ignore_content_type(true)
            .with_ignore_properties(true)
            .with_properties_only(true)
            .with_use_git_diff_format(true)
            .with_header_encoding("UTF-8".to_string())
            .with_changelists(vec!["cl".to_string()]);
        assert_eq!(o.diff_options, vec!["-x".to_string()]);
        assert_eq!(o.depth, Depth::Files);
        assert!(o.ignore_ancestry);
        assert!(o.no_diff_added);
        assert!(o.no_diff_deleted);
        assert!(o.show_copies_as_adds);
        assert!(o.ignore_content_type);
        assert!(o.ignore_properties);
        assert!(o.properties_only);
        assert!(o.use_git_diff_format);
        assert_eq!(o.header_encoding, "UTF-8");
        assert_eq!(o.changelists.as_deref(), Some(&["cl".to_string()][..]));
    }

    #[test]
    fn test_status_options_builder() {
        let o = StatusOptions::new()
            .with_revision(Revision::Number(Revnum(2)))
            .with_get_all(true)
            .with_check_out_of_date(true)
            .with_check_working_copy(true)
            .with_no_ignore(true)
            .with_ignore_externals(true)
            .with_depth_as_sticky(true)
            .with_changelists(vec!["cl".to_string()]);
        assert!(matches!(o.revision, Revision::Number(Revnum(2))));
        assert!(o.get_all);
        assert!(o.check_out_of_date);
        assert!(o.check_working_copy);
        assert!(o.no_ignore);
        assert!(o.ignore_externals);
        assert!(o.depth_as_sticky);
        assert_eq!(o.changelists.as_deref(), Some(&["cl".to_string()][..]));
    }

    #[test]
    fn test_info_options_builder() {
        let o = InfoOptions::new()
            .with_peg_revision(Revision::Number(Revnum(1)))
            .with_revision(Revision::Number(Revnum(2)))
            .with_depth(Depth::Files)
            .with_fetch_excluded(true)
            .with_fetch_actual_only(true)
            .with_include_externals(true)
            .with_changelists(vec!["cl".to_string()]);
        assert!(matches!(o.peg_revision, Revision::Number(Revnum(1))));
        assert!(matches!(o.revision, Revision::Number(Revnum(2))));
        assert_eq!(o.depth, Depth::Files);
        assert!(o.fetch_excluded);
        assert!(o.fetch_actual_only);
        assert!(o.include_externals);
        assert_eq!(o.changelists.as_deref(), Some(&["cl".to_string()][..]));
    }

    #[test]
    fn test_list_options_builder() {
        let o = ListOptions::new()
            .with_peg_revision(Revision::Number(Revnum(1)))
            .with_revision(Revision::Number(Revnum(2)))
            .with_patterns(vec!["*.rs".to_string()])
            .with_depth(Depth::Files)
            .with_dirent_fields(7)
            .with_fetch_locks(true)
            .with_include_externals(true);
        assert!(matches!(o.peg_revision, Revision::Number(Revnum(1))));
        assert!(matches!(o.revision, Revision::Number(Revnum(2))));
        assert_eq!(o.patterns.as_deref(), Some(&["*.rs".to_string()][..]));
        assert_eq!(o.depth, Depth::Files);
        assert_eq!(o.dirent_fields, 7);
        assert!(o.fetch_locks);
        assert!(o.include_externals);
    }

    #[test]
    fn test_log_options_builder() {
        let o = LogOptions::new()
            .with_peg_revision(Revision::Number(Revnum(1)))
            .with_limit(10)
            .with_discover_changed_paths(true)
            .with_strict_node_history(true)
            .with_include_merged_revisions(true)
            .with_revprops(Some(vec!["svn:log".to_string()]));
        assert!(matches!(o.peg_revision, Revision::Number(Revnum(1))));
        assert_eq!(o.limit, Some(10));
        assert!(o.discover_changed_paths);
        assert!(o.strict_node_history);
        assert!(o.include_merged_revisions);
        assert_eq!(o.revprops.as_deref(), Some(&["svn:log".to_string()][..]));
    }

    #[test]
    fn test_blame_options_builder() {
        let o = BlameOptions::new()
            .with_peg_revision(Revision::Number(Revnum(1)))
            .with_start_revision(Revision::Number(Revnum(2)))
            .with_end_revision(Revision::Number(Revnum(3)))
            .with_diff_options(vec!["-x".to_string()])
            .with_ignore_mime_type(true)
            .with_include_merged_revisions(true);
        assert!(matches!(o.peg_revision, Revision::Number(Revnum(1))));
        assert!(matches!(o.start_revision, Revision::Number(Revnum(2))));
        assert!(matches!(o.end_revision, Revision::Number(Revnum(3))));
        assert_eq!(o.diff_options, vec!["-x".to_string()]);
        assert!(o.ignore_mime_type);
        assert!(o.include_merged_revisions);
    }

    #[test]
    fn test_mergeinfo_log_options_builder() {
        let o = MergeinfoLogOptions::new()
            // The default is true, so set false to pin the setter.
            .with_finding_merged(false)
            .with_target_peg_revision(Revision::Number(Revnum(1)))
            .with_source_peg_revision(Revision::Number(Revnum(2)))
            .with_source_start_revision(Revision::Number(Revnum(3)))
            .with_source_end_revision(Revision::Number(Revnum(4)))
            .with_discover_changed_paths(true)
            .with_depth(Depth::Files)
            .with_revprops(vec!["svn:log".to_string()]);
        assert!(!o.finding_merged);
        assert!(matches!(o.target_peg_revision, Revision::Number(Revnum(1))));
        assert!(matches!(o.source_peg_revision, Revision::Number(Revnum(2))));
        assert!(matches!(
            o.source_start_revision,
            Revision::Number(Revnum(3))
        ));
        assert!(matches!(o.source_end_revision, Revision::Number(Revnum(4))));
        assert!(o.discover_changed_paths);
        assert_eq!(o.depth, Depth::Files);
        assert_eq!(o.revprops, vec!["svn:log".to_string()]);
    }

    #[test]
    fn test_commit_options_builder() {
        let o = CommitOptions::new()
            .with_depth(Depth::Files)
            .with_keep_locks(true)
            .with_keep_changelists(true)
            .with_commit_as_operations(true)
            .with_include_file_externals(true)
            .with_include_dir_externals(true)
            .with_changelists(vec!["cl".to_string()]);
        assert_eq!(o.depth, Depth::Files);
        assert!(o.keep_locks);
        assert!(o.keep_changelists);
        assert!(o.commit_as_operations);
        assert!(o.include_file_externals);
        assert!(o.include_dir_externals);
        assert_eq!(o.changelists.as_deref(), Some(&["cl".to_string()][..]));
    }

    #[test]
    fn test_delete_options_builder() {
        let o = DeleteOptions::new().with_force(true).with_keep_local(true);
        assert!(o.force);
        assert!(o.keep_local);
    }

    #[test]
    fn test_mkdir_options_builder() {
        let mut revprops = std::collections::HashMap::new();
        revprops.insert("svn:log".to_string(), b"m".to_vec());
        let o = MkdirOptions::new()
            .with_make_parents(true)
            .with_revprop_table(revprops);
        assert!(o.make_parents);
        assert!(o.revprop_table.is_some());
        assert_eq!(
            o.revprop_table.as_ref().unwrap().get("svn:log"),
            Some(&b"m".to_vec())
        );
    }

    #[test]
    fn test_move_options_builder() {
        let mut revprops = std::collections::HashMap::new();
        revprops.insert("svn:log".to_string(), b"m".to_vec());
        let o = MoveOptions::new()
            .with_move_as_child(true)
            .with_make_parents(true)
            .with_allow_mixed_revisions(false)
            .with_metadata_only(true)
            .with_revprop_table(revprops);
        assert!(o.move_as_child);
        assert!(o.make_parents);
        assert!(!o.allow_mixed_revisions); // default is true
        assert!(o.metadata_only);
        assert!(o.revprop_table.is_some());
    }

    #[test]
    fn test_copy_options_builder() {
        let mut pin = std::collections::HashMap::new();
        pin.insert("ext".to_string(), "2".to_string());
        let mut revprops = std::collections::HashMap::new();
        revprops.insert("svn:log".to_string(), "m".to_string());
        let o = CopyOptions::new()
            .with_copy_as_child(true)
            .with_make_parents(true)
            .with_ignore_externals(true)
            .with_metadata_only(true)
            .with_pin_externals(true)
            .with_externals_to_pin(pin)
            .with_revprop_table(revprops);
        assert!(o.copy_as_child);
        assert!(o.make_parents);
        assert!(o.ignore_externals);
        assert!(o.metadata_only);
        assert!(o.pin_externals);
        assert_eq!(
            o.externals_to_pin.as_ref().unwrap().get("ext"),
            Some(&"2".to_string())
        );
        assert!(o.revprop_table.is_some());
    }

    #[test]
    fn test_import_options_builder() {
        let mut revprops = std::collections::HashMap::new();
        revprops.insert("svn:log".to_string(), "m".to_string());
        let o = ImportOptions::new()
            .with_depth(Depth::Files)
            .with_no_ignore(true)
            .with_no_autoprops(true)
            .with_ignore_unknown_node_types(true)
            .with_revprop_table(revprops);
        assert_eq!(o.depth, Depth::Files);
        assert!(o.no_ignore);
        assert!(o.no_autoprops);
        assert!(o.ignore_unknown_node_types);
        assert!(o.revprop_table.is_some());
    }

    #[test]
    fn test_prop_set_remote_options_builder() {
        let mut revprops = std::collections::HashMap::new();
        revprops.insert("svn:log".to_string(), "m".to_string());
        let o = PropSetRemoteOptions::new()
            .with_skip_checks(true)
            .with_revprop_table(revprops);
        assert!(o.skip_checks);
        assert!(o.revprop_table.is_some());
    }

    #[test]
    fn test_info_and_wc_info_accessors() {
        let td = tempfile::tempdir().unwrap();
        let repo = td.path().join("repo");
        let wc = td.path().join("wc");
        let repos = crate::repos::Repos::create(&repo).unwrap();

        // Commit a directory so the repository is at revision 1; this makes the
        // revision accessors non-zero (revision 0's default would otherwise
        // coincide with Revnum's Default).
        let mut txn = repos
            .begin_txn_for_commit(Revnum(0), "alice", "init")
            .unwrap();
        {
            let mut root = txn.root().unwrap();
            root.make_dir("/trunk").unwrap();
        }
        repos.commit_txn(txn).unwrap();

        let url = crate::path_to_file_url(&repo);
        let mut ctx = Context::new().unwrap();
        ctx.checkout(
            crate::uri::Uri::new(&url).unwrap(),
            &wc,
            &CheckoutOptions {
                peg_revision: Revision::Head,
                revision: Revision::Head,
                depth: Depth::Infinity,
                ignore_externals: false,
                allow_unver_obstructions: false,
            },
        )
        .unwrap();

        // An unspecified revision queries the working copy, which populates
        // wc_info (HEAD/working-revision queries leave it null).
        let wc_abs = wc.canonicalize().unwrap();
        let mut visited = 0;
        ctx.info(
            wc_abs.to_str().unwrap(),
            &InfoOptions {
                revision: Revision::Unspecified,
                peg_revision: Revision::Unspecified,
                ..Default::default()
            },
            &|_path, info| {
                visited += 1;

                // Info accessors for the checked-out root, now at revision 1.
                assert_eq!(info.url(), url);
                assert_eq!(info.revision(), Revnum(1));
                assert_eq!(info.kind(), subversion_sys::svn_node_kind_t_svn_node_dir);
                assert_eq!(info.repos_root_url(), url);
                assert_eq!(info.repos_uuid().len(), 36);
                // A directory has no meaningful size.
                assert_eq!(info.size(), -1);
                assert_eq!(info.last_changed_rev(), Some(Revnum(1)));

                // wc_info is populated for a working-copy target.
                let wc_info = info.wc_info().expect("wc_info present for WC target");
                assert_eq!(
                    wc_info.schedule(),
                    subversion_sys::svn_wc_schedule_t_svn_wc_schedule_normal
                );
                assert_eq!(
                    wc_info.depth(),
                    subversion_sys::svn_depth_t_svn_depth_infinity
                );
                // A plain checkout is not a copy and has no changelist.
                assert_eq!(wc_info.copyfrom_url(), None);
                assert_eq!(wc_info.copyfrom_rev(), None);
                assert_eq!(wc_info.changelist(), None);

                // dup() produces an owned copy with the same data.
                let owned = wc_info.dup();
                assert_eq!(
                    owned.schedule(),
                    subversion_sys::svn_wc_schedule_t_svn_wc_schedule_normal
                );

                Ok(())
            },
        )
        .unwrap();
        assert_eq!(visited, 1);
    }

    /// Test fixture for client tests with repository and working copy.
    struct ClientTestFixture {
        pub wc_path: PathBuf,
        pub url: String,
        pub ctx: Context,
        pub temp_dir: tempfile::TempDir,
    }

    impl ClientTestFixture {
        /// Creates a repository and checks out a working copy.
        fn new() -> Self {
            let temp_dir = tempfile::TempDir::new().unwrap();
            let repos_path = temp_dir.path().join("repo");
            let wc_path = temp_dir.path().join("wc");

            // Create repository
            let _repos = crate::repos::Repos::create(&repos_path).unwrap();

            // Prepare URL
            let repos_dirent = crate::dirent::Dirent::new(&repos_path).unwrap();
            let uri = repos_dirent.to_file_url().unwrap();
            let url = uri.to_string();

            // Create client context and checkout
            let mut ctx = Context::new().unwrap();
            ctx.checkout(uri, &wc_path, &Self::default_checkout_options())
                .unwrap();

            Self {
                wc_path,
                url,
                ctx,
                temp_dir,
            }
        }

        /// Returns default checkout options for HEAD with full depth.
        fn default_checkout_options() -> CheckoutOptions {
            CheckoutOptions {
                peg_revision: Revision::Head,
                revision: Revision::Head,
                depth: Depth::Infinity,
                ignore_externals: false,
                allow_unver_obstructions: false,
            }
        }

        /// Creates and adds a file to the working copy.
        fn add_file(&mut self, name: &str, content: &str) -> PathBuf {
            let file_path = self.wc_path.join(name);
            std::fs::write(&file_path, content).unwrap();
            self.ctx.add(&file_path, &AddOptions::new()).unwrap();
            file_path
        }

        /// Creates and adds a directory to the working copy.
        fn add_dir(&mut self, name: &str) -> PathBuf {
            let dir_path = self.wc_path.join(name);
            std::fs::create_dir(&dir_path).unwrap();
            self.ctx.add(&dir_path, &AddOptions::new()).unwrap();
            dir_path
        }

        /// Commits all changes in the working copy.
        fn commit(&mut self) -> Revnum {
            let commit_opts = CommitOptions::default();
            let revprops = std::collections::HashMap::new();
            let mut committed_rev = None;
            self.ctx
                .commit(
                    &[self
                        .wc_path
                        .to_str()
                        .expect("wc path should be valid UTF-8")],
                    &commit_opts,
                    revprops,
                    None,
                    &mut |info| {
                        committed_rev = Some(info.revision());
                        Ok(())
                    },
                )
                .unwrap();
            committed_rev.unwrap_or(Revnum(0))
        }

        /// Gets the working copy path as a UTF-8 string slice.
        fn wc_path_str(&self) -> &str {
            self.wc_path
                .to_str()
                .expect("working copy path should be valid UTF-8")
        }
    }

    #[test]
    fn test_version() {
        let version = version();
        assert_eq!(version.major(), 1);
    }

    #[test]
    fn test_set_auth_unchecked() {
        let td = tempfile::tempdir().unwrap();
        let repo_path = td.path().join("repo");
        let _repos = crate::repos::Repos::create(&repo_path).unwrap();
        let dirent = crate::dirent::Dirent::new(&repo_path).unwrap();
        let url = dirent.to_file_url().unwrap();

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

        let mut ctx = Context::new().unwrap();
        // Safety: auth_baton outlives ctx in this test
        unsafe { ctx.set_auth_unchecked(&mut auth_baton) };

        // Verify the context works with the borrowed auth baton
        let mut found = false;
        ctx.info(
            url.as_str(),
            &InfoOptions {
                revision: Revision::Head,
                ..Default::default()
            },
            &|_path, _info| {
                found = true;
                Ok(())
            },
        )
        .unwrap();
        assert!(found);
    }

    #[test]
    fn test_open() {
        let td = tempfile::tempdir().unwrap();
        let repo_path = td.path().join("repo");
        let mut repos = crate::repos::Repos::create(&repo_path).unwrap();
        assert_eq!(repos.path(), td.path().join("repo"));
        let mut ctx = Context::new().unwrap();
        let dirent = crate::dirent::Dirent::new(&repo_path).unwrap();
        let url = dirent.to_file_url().unwrap();
        let revnum = ctx
            .checkout(
                url,
                td.path().join("wc"),
                &CheckoutOptions {
                    peg_revision: Revision::Head,
                    revision: Revision::Head,
                    depth: Depth::Infinity,
                    ignore_externals: false,
                    allow_unver_obstructions: false,
                },
            )
            .unwrap();
        assert_eq!(revnum, Revnum(0));
    }

    #[test]
    fn test_copy() {
        let mut fixture = ClientTestFixture::new();
        let test_file = fixture.add_file("test.txt", "test content");

        // Test copy operation (would need a commit first in real scenario)
        let copy_result = fixture.ctx.copy(
            &[(
                test_file.to_str().expect("path should be valid UTF-8"),
                None,
            )],
            fixture
                .wc_path
                .join("test_copy.txt")
                .to_str()
                .expect("path should be valid UTF-8"),
            &mut CopyOptions::new(),
        );

        // Copy fails because the file isn't committed yet (has no URL)
        assert!(copy_result.is_err());
    }

    #[test]
    fn test_mkdir_multiple() {
        let mut fixture = ClientTestFixture::new();

        // Test creating multiple directories
        let dir1 = fixture.wc_path.join("dir1");
        let dir2 = fixture.wc_path.join("dir2");

        let result = fixture.ctx.mkdir_multiple(
            &[
                dir1.to_str().expect("path should be valid UTF-8"),
                dir2.to_str().expect("path should be valid UTF-8"),
            ],
            &mut MkdirOptions::new(),
        );

        // Should succeed in working copy
        result.unwrap();
        assert!(dir1.exists());
        assert!(dir2.exists());
    }

    #[test]
    fn test_propget_propset() {
        let mut fixture = ClientTestFixture::new();
        let test_file = fixture.add_file("test.txt", "test content");

        // Test setting a property
        let propset_result = fixture.ctx.propset(
            "test:property",
            Some(b"test value"),
            test_file.to_str().expect("path should be valid UTF-8"),
            &PropSetOptions::new().with_depth(Depth::Empty),
        );

        // Setting properties should work on added files
        propset_result.unwrap();

        // Test getting the property back
        let props = fixture
            .ctx
            .propget(
                "test:property",
                test_file.to_str().expect("path should be valid UTF-8"),
                &PropGetOptions::new()
                    .with_peg_revision(Revision::Working)
                    .with_revision(Revision::Working)
                    .with_depth(Depth::Empty),
                None, // actual_revnum
            )
            .unwrap();

        assert!(!props.is_empty(), "Should have at least one property entry");
        for (_path, value) in props.iter() {
            assert_eq!(value, b"test value");
        }
    }

    #[test]
    fn test_propget_with_inherited() {
        let mut fixture = ClientTestFixture::new();

        // Set a property on the working copy root
        let wc_path_str = fixture.wc_path_str().to_string();
        fixture
            .ctx
            .propset(
                "test:parent-property",
                Some(b"parent value"),
                &wc_path_str,
                &PropSetOptions::new().with_depth(Depth::Empty),
            )
            .unwrap();

        // Create a subdirectory and a file in it
        let subdir = fixture.add_dir("subdir");
        let test_file = subdir.join("test.txt");
        std::fs::write(&test_file, "test content").unwrap();
        fixture.ctx.add(&test_file, &AddOptions::new()).unwrap();

        // Set a property on the file
        fixture
            .ctx
            .propset(
                "test:file-property",
                Some(b"file value"),
                test_file.to_str().expect("path should be valid UTF-8"),
                &PropSetOptions::new().with_depth(Depth::Empty),
            )
            .unwrap();

        // Get properties with inherited properties
        let test_file_str = test_file.to_str().expect("path should be valid UTF-8");
        let result = fixture.ctx.propget_with_inherited(
            "test:parent-property",
            test_file_str,
            &PropGetOptions::new()
                .with_peg_revision(Revision::Working)
                .with_revision(Revision::Working)
                .with_depth(Depth::Empty),
            None,
        );

        assert!(result.is_ok(), "propget_with_inherited should succeed");
        let (props, inherited) = result.unwrap();

        // The file itself doesn't have test:parent-property
        assert!(props.is_empty() || !props.contains_key(test_file_str));

        // But it should be in the inherited properties from the parent
        // Note: inherited properties might be empty if SVN version doesn't support it
        // or if the implementation differs. This is a basic smoke test.
        // Just verify the function succeeds and returns the expected structure
        assert!(inherited.is_empty() || !inherited.is_empty()); // Either outcome is valid
    }

    #[test]
    fn test_move_path() {
        let mut fixture = ClientTestFixture::new();
        let test_file = fixture.add_file("test_file.txt", "Test content");
        fixture.commit();

        // Now move the file
        let new_path = fixture.wc_path.join("renamed_file.txt");
        let result = fixture.ctx.move_path(
            &[test_file.to_str().expect("path should be valid UTF-8")],
            new_path.to_str().expect("path should be valid UTF-8"),
            &mut MoveOptions::new(),
        );

        result.unwrap();

        // Check that the new file exists
        assert!(new_path.exists());

        // Check that the old file doesn't exist
        assert!(!test_file.exists());

        // Check the content is preserved
        let content = std::fs::read(&new_path).expect("should read file");
        assert_eq!(content, b"Test content");
    }

    #[test]
    fn test_revert() {
        let mut fixture = ClientTestFixture::new();
        let test_file = fixture.add_file("test_file.txt", "Original content");
        fixture.commit();

        // Modify the file
        std::fs::write(&test_file, b"Modified content").expect("should write file");

        // Verify the file is modified
        assert_eq!(
            std::fs::read(&test_file).expect("should read file"),
            b"Modified content"
        );

        // Revert the changes
        let result = fixture.ctx.revert(
            &[test_file.to_str().expect("path should be valid UTF-8")],
            &RevertOptions::new().with_depth(Depth::Empty),
        );

        result.unwrap();

        // Verify the file is reverted to original content
        assert_eq!(
            std::fs::read(&test_file).expect("should read file"),
            b"Original content"
        );
    }

    #[test]
    fn test_resolved() {
        let mut fixture = ClientTestFixture::new();
        let test_file = fixture.add_file("conflict_file.txt", "Initial content");
        fixture.commit();

        // Test the resolved function - it should not fail even without actual conflicts
        // (In a real scenario, this would be called after manually resolving conflicts)
        let result = fixture.ctx.resolved(
            test_file.to_str().expect("path should be valid UTF-8"),
            false, // recursive
        );

        // The function should succeed (even if there were no conflicts to resolve)
        result.unwrap();
    }

    #[test]
    fn test_proplist_all() {
        let mut fixture = ClientTestFixture::new();

        // Test listing properties
        let mut prop_count = 0;
        let options = ProplistOptions {
            peg_revision: Revision::Working,
            revision: Revision::Working,
            depth: Depth::Empty,
            changelists: None,
            get_target_inherited_props: false,
        };
        let result = fixture.ctx.proplist_all(
            fixture
                .wc_path
                .to_str()
                .expect("path should be valid UTF-8"),
            &options,
            &mut |_path, _props| {
                prop_count += 1;
                Ok(())
            },
        );

        // Should not fail even if no properties
        result.unwrap();
    }

    #[test]
    fn test_diff() {
        let mut fixture = ClientTestFixture::new();

        // Create output streams for diff
        let mut out_stream = crate::io::Stream::buffered();
        let mut err_stream = crate::io::Stream::buffered();

        // Test diff between repository revisions using direct function
        let diff_result = fixture.ctx.diff(
            &fixture.url,
            &Revision::Head,
            &fixture.url,
            &Revision::Head,
            None, // relative_to_dir
            &mut out_stream,
            &mut err_stream,
            &DiffOptions::new().with_depth(Depth::Infinity),
        );

        // Should succeed even with no differences
        diff_result.unwrap();
    }

    #[test]
    fn test_diff_relative_to_dir() {
        let mut fixture = ClientTestFixture::new();
        let test_file = fixture.add_file("test.txt", "line1\n");
        fixture.commit();

        // Modify file
        std::fs::write(&test_file, "line1\nline2\n").unwrap();

        let mut out_stream = crate::io::Stream::buffered();
        let mut err_stream = crate::io::Stream::buffered();

        // Test diff with relative_to_dir as a relative path
        let wc_path = fixture.wc_path.to_str().unwrap();
        let result = fixture.ctx.diff(
            wc_path,
            &Revision::Number(Revnum(1)),
            wc_path,
            &Revision::Working,
            Some(wc_path),
            &mut out_stream,
            &mut err_stream,
            &DiffOptions::new().with_depth(Depth::Infinity),
        );
        result.unwrap();
    }

    #[test]
    fn test_diff_builder() {
        let mut fixture = ClientTestFixture::new();

        // Create output streams
        let mut out_stream = crate::io::Stream::buffered();
        let mut err_stream = crate::io::Stream::buffered();

        // Test diff using builder pattern - much cleaner!
        let result = fixture
            .ctx
            .diff_builder(
                fixture.url.clone(),
                Revision::Head,
                fixture.url.clone(),
                Revision::Head,
            )
            .depth(Depth::Infinity)
            .ignore_ancestry(true)
            .use_git_diff_format(true)
            .execute(&mut out_stream, &mut err_stream);

        result.unwrap();
    }

    #[test]
    fn test_list() {
        let mut fixture = ClientTestFixture::new();

        // Test listing repository contents
        let mut entries = Vec::new();
        let options = ListOptions {
            peg_revision: Revision::Head,
            revision: Revision::Head,
            patterns: None,
            depth: Depth::Infinity,
            dirent_fields: subversion_sys::SVN_DIRENT_KIND
                | subversion_sys::SVN_DIRENT_SIZE
                | subversion_sys::SVN_DIRENT_HAS_PROPS
                | subversion_sys::SVN_DIRENT_CREATED_REV
                | subversion_sys::SVN_DIRENT_TIME
                | subversion_sys::SVN_DIRENT_LAST_AUTHOR,
            fetch_locks: false,
            include_externals: false,
        };
        let result = fixture
            .ctx
            .list(&fixture.url, &options, &mut |path: &str, _dirent, _lock| {
                entries.push(path.to_string());
                Ok(())
            });

        // Should succeed for empty repository
        result.unwrap();
    }

    #[test]
    fn test_list_builder() {
        let mut fixture = ClientTestFixture::new();

        // Test listing using builder pattern
        let mut entries = Vec::new();
        let result = fixture
            .ctx
            .list_builder(&fixture.url)
            .depth(Depth::Files)
            .fetch_locks(true)
            .patterns(vec!["*.txt".to_string()])
            .execute(&mut |path, _dirent, _lock| {
                entries.push(path.to_string());
                Ok(())
            });

        result.unwrap();
    }

    #[test]
    fn test_copy_builder() {
        let mut fixture = ClientTestFixture::new();
        let test_file = fixture.add_file("test.txt", "test content");

        // Copy fails because the file isn't committed yet (has no URL)
        let result = fixture
            .ctx
            .copy_builder(
                fixture
                    .wc_path
                    .join("test_copy.txt")
                    .to_str()
                    .expect("path should be valid UTF-8"),
            )
            .add_source(
                test_file.to_str().expect("path should be valid UTF-8"),
                None,
            )
            .make_parents(true)
            .ignore_externals(true)
            .execute();

        assert!(result.is_err());
    }

    #[test]
    fn test_info_builder() {
        let mut fixture = ClientTestFixture::new();

        // Test info using builder pattern
        let mut info_count = 0;
        let result = fixture
            .ctx
            .info_builder(&fixture.url)
            .depth(Depth::Empty)
            .fetch_excluded(true)
            .execute(&|_path, _info| {
                info_count += 1;
                Ok(())
            });

        result.unwrap();
    }

    #[test]
    fn test_resolve() {
        let mut fixture = ClientTestFixture::new();

        // Test resolve (even though there are no conflicts)
        let result = fixture.ctx.resolve(
            fixture
                .wc_path
                .to_str()
                .expect("path should be valid UTF-8"),
            Depth::Infinity,
            crate::ConflictChoice::Postpone,
        );

        // Should succeed even with no conflicts
        result.unwrap();
    }

    #[test]
    fn test_commit_builder() {
        // Create temporary directories for test
        let td = tempfile::tempdir().unwrap();
        let repo_path = td.path().join("repo");
        let wc_path = td.path().join("wc");

        // Create a test repository and get its UUID
        let repos = crate::repos::Repos::create(&repo_path).unwrap();
        let fs = repos.fs().unwrap();
        let uuid = fs.get_uuid().unwrap();
        drop(fs);
        drop(repos);

        // Create working copy directory
        std::fs::create_dir_all(&wc_path).unwrap();

        // Initialize working copy using wc context
        let mut wc_ctx = crate::wc::Context::new().unwrap();
        let repo_abs_path = repo_path.canonicalize().unwrap();
        let wc_abs_path = wc_path.canonicalize().unwrap();
        let url_str = crate::path_to_file_url(&repo_abs_path);

        // Create basic .svn structure using ensure_adm
        wc_ctx
            .ensure_adm(
                wc_abs_path.to_str().unwrap(),
                &url_str,
                &url_str,
                &uuid,
                crate::Revnum(0),
                crate::Depth::Infinity,
            )
            .unwrap();

        // Create and add a test file
        let test_file = wc_path.join("test.txt");
        std::fs::write(&test_file, "test content").unwrap();

        // Create client context for adding and committing
        let mut ctx = Context::new().unwrap();

        // Add the file using its absolute path
        let test_file_abs = test_file.canonicalize().unwrap();
        ctx.add(
            &test_file_abs,
            &AddOptions {
                depth: Depth::Empty,
                force: false,
                no_ignore: false,
                no_autoprops: false,
                add_parents: false,
            },
        )
        .unwrap();

        // Test commit using builder pattern
        let builder = CommitBuilder::new(&mut ctx)
            .add_target(wc_abs_path.to_str().unwrap())
            .depth(Depth::Infinity);

        // Execute the commit
        let result = builder.execute(None, &mut |_info| Ok(()));

        // Check that the commit succeeded
        assert!(result.is_ok(), "Commit failed: {:?}", result.err());
    }

    #[test]
    fn test_log_builder() {
        let mut fixture = ClientTestFixture::new();
        fixture.add_file("test.txt", "test content");
        fixture.commit();

        // Test log using builder pattern
        let mut log_entries = Vec::new();
        let result = fixture
            .ctx
            .log_builder()
            .add_target(fixture.url.clone())
            .add_revision_range(Revision::Number(Revnum(0)), Revision::Head)
            .discover_changed_paths(true)
            .strict_node_history(false)
            .include_merged_revisions(false)
            .execute(&|_entry| {
                log_entries.push(());
                Ok(())
            });

        assert!(result.is_ok(), "Log failed: {:?}", result.err());
    }

    #[test]
    fn test_update_builder() {
        let mut fixture = ClientTestFixture::new();

        // Test update using builder pattern
        let result = fixture
            .ctx
            .update_builder()
            .add_path(
                fixture
                    .wc_path
                    .to_str()
                    .expect("path should be valid UTF-8"),
            )
            .revision(Revision::Head)
            .depth(Depth::Infinity)
            .depth_is_sticky(false)
            .ignore_externals(false)
            .allow_unver_obstructions(false)
            .adds_as_modification(false)
            .make_parents(false)
            .execute();

        result.unwrap();
    }

    #[test]
    fn test_mkdir_builder() {
        let mut fixture = ClientTestFixture::new();

        // Test mkdir using builder pattern
        let new_dir = fixture.wc_path.join("new_dir");
        let result = fixture
            .ctx
            .mkdir_builder()
            .add_path(new_dir.to_str().expect("path should be valid UTF-8"))
            .make_parents(true)
            .execute(&mut |_info| Ok(()));

        assert!(result.is_ok(), "Mkdir failed: {:?}", result.err());
        assert!(new_dir.exists());
    }

    #[test]
    fn test_patch_dry_run() {
        let mut ctx = Context::new().unwrap();

        // Create a simple patch file
        let temp_dir = std::env::temp_dir();
        let patch_file = temp_dir.join("test.patch");
        std::fs::write(
            &patch_file,
            "--- a/test.txt\n+++ b/test.txt\n@@ -1 +1 @@\n-old\n+new\n",
        )
        .unwrap();

        let wc_dir = temp_dir.join("test_wc_patch");
        let _ = std::fs::remove_dir_all(&wc_dir);
        std::fs::create_dir_all(&wc_dir).unwrap();

        // Test dry run patch application (should not fail even without valid WC)
        let result = ctx.patch(
            &patch_file,
            &wc_dir,
            &mut PatchOptions::new().with_dry_run(true),
        );

        // Clean up
        let _ = std::fs::remove_file(&patch_file);
        let _ = std::fs::remove_dir_all(&wc_dir);

        // Patch should fail since the directory is not a working copy
        assert!(
            result.is_err(),
            "Patch should fail for non-working-copy directory"
        );
    }

    #[test]
    fn test_relocate_invalid_path() {
        let mut ctx = Context::new().unwrap();

        let temp_dir = std::env::temp_dir();
        let invalid_wc = temp_dir.join("non_existent_wc");

        // Test relocate with invalid working copy path
        let result = ctx.relocate(
            &invalid_wc,
            "http://old.example.com/repo",
            "http://new.example.com/repo",
            false, // ignore_externals
        );

        // Should fail for non-existent working copy
        assert!(result.is_err());
    }

    #[test]
    fn test_revprop_operations() {
        let mut ctx = Context::new().unwrap();

        // Test with invalid URL (should fail gracefully)
        {
            let result =
                ctx.revprop_get("svn:author", "file:///non/existent/repo", &Revision::Head);
            // Should return error for invalid repository
            assert!(result.is_err());
        }

        // Test revprop_list with invalid URL
        {
            let result = ctx.revprop_list("file:///non/existent/repo", &Revision::Head);
            // Should return error for invalid repository
            assert!(result.is_err());
        }

        // Test revprop_set with invalid URL
        {
            let result = ctx.revprop_set(
                "test:property",
                Some(b"test value"),
                "file:///non/existent/repo",
                &RevpropSetOptions::new().with_revision(Revision::Head),
            );
            // Should return error for invalid repository
            assert!(result.is_err());
        }
    }

    #[test]
    fn test_revprop_property_name_validation() {
        let mut ctx = Context::new().unwrap();

        // Test that property name validation works for valid names
        // (The actual repository error will happen, but the property name should be accepted)
        {
            let result =
                ctx.revprop_get("svn:author", "file:///non/existent/repo", &Revision::Head);
            assert!(
                result.is_err(),
                "Should fail due to non-existent repo, not property name"
            );
        }

        // Test that custom properties with valid names are accepted
        {
            let result = ctx.revprop_set(
                "custom:property",
                Some(b"test value"),
                "file:///non/existent/repo",
                &RevpropSetOptions::new().with_revision(Revision::Head),
            );
            assert!(
                result.is_err(),
                "Should fail due to non-existent repo, not property name"
            );
        }

        // Note: Invalid property names (with NUL bytes or other issues) are caught
        // during CString conversion, which returns a proper error
    }

    #[test]
    fn test_revprop_hash_conversion() {
        // Test that revprops can be properly created and converted
        // This verifies our APR hash API changes work correctly
        use std::collections::HashMap;

        // Test BlameInfo structure which stores revprops
        let mut revprops_map = HashMap::new();
        revprops_map.insert("svn:author".to_string(), b"test_user".to_vec());
        revprops_map.insert("svn:log".to_string(), b"test commit message".to_vec());
        revprops_map.insert("custom:prop".to_string(), b"custom value".to_vec());

        let blame_info = BlameInfo {
            line_no: 1,
            revision: Revnum::from(123u64),
            revprops: revprops_map.clone(),
            merged_revision: None,
            merged_revprops: HashMap::new(),
            merged_path: None,
            line: "test line".to_string(),
            local_change: false,
        };

        // Verify the data is stored correctly
        assert_eq!(blame_info.revprops.len(), 3);
        assert_eq!(blame_info.revprops.get("svn:author").unwrap(), b"test_user");
        assert_eq!(
            blame_info.revprops.get("svn:log").unwrap(),
            b"test commit message"
        );
        assert_eq!(
            blame_info.revprops.get("custom:prop").unwrap(),
            b"custom value"
        );

        // Test that CommitBuilder and MkdirBuilder patterns work
        let mut ctx = Context::new().unwrap();
        let _commit_builder = CommitBuilder::new(&mut ctx)
            .add_revprop("svn:author", "test_author")
            .add_revprop("svn:log", "test message");

        let _mkdir_builder = MkdirBuilder::new(&mut ctx)
            .add_revprop("svn:author", b"test_author".to_vec())
            .add_revprop("svn:log", b"test message".to_vec());
    }

    #[test]
    fn test_suggest_merge_sources_invalid_url() {
        let mut ctx = Context::new().unwrap();

        // Test with invalid URL
        let result = ctx.suggest_merge_sources("file:///non/existent/repo/trunk", &Revision::Head);

        // Should return error for invalid repository
        assert!(result.is_err());
    }

    #[test]
    fn test_upgrade_invalid_path() {
        let mut ctx = Context::new().unwrap();

        let temp_dir = std::env::temp_dir();
        let invalid_wc = temp_dir.join("non_existent_wc_upgrade");

        // Test upgrade with non-existent working copy
        let result = ctx.upgrade(&invalid_wc);

        // Should fail for non-existent working copy
        assert!(result.is_err());
    }

    #[test]
    fn test_cleanup_basic() {
        let mut ctx = Context::new().unwrap();
        let temp_dir = tempfile::tempdir().unwrap();
        let temp_path_str = temp_dir.path().to_str().unwrap();

        // Test cleanup on a non-working-copy directory (should fail)
        let options = CleanupOptions {
            break_locks: false,
            fix_recorded_timestamps: false,
            clear_dav_cache: false,
            vacuum_pristines: false,
            include_externals: false,
        };
        let result = ctx.cleanup(temp_path_str, &options);

        // Expected to fail without valid working copy
        assert!(result.is_err());
    }

    #[test]
    fn test_vacuum_basic() {
        let mut ctx = Context::new().unwrap();
        let temp_dir = tempfile::tempdir().unwrap();
        let temp_path_str = temp_dir.path().to_str().unwrap();

        // Test vacuum on a non-working-copy directory (should fail)
        let options = VacuumOptions {
            remove_unversioned_items: false,
            remove_ignored_items: false,
            fix_recorded_timestamps: false,
            vacuum_pristines: true,
            include_externals: false,
        };
        let result = ctx.vacuum(temp_path_str, &options);

        // Expected to fail without valid working copy
        assert!(result.is_err());
    }

    #[test]
    fn test_merge_reintegrate_invalid_params() {
        let mut ctx = Context::new().unwrap();

        let temp_dir = std::env::temp_dir();
        let invalid_wc = temp_dir.join("non_existent_merge_wc");

        // Test merge reintegrate with invalid parameters
        let result = ctx.merge_reintegrate(
            "file:///non/existent/source",
            &Revision::Head,
            &invalid_wc,
            true, // dry_run
            &[],  // merge_options
        );

        // Should fail for invalid source and target
        assert!(result.is_err());
    }

    #[test]
    fn test_blame_invalid_path() {
        let mut ctx = Context::new().unwrap();

        let mut blame_info_received = false;
        let mut receiver = |info: BlameInfo| -> Result<(), Error<'static>> {
            blame_info_received = true;
            // Verify the BlameInfo structure has expected fields
            assert!(info.line_no >= 0);
            // For invalid path, we might still get blame info or error
            Ok(())
        };

        // Test blame with invalid path/URL (should fail gracefully)
        let result = ctx.blame(
            "file:///non/existent/file.txt",
            &BlameOptions::new()
                .with_peg_revision(Revision::Head)
                .with_start_revision(Revision::Number(Revnum::from(1u64)))
                .with_end_revision(Revision::Head),
            &mut receiver,
        );

        // Should return error for invalid file
        assert!(result.is_err());
    }

    #[test]
    fn test_cat_invalid_path() {
        let mut ctx = Context::new().unwrap();

        let mut output = Vec::new();
        let options = CatOptions {
            revision: Revision::Head,
            peg_revision: Revision::Head,
            expand_keywords: false,
        };

        // Test cat with invalid path/URL (should fail gracefully)
        let result = ctx.cat("file:///non/existent/file.txt", &mut output, &options);

        // Should return error for invalid file
        assert!(result.is_err());
        // Output should remain empty
        assert!(output.is_empty());
    }

    #[test]
    fn test_blame_api_structure() {
        // Test that BlameInfo can be constructed and has expected fields
        use std::collections::HashMap;

        let info = BlameInfo {
            line_no: 42,
            revision: Revnum::from(123u64),
            revprops: HashMap::new(),
            merged_revision: None,
            merged_revprops: HashMap::new(),
            merged_path: None,
            line: "test line content".to_string(),
            local_change: false,
        };

        assert_eq!(info.line_no, 42);
        assert_eq!(info.revision, Revnum::from(123u64));
        assert_eq!(info.line, "test line content");
        assert!(!info.local_change);
        assert!(info.merged_revision.is_none());
        assert!(info.merged_path.is_none());
    }

    #[test]
    fn test_cat_options_structure() {
        let options = CatOptions {
            revision: Revision::Number(Revnum::from(42u64)),
            peg_revision: Revision::Head,
            expand_keywords: true,
        };

        // Verify options structure
        match options.revision {
            Revision::Number(n) => assert_eq!(n, Revnum::from(42u64)),
            _ => panic!("Expected Number revision"),
        }
        match options.peg_revision {
            Revision::Head => {} // expected
            _ => panic!("Expected Head revision"),
        }
        assert!(options.expand_keywords);

        // Test default
        let default_options = CatOptions::default();
        match default_options.revision {
            Revision::Unspecified => {} // expected default
            _ => panic!("Expected Unspecified revision as default"),
        }
        match default_options.peg_revision {
            Revision::Unspecified => {} // expected default
            _ => panic!("Expected Unspecified peg revision as default"),
        }
        assert!(!default_options.expand_keywords);
    }

    #[test]
    fn test_uuid_from_invalid_url() {
        let mut ctx = Context::new().unwrap();

        // Test UUID from invalid URL (should fail gracefully)
        let result = ctx.uuid_from_url("file:///non/existent/repo");

        // Should return error for invalid repository
        assert!(result.is_err());
    }

    #[test]
    fn test_uuid_from_invalid_path() {
        let mut ctx = Context::new().unwrap();

        let temp_dir = std::env::temp_dir();
        let invalid_path = temp_dir.join("non_existent_wc");

        // Test UUID from invalid working copy path (should fail gracefully)
        let result = ctx.uuid_from_path(&invalid_path);

        // Should return error for invalid working copy path
        assert!(result.is_err());
    }

    #[test]
    fn test_switch_with_ignore_ancestry() {
        // Create a repository with two unrelated directories
        let mut fixture = ClientTestFixture::new();

        let dir1_url = format!("{}/dir1", fixture.url);
        let dir2_url = format!("{}/dir2", fixture.url);

        // Create two unrelated directories in the repository
        fixture
            .ctx
            .mkdir(
                &[&dir1_url, &dir2_url],
                &mut MkdirOptions::new().with_make_parents(true),
            )
            .unwrap();

        // Checkout dir1 into a separate path
        let wc_path = fixture.temp_dir.path().join("switch_wc");
        let dir1_uri = crate::uri::Uri::new(&dir1_url).unwrap();
        fixture
            .ctx
            .checkout(
                dir1_uri.clone(),
                &wc_path,
                &CheckoutOptions {
                    peg_revision: Revision::Head,
                    revision: Revision::Head,
                    depth: Depth::Infinity,
                    ignore_externals: false,
                    allow_unver_obstructions: false,
                },
            )
            .unwrap();

        // Test switch to unrelated dir2 with ignore_ancestry=false - should fail
        let dir2_uri = crate::uri::Uri::new(&dir2_url).unwrap();
        let result = fixture.ctx.switch(
            &wc_path,
            dir2_uri.clone(),
            &SwitchOptions {
                peg_revision: Revision::Head,
                revision: Revision::Head,
                depth: Depth::Infinity,
                depth_is_sticky: false,
                ignore_externals: false,
                allow_unver_obstructions: false,
                ignore_ancestry: false,
            },
        );

        // Should fail because dir1 and dir2 don't share ancestry
        assert!(
            result.is_err(),
            "Switch without ignore_ancestry should fail for unrelated paths"
        );

        // Test switch with ignore_ancestry=true - should succeed
        let result = fixture.ctx.switch(
            &wc_path,
            dir2_uri.clone(),
            &SwitchOptions::new().with_ignore_ancestry(true),
        );

        // Should succeed with ignore_ancestry=true
        assert!(
            result.is_ok(),
            "Switch with ignore_ancestry=true should succeed: {:?}",
            result
        );
    }

    #[test]
    fn test_delete_with_commit_callback() {
        // Test that DeleteOptions accepts a commit_callback and delete() can be called with it
        let mut fixture = ClientTestFixture::new();
        let file_path = fixture.add_file("test.txt", "test content");
        fixture.commit();

        // Test delete with callback (callback only invoked for URL deletes, not WC deletes)
        let mut callback = |_info: &crate::CommitInfo| Ok(());

        let result = fixture.ctx.delete(
            &[file_path.to_str().expect("path should be valid UTF-8")],
            std::collections::HashMap::new(),
            &mut DeleteOptions::new().with_commit_callback(&mut callback),
        );

        assert!(
            result.is_ok(),
            "Delete with callback should succeed: {:?}",
            result
        );

        // Also test delete without callback
        let file_path2 = fixture.add_file("test2.txt", "test content 2");
        fixture.commit();

        let result = fixture.ctx.delete(
            &[file_path2.to_str().expect("path should be valid UTF-8")],
            std::collections::HashMap::new(),
            &mut DeleteOptions::new(),
        );

        assert!(
            result.is_ok(),
            "Delete without callback should succeed: {:?}",
            result
        );
    }

    #[test]
    fn test_commit_with_log_message() {
        use std::collections::HashMap;

        let mut fixture = ClientTestFixture::new();
        let filepath = fixture.add_file("test.txt", "content");

        // Test backward compatibility: commit with svn:log in revprops
        let mut revprops = HashMap::new();
        revprops.insert("svn:log", "test message");

        let result = fixture.ctx.commit(
            &[filepath.to_str().expect("path should be valid UTF-8")],
            &CommitOptions::default(),
            revprops,
            None, // No callback, use svn:log from revprops
            &mut |_info| Ok(()),
        );

        match &result {
            Ok(()) => println!("Success with svn:log in revprops!"),
            Err(e) => {
                println!("Error: {:?}", e);
                println!("Error code: {}", e.apr_err());
            }
        }

        result.unwrap();
    }

    #[test]
    fn test_copy_with_options() {
        // Test that CopyOptions works with revprop_table and commit_callback
        let mut fixture = ClientTestFixture::new();
        let file_path = fixture.add_file("original.txt", "original content");
        fixture.commit();

        // Test 1: Basic copy with default options (WC to WC)
        let copy_path = fixture.wc_path.join("copy1.txt");
        let result = fixture.ctx.copy(
            &[(
                file_path.to_str().expect("path should be valid UTF-8"),
                None,
            )],
            copy_path.to_str().expect("path should be valid UTF-8"),
            &mut CopyOptions::new(),
        );
        assert!(result.is_ok(), "Basic copy should succeed: {:?}", result);

        // Test 2: Copy with make_parents option
        let nested_copy_path = fixture.wc_path.join("subdir/nested/copy2.txt");
        let result = fixture.ctx.copy(
            &[(
                file_path.to_str().expect("path should be valid UTF-8"),
                None,
            )],
            nested_copy_path
                .to_str()
                .expect("path should be valid UTF-8"),
            &mut CopyOptions::new().with_make_parents(true),
        );
        assert!(
            result.is_ok(),
            "Copy with make_parents should create intermediate directories: {:?}",
            result
        );
        assert!(nested_copy_path.exists(), "Nested copy should exist");
    }

    #[test]
    fn test_move_with_options() {
        // Test that MoveOptions works with make_parents and callback
        let mut fixture = ClientTestFixture::new();
        let file_path = fixture.add_file("original.txt", "move test content");
        fixture.commit();

        // Test 1: Basic move with default options
        let move_path = fixture.wc_path.join("moved.txt");
        let result = fixture.ctx.move_path(
            &[file_path.to_str().expect("path should be valid UTF-8")],
            move_path.to_str().expect("path should be valid UTF-8"),
            &mut MoveOptions::new(),
        );
        assert!(result.is_ok(), "Basic move should succeed: {:?}", result);
        assert!(move_path.exists(), "Moved file should exist");
        assert!(!file_path.exists(), "Original file should not exist");

        // Test 2: Move with make_parents option
        let file_path2 = fixture.add_file("file2.txt", "another file");
        fixture.commit();

        let nested_move_path = fixture.wc_path.join("subdir/nested/moved2.txt");
        let result = fixture.ctx.move_path(
            &[file_path2.to_str().expect("path should be valid UTF-8")],
            nested_move_path
                .to_str()
                .expect("path should be valid UTF-8"),
            &mut MoveOptions::new().with_make_parents(true),
        );
        assert!(
            result.is_ok(),
            "Move with make_parents should create intermediate directories: {:?}",
            result
        );
        assert!(nested_move_path.exists(), "Nested moved file should exist");
    }

    #[test]
    fn test_merge_sources_with_options() {
        // Test that MergeSourcesOptions works with separate ignore flags
        let mut fixture = ClientTestFixture::new();
        let file_path = fixture.add_file("file.txt", "initial content");
        fixture.commit();

        // Modify the file and commit again
        std::fs::write(&file_path, "modified content").expect("should write file");
        fixture.commit();

        // Test merge_sources with dry_run option
        let result = fixture.ctx.merge_sources(
            &fixture.url,
            &Revision::Number(Revnum(1)),
            &fixture.url,
            &Revision::Number(Revnum(2)),
            fixture
                .wc_path
                .to_str()
                .expect("path should be valid UTF-8"),
            Depth::Infinity,
            &MergeSourcesOptions::new().with_dry_run(true),
        );

        // Dry run should succeed without actually making changes
        assert!(
            result.is_ok(),
            "Merge with dry_run should succeed: {:?}",
            result
        );

        // Test merge_sources with different options
        let result = fixture.ctx.merge_sources(
            &fixture.url,
            &Revision::Number(Revnum(1)),
            &fixture.url,
            &Revision::Number(Revnum(2)),
            fixture
                .wc_path
                .to_str()
                .expect("path should be valid UTF-8"),
            Depth::Infinity,
            &MergeSourcesOptions::new()
                .with_ignore_mergeinfo(true)
                .with_diff_ignore_ancestry(true),
        );

        assert!(
            result.is_ok(),
            "Merge with custom options should succeed: {:?}",
            result
        );
    }

    #[test]
    fn test_mergeinfo_log() {
        let mut fixture = ClientTestFixture::new();

        // Create a file and commit to trunk, capturing trunk revision
        fixture.add_file("file.txt", "initial content");
        let mut trunk_rev = Revnum(0);
        let wc_path_str = fixture.wc_path_str().to_string();
        fixture
            .ctx
            .commit(
                &[&wc_path_str],
                &CommitOptions::default(),
                std::collections::HashMap::new(),
                None,
                &mut |info: &crate::CommitInfo| {
                    trunk_rev = info.revision();
                    Ok(())
                },
            )
            .unwrap();

        // Create a branch URL and copy trunk to branch
        let trunk_url = fixture.url.clone();
        let branch_url = format!("{}/branch", fixture.url);
        fixture
            .ctx
            .copy(
                &[(trunk_url.as_str(), Some(Revision::Number(trunk_rev)))],
                &branch_url,
                &mut CopyOptions::new(),
            )
            .unwrap();

        // Check out the branch
        let branch_path = fixture.temp_dir.path().join("branch");
        fixture
            .ctx
            .checkout(
                crate::uri::Uri::new(&branch_url).unwrap(),
                &branch_path,
                &ClientTestFixture::default_checkout_options(),
            )
            .unwrap();

        // Modify trunk and commit
        let file_path = fixture.wc_path.join("file.txt");
        std::fs::write(&file_path, "trunk modification").unwrap();
        fixture
            .ctx
            .commit(
                &[&wc_path_str],
                &CommitOptions::default(),
                std::collections::HashMap::new(),
                Some(&mut |_items| Ok("test commit message".to_string())),
                &mut |_info: &crate::CommitInfo| Ok(()),
            )
            .unwrap();

        // Merge trunk changes to branch
        fixture
            .ctx
            .merge_sources(
                &trunk_url,
                &Revision::Number(trunk_rev),
                &trunk_url,
                &Revision::Head,
                branch_path.to_str().unwrap(),
                Depth::Infinity,
                &MergeSourcesOptions::new(),
            )
            .unwrap();

        // Commit the merge
        fixture
            .ctx
            .commit(
                &[branch_path.to_str().unwrap()],
                &CommitOptions::default(),
                std::collections::HashMap::new(),
                Some(&mut |_items| Ok("test commit message".to_string())),
                &mut |_info: &crate::CommitInfo| Ok(()),
            )
            .unwrap();

        // Test mergeinfo_log - query what was merged to branch from trunk
        let mut log_count = 0;
        let options = MergeinfoLogOptions::new()
            .with_finding_merged(true)
            .with_discover_changed_paths(true);

        let result = fixture
            .ctx
            .mergeinfo_log(&branch_url, &trunk_url, &options, &mut |_entry| {
                log_count += 1;
                Ok(())
            });

        // The call should succeed
        assert!(result.is_ok(), "mergeinfo_log should succeed: {:?}", result);

        // We should get at least one log entry (the merged revision)
        // Note: This is a basic smoke test - the exact count depends on implementation
        assert!(log_count > 0, "Should get at least one merge log entry");
    }

    #[test]
    fn test_patch_with_options() {
        // Test that PatchOptions works with different settings
        let mut fixture = ClientTestFixture::new();
        let file_path = fixture.add_file("test.txt", "line 1\nline 2\nline 3\n");
        fixture.commit();

        // Create a patch file
        let patch_path = fixture.temp_dir.path().join("test.patch");
        std::fs::write(
            &patch_path,
            "--- test.txt\n+++ test.txt\n@@ -1,3 +1,3 @@\n line 1\n-line 2\n+line 2 modified\n line 3\n",
        )
        .unwrap();

        // Test 1: Apply patch with dry_run
        let result = fixture.ctx.patch(
            &patch_path,
            &fixture.wc_path,
            &mut PatchOptions::new().with_dry_run(true),
        );
        assert!(result.is_ok(), "Patch dry_run should succeed: {:?}", result);

        // Verify file was not modified (dry run)
        let content = std::fs::read_to_string(&file_path).unwrap();
        assert_eq!(content, "line 1\nline 2\nline 3\n");

        // Test 2: Apply patch for real
        let result = fixture
            .ctx
            .patch(&patch_path, &fixture.wc_path, &mut PatchOptions::new());
        assert!(result.is_ok(), "Patch should succeed: {:?}", result);

        // Verify file was modified
        let content = std::fs::read_to_string(&file_path).unwrap();
        assert!(
            content.contains("line 2 modified"),
            "File should be patched"
        );
    }

    #[test]
    fn test_propset_remote() {
        let mut fixture = ClientTestFixture::new();
        let _test_file = fixture.add_file("file.txt", "test content");

        // Commit and capture revision
        let mut committed_rev = Revnum(0);
        fixture
            .ctx
            .commit(
                &[fixture
                    .wc_path
                    .to_str()
                    .expect("path should be valid UTF-8")],
                &CommitOptions::default(),
                std::collections::HashMap::new(),
                None,
                &mut |info| {
                    committed_rev = info.revision();
                    Ok(())
                },
            )
            .unwrap();

        // Set a property on the remote URL
        let file_url = format!("{}/file.txt", fixture.url);
        let mut options = PropSetRemoteOptions::new()
            .with_skip_checks(false)
            .with_base_revision_for_url(committed_rev);

        let result = fixture.ctx.propset_remote(
            "test:property",
            Some(b"test value"),
            &file_url,
            &mut options,
        );

        assert!(
            result.is_ok(),
            "propset_remote should succeed: {:?}",
            result
        );

        // Verify the property was set by getting it back
        let props_result = fixture.ctx.propget(
            "test:property",
            &file_url,
            &PropGetOptions {
                peg_revision: Revision::Head,
                revision: Revision::Head,
                depth: Depth::Empty,
                changelists: None,
            },
            None,
        );

        let props = props_result.unwrap();
        assert!(!props.is_empty(), "Property should be set");

        // Check the property value
        let prop_val = props.get(&file_url);
        assert!(prop_val.is_some());
        assert_eq!(prop_val.unwrap(), b"test value");
    }

    #[test]
    fn test_get_changelists() {
        let mut fixture = ClientTestFixture::new();

        // First try on empty working copy to see if basic call works
        let mut empty_found = Vec::new();
        fixture
            .ctx
            .get_changelists(
                fixture
                    .wc_path
                    .to_str()
                    .expect("path should be valid UTF-8"),
                Depth::Infinity,
                None,
                &mut |path, changelist| {
                    empty_found.push((path.to_string(), changelist.to_string()));
                    Ok(())
                },
            )
            .unwrap();
        assert_eq!(empty_found.len(), 0);

        // Create some test files
        let file1 = fixture.add_file("file1.txt", "content1");
        let file2 = fixture.add_file("file2.txt", "content2");
        let file3 = fixture.add_file("file3.txt", "content3");

        // Add files to changelists
        fixture
            .ctx
            .add_to_changelist(
                &[
                    file1.to_str().expect("path should be valid UTF-8"),
                    file2.to_str().expect("path should be valid UTF-8"),
                ],
                "changelist1",
                Depth::Empty,
                None,
            )
            .unwrap();

        fixture
            .ctx
            .add_to_changelist(
                &[file3.to_str().expect("path should be valid UTF-8")],
                "changelist2",
                Depth::Empty,
                None,
            )
            .unwrap();

        // Get all changelists
        let mut found_paths = Vec::new();
        fixture
            .ctx
            .get_changelists(
                fixture
                    .wc_path
                    .to_str()
                    .expect("path should be valid UTF-8"),
                Depth::Infinity,
                None,
                &mut |path, changelist| {
                    found_paths.push((path.to_string(), changelist.to_string()));
                    Ok(())
                },
            )
            .unwrap();

        assert_eq!(found_paths.len(), 3);
        assert!(found_paths
            .iter()
            .any(|(p, cl)| p.ends_with("file1.txt") && cl == "changelist1"));
        assert!(found_paths
            .iter()
            .any(|(p, cl)| p.ends_with("file2.txt") && cl == "changelist1"));
        assert!(found_paths
            .iter()
            .any(|(p, cl)| p.ends_with("file3.txt") && cl == "changelist2"));

        // Get specific changelist
        let mut found_paths2 = Vec::new();
        fixture
            .ctx
            .get_changelists(
                fixture
                    .wc_path
                    .to_str()
                    .expect("path should be valid UTF-8"),
                Depth::Infinity,
                Some(&["changelist1"]),
                &mut |path, changelist| {
                    found_paths2.push((path.to_string(), changelist.to_string()));
                    Ok(())
                },
            )
            .unwrap();

        assert_eq!(found_paths2.len(), 2);
        assert!(found_paths2.iter().all(|(_, cl)| cl == "changelist1"));
    }

    #[test]
    fn test_export() {
        // Create a repository and working copy with some files
        let mut fixture = ClientTestFixture::new();
        fixture.add_file("file1.txt", "content 1");
        fixture.add_dir("subdir");
        // Add a file in the subdir manually since add_file only adds to wc root
        let file2 = fixture.wc_path.join("subdir/file2.txt");
        std::fs::write(&file2, "content 2").unwrap();
        fixture.ctx.add(&file2, &AddOptions::new()).unwrap();
        fixture.commit();

        let export_path = fixture.temp_dir.path().join("export");
        let wc_path_str = fixture.wc_path_str().to_string();

        // Test 1: Export from working copy path
        let export_opts = ExportOptions {
            peg_revision: crate::Revision::Head,
            revision: crate::Revision::Head,
            overwrite: false,
            ignore_externals: false,
            ignore_keywords: false,
            depth: crate::Depth::Infinity,
            native_eol: crate::NativeEOL::Standard,
        };

        let result = fixture.ctx.export(&wc_path_str, &export_path, &export_opts);
        assert!(
            result.is_ok(),
            "Export from WC should succeed: {:?}",
            result
        );

        // Verify exported files exist
        assert!(
            export_path.join("file1.txt").exists(),
            "file1.txt should be exported"
        );
        assert!(
            export_path.join("subdir").exists(),
            "subdir should be exported"
        );
        assert!(
            export_path.join("subdir/file2.txt").exists(),
            "file2.txt should be exported"
        );

        // Verify .svn directories are NOT exported
        assert!(
            !export_path.join(".svn").exists(),
            ".svn should not be exported"
        );
        assert!(
            !export_path.join("subdir/.svn").exists(),
            "subdir/.svn should not be exported"
        );

        // Verify content is correct
        let content1 = std::fs::read_to_string(export_path.join("file1.txt")).unwrap();
        assert_eq!(content1, "content 1");
        let content2 = std::fs::read_to_string(export_path.join("subdir/file2.txt")).unwrap();
        assert_eq!(content2, "content 2");

        // Test 2: Export with depth=Files (only files, not subdirectories)
        let export_path3 = fixture.temp_dir.path().join("export3");
        let shallow_opts = ExportOptions {
            peg_revision: crate::Revision::Head,
            revision: crate::Revision::Head,
            overwrite: false,
            ignore_externals: false,
            ignore_keywords: false,
            depth: crate::Depth::Files,
            native_eol: crate::NativeEOL::Standard,
        };

        let result = fixture
            .ctx
            .export(&wc_path_str, &export_path3, &shallow_opts);
        assert!(
            result.is_ok(),
            "Shallow export should succeed: {:?}",
            result
        );

        assert!(
            export_path3.join("file1.txt").exists(),
            "Top-level file should be exported"
        );

        // With depth=Files, only top-level files are exported, not subdirectories.
        assert!(
            !export_path3.join("subdir").exists(),
            "Subdirectory should not be exported with depth=Files"
        );
        assert!(
            !export_path3.join("subdir/file2.txt").exists(),
            "Files in subdirectories should not be exported with depth=Files"
        );
    }

    #[test]
    fn test_export_depth_infinity_honors_externals() {
        // Regression test: Depth::Infinity was being passed as its Rust
        // discriminant (5) rather than the C svn_depth_infinity value (2).
        // Since svn_client_export5 only processes externals when depth is
        // svn_depth_infinity, this caused ignore_externals=false to be
        // silently ignored.
        let mut fixture = ClientTestFixture::new();

        // Create content in the repo to serve as an external target.
        fixture.add_dir("ext-source");
        let ext_file = fixture.wc_path.join("ext-source/ext-file.txt");
        std::fs::write(&ext_file, "external content").unwrap();
        fixture.ctx.add(&ext_file, &AddOptions::new()).unwrap();
        fixture.commit();

        // Update the working copy so it is not out of date.
        let wc_path_str = fixture.wc_path_str().to_string();
        let update_opts = UpdateOptions {
            depth: crate::Depth::Infinity,
            depth_is_sticky: false,
            ignore_externals: true,
            allow_unver_obstructions: false,
            adds_as_modifications: false,
            make_parents: false,
        };
        fixture
            .ctx
            .update(&[&wc_path_str], crate::Revision::Head, &update_opts)
            .unwrap();

        // Set svn:externals on the root to pull ext-source as "ext-link".
        let externals_val = format!("{}/ext-source ext-link", fixture.url);
        fixture
            .ctx
            .propset(
                "svn:externals",
                Some(externals_val.as_bytes()),
                &wc_path_str,
                &PropSetOptions::default(),
            )
            .unwrap();
        fixture.commit();

        // Export from the repository URL with depth=Infinity and
        // ignore_externals=false so that externals should be fetched.
        let export_path = fixture.temp_dir.path().join("export-ext");
        let export_opts = ExportOptions {
            peg_revision: crate::Revision::Head,
            revision: crate::Revision::Head,
            overwrite: false,
            ignore_externals: false,
            ignore_keywords: false,
            depth: crate::Depth::Infinity,
            native_eol: crate::NativeEOL::Standard,
        };

        fixture
            .ctx
            .export(&fixture.url, &export_path, &export_opts)
            .unwrap();

        // The external should have been exported.
        assert!(
            export_path.join("ext-link").exists(),
            "External directory should be exported when depth=Infinity and ignore_externals=false"
        );
        assert_eq!(
            std::fs::read_to_string(export_path.join("ext-link/ext-file.txt")).unwrap(),
            "external content"
        );
    }

    #[test]
    fn test_import() {
        // Create a repository and a directory to import
        let mut fixture = ClientTestFixture::new();

        // Create some files to import
        let import_path = fixture.temp_dir.path().join("to_import");
        std::fs::create_dir(&import_path).unwrap();
        std::fs::write(import_path.join("file1.txt"), "import content 1").unwrap();
        let subdir = import_path.join("subdir");
        std::fs::create_dir(&subdir).unwrap();
        std::fs::write(subdir.join("file2.txt"), "import content 2").unwrap();

        // Test 1: Import the directory
        let import_url_str = format!("{}/imported", fixture.url);
        let import_url = crate::uri::Uri::new(&import_url_str).unwrap();
        let canonical_url = import_url.canonical();

        let mut import_opts = ImportOptions {
            depth: crate::Depth::Infinity,
            no_ignore: false,
            no_autoprops: false,
            ignore_unknown_node_types: false,
            revprop_table: None,
            filter_callback: None,
            commit_callback: None,
        };

        let result = fixture
            .ctx
            .import(&import_path, canonical_url.as_str(), &mut import_opts);
        assert!(result.is_ok(), "Import should succeed: {:?}", result);

        // Test 2: Update working copy to verify the import worked
        let wc_path_str = fixture.wc_path_str().to_string();
        fixture
            .ctx
            .update(
                &[&wc_path_str],
                crate::Revision::Head,
                &UpdateOptions::default(),
            )
            .unwrap();

        // Verify imported files exist in the checked-out working copy
        assert!(
            fixture.wc_path.join("imported/file1.txt").exists(),
            "Imported file1.txt should exist"
        );
        assert!(
            fixture.wc_path.join("imported/subdir").exists(),
            "Imported subdir should exist"
        );
        assert!(
            fixture.wc_path.join("imported/subdir/file2.txt").exists(),
            "Imported file2.txt should exist"
        );

        // Verify content
        let content1 = std::fs::read_to_string(fixture.wc_path.join("imported/file1.txt")).unwrap();
        assert_eq!(content1, "import content 1");
        let content2 =
            std::fs::read_to_string(fixture.wc_path.join("imported/subdir/file2.txt")).unwrap();
        assert_eq!(content2, "import content 2");

        // Test 3: Import with depth=Empty (only the directory itself, no children)
        let import_path2 = fixture.temp_dir.path().join("to_import2");
        std::fs::create_dir(&import_path2).unwrap();
        std::fs::write(import_path2.join("file3.txt"), "content 3").unwrap();
        let subdir2 = import_path2.join("subdir2");
        std::fs::create_dir(&subdir2).unwrap();
        std::fs::write(subdir2.join("file4.txt"), "content 4").unwrap();

        let import_url2_str = format!("{}/imported2", fixture.url);
        let import_url2 = crate::uri::Uri::new(&import_url2_str).unwrap();
        let canonical_url2 = import_url2.canonical();
        let mut import_opts2 = ImportOptions {
            depth: crate::Depth::Empty,
            no_ignore: false,
            no_autoprops: false,
            ignore_unknown_node_types: false,
            revprop_table: None,
            filter_callback: None,
            commit_callback: None,
        };

        let result = fixture
            .ctx
            .import(&import_path2, canonical_url2.as_str(), &mut import_opts2);
        assert!(
            result.is_ok(),
            "Import with depth=Empty should succeed: {:?}",
            result
        );

        // Update to get the new content
        fixture
            .ctx
            .update(
                &[&wc_path_str],
                crate::Revision::Head,
                &UpdateOptions::default(),
            )
            .unwrap();

        // The imported2 directory should exist but be empty
        assert!(
            fixture.wc_path.join("imported2").exists(),
            "imported2 directory should exist"
        );
        assert!(
            !fixture.wc_path.join("imported2/file3.txt").exists(),
            "file3.txt should NOT be imported with depth=Empty"
        );
        assert!(
            !fixture.wc_path.join("imported2/subdir2").exists(),
            "subdir2 should NOT be imported with depth=Empty"
        );
    }

    #[test]
    fn test_lock_unlock() {
        let mut fixture = ClientTestFixture::new();

        // Set up authentication with a username for lock/unlock operations
        std::env::set_var("USER", "testuser");
        let username_provider = crate::auth::get_username_provider();
        let mut auth_baton = crate::auth::AuthBaton::open(vec![username_provider]).unwrap();
        fixture.ctx.set_auth(&mut auth_baton);

        // Create a file and commit it
        let file1 = fixture.add_file("file1.txt", "lockable content");
        fixture.commit();

        // Test 1: Lock the file
        let file1_path = file1.to_str().expect("path should be valid UTF-8");
        let result = fixture
            .ctx
            .lock(&[file1_path], "Locking for testing", false);
        assert!(result.is_ok(), "Lock should succeed: {:?}", result);

        // Test 2: Lock again with same context (SVN allows re-locking files you own)
        let result = fixture
            .ctx
            .lock(&[file1_path], "Re-locking owned file", false);
        assert!(
            result.is_ok(),
            "Re-locking owned file should succeed: {:?}",
            result
        );

        // Test 3: Lock with steal_lock=true (should also succeed)
        let result = fixture.ctx.lock(&[file1_path], "Locking with steal", true);
        assert!(
            result.is_ok(),
            "Lock with steal should succeed: {:?}",
            result
        );

        // Test 4: Unlock the file
        let result = fixture.ctx.unlock(&[file1_path], false);
        assert!(result.is_ok(), "Unlock should succeed: {:?}", result);

        // Test 5: Try to unlock again (should fail - no lock to break)
        let result = fixture.ctx.unlock(&[file1_path], false);
        assert!(
            result.is_err(),
            "Unlocking already unlocked file should fail"
        );

        // Test 6: Lock again after unlocking
        let result = fixture
            .ctx
            .lock(&[file1_path], "Locking after unlock", false);
        assert!(
            result.is_ok(),
            "Lock after unlock should succeed: {:?}",
            result
        );

        // Test 7: Unlock with break_lock
        let result = fixture.ctx.unlock(&[file1_path], true);
        assert!(
            result.is_ok(),
            "Unlock with break_lock should succeed: {:?}",
            result
        );
    }

    #[test]
    fn test_conflict_walk_no_conflicts() {
        let mut fixture = ClientTestFixture::new();

        // Walk conflicts - should find none
        let mut count = 0;
        fixture
            .ctx
            .conflict_walk(&fixture.wc_path, Depth::Infinity, |_conflict| {
                count += 1;
                Ok(())
            })
            .unwrap();

        assert_eq!(count, 0);
    }

    #[test]
    fn test_conflict_get_no_conflict() {
        let mut fixture = ClientTestFixture::new();
        let test_file = fixture.add_file("test.txt", "content");

        // Get conflict for non-conflicted file - should return conflict with no conflicts
        let conflict = fixture.ctx.conflict_get(&test_file).unwrap();
        let (text, props, tree) = conflict.get_conflicted().unwrap();
        assert!(!text);
        assert!(props.is_empty());
        assert!(!tree);
    }

    #[test]
    fn test_conflict_methods_no_conflict() {
        let mut fixture = ClientTestFixture::new();
        let test_file = fixture.add_file("test.txt", "content");

        let conflict = fixture.ctx.conflict_get(&test_file).unwrap();

        // Test various conflict methods on non-conflicted file
        let abspath = conflict.get_local_abspath();
        assert!(abspath.ends_with("test.txt"));

        // For non-conflicted files, there's no conflict data
        let (text, props, tree) = conflict.get_conflicted().unwrap();
        assert!(!text);
        assert!(props.is_empty());
        assert!(!tree);
    }

    #[test]
    fn test_conflict_with_text_conflict() {
        let mut fixture = ClientTestFixture::new();

        // Create and commit a file in wc1 (fixture's wc)
        let test_file1 = fixture.add_file("test.txt", "line1\nline2\nline3\n");
        fixture.commit();

        // Checkout second working copy
        let wc2_path = fixture.temp_dir.path().join("wc2");
        let url = crate::uri::Uri::new(&fixture.url).unwrap();
        fixture
            .ctx
            .checkout(
                url,
                &wc2_path,
                &ClientTestFixture::default_checkout_options(),
            )
            .unwrap();

        // Modify file in wc1 and commit
        std::fs::write(&test_file1, "line1\nmodified by wc1\nline3\n").unwrap();
        fixture.commit();

        // Modify same file in wc2 (different change on same line)
        let test_file2 = wc2_path.join("test.txt");
        std::fs::write(&test_file2, "line1\nmodified by wc2\nline3\n").unwrap();

        // Update wc2 - should create a conflict
        let wc2_str = wc2_path.to_str().expect("path should be valid UTF-8");
        fixture
            .ctx
            .update(&[wc2_str], Revision::Head, &UpdateOptions::default())
            .unwrap();

        // Get conflict for the file directly
        let conflict = fixture.ctx.conflict_get(&test_file2).unwrap();

        // Test conflict methods
        let (text, _props, _tree) = conflict.get_conflicted().unwrap();
        assert!(text, "Should have text conflict");

        let abspath = conflict.get_local_abspath();
        assert!(abspath.ends_with("test.txt"));

        // text_get_contents should return paths
        let contents = conflict.text_get_contents().unwrap();
        // At least some should be present
        assert!(
            contents.0.is_some()
                || contents.1.is_some()
                || contents.2.is_some()
                || contents.3.is_some(),
            "Should have at least one content file"
        );

        // Test resolution methods on actual conflict
        let text_res = conflict.text_get_resolution();
        // Initially unresolved
        assert_eq!(text_res, crate::ClientConflictOptionId::Unspecified);
    }

    #[test]
    fn test_text_conflict_resolution() {
        let mut fixture = ClientTestFixture::new();

        // Create and commit a file in wc1 (fixture's wc)
        let test_file1 = fixture.add_file("test.txt", "line1\nline2\nline3\n");
        fixture.commit();

        // Checkout second working copy
        let wc2_path = fixture.temp_dir.path().join("wc2");
        let url = crate::uri::Uri::new(&fixture.url).unwrap();
        fixture
            .ctx
            .checkout(
                url,
                &wc2_path,
                &ClientTestFixture::default_checkout_options(),
            )
            .unwrap();

        // Modify file in wc1 and commit
        std::fs::write(&test_file1, "line1\nmodified by wc1\nline3\n").unwrap();
        fixture.commit();

        // Modify same file in wc2 (different change on same line)
        let test_file2 = wc2_path.join("test.txt");
        std::fs::write(&test_file2, "line1\nmodified by wc2\nline3\n").unwrap();

        // Update wc2 - should create a conflict
        let wc2_str = wc2_path.to_str().expect("path should be valid UTF-8");
        fixture
            .ctx
            .update(&[wc2_str], Revision::Head, &UpdateOptions::default())
            .unwrap();

        // Get conflict for the file
        let mut conflict = fixture.ctx.conflict_get(&test_file2).unwrap();

        // Test text_get_resolution_options and find_by_id
        let options = conflict
            .text_get_resolution_options(&mut fixture.ctx)
            .unwrap();
        assert!(!options.is_empty(), "Should have resolution options");

        // Test find_by_id
        let base_option =
            ConflictOption::find_by_id(&options, crate::ClientConflictOptionId::MergedText);
        assert!(base_option.is_some(), "Should find merged text option");

        // Test text_resolve_by_id
        conflict
            .text_resolve_by_id(crate::TextConflictChoice::Merged, &mut fixture.ctx)
            .unwrap();

        // Verify resolution was applied
        let text_res = conflict.text_get_resolution();
        assert_eq!(text_res, crate::ClientConflictOptionId::MergedText);
    }

    #[test]
    fn test_conflict_with_prop_conflict() {
        let mut fixture = ClientTestFixture::new();

        // Create and commit a file with property in wc1 (fixture's wc)
        let test_file1 = fixture.add_file("test.txt", "content\n");
        fixture
            .ctx
            .propset(
                "svn:custom",
                Some(b"value1"),
                test_file1.to_str().expect("path should be valid UTF-8"),
                &PropSetOptions::default(),
            )
            .unwrap();
        fixture.commit();

        // Checkout second working copy
        let wc2_path = fixture.temp_dir.path().join("wc2");
        let url = crate::uri::Uri::new(&fixture.url).unwrap();
        fixture
            .ctx
            .checkout(
                url,
                &wc2_path,
                &ClientTestFixture::default_checkout_options(),
            )
            .unwrap();

        // Change property in wc1 and commit
        fixture
            .ctx
            .propset(
                "svn:custom",
                Some(b"value_from_wc1"),
                test_file1.to_str().expect("path should be valid UTF-8"),
                &PropSetOptions::default(),
            )
            .unwrap();
        fixture.commit();

        // Change same property in wc2 (different value)
        let test_file2 = wc2_path.join("test.txt");
        fixture
            .ctx
            .propset(
                "svn:custom",
                Some(b"value_from_wc2"),
                test_file2.to_str().expect("path should be valid UTF-8"),
                &PropSetOptions::default(),
            )
            .unwrap();

        // Update wc2 - should create a property conflict
        let wc2_str = wc2_path.to_str().expect("path should be valid UTF-8");
        fixture
            .ctx
            .update(&[wc2_str], Revision::Head, &UpdateOptions::default())
            .unwrap();

        // Get conflict for the file
        let conflict = fixture.ctx.conflict_get(&test_file2).unwrap();

        // Check for property conflict
        let (text, props, _tree) = conflict.get_conflicted().unwrap();
        assert!(!text, "Should not have text conflict");
        assert!(!props.is_empty(), "Should have property conflict");

        // prop_get_reject_abspath should return a path now
        let reject_path = conflict.prop_get_reject_abspath();
        assert!(reject_path.is_some(), "Should have reject file path");
    }

    #[test]
    fn test_prop_conflict_resolution() {
        let mut fixture = ClientTestFixture::new();

        // Create and commit a file with property in wc1 (fixture's wc)
        let test_file1 = fixture.add_file("test.txt", "content\n");
        fixture
            .ctx
            .propset(
                "svn:custom",
                Some(b"initial"),
                test_file1.to_str().expect("path should be valid UTF-8"),
                &PropSetOptions::default(),
            )
            .unwrap();
        fixture.commit();

        // Checkout second working copy
        let wc2_path = fixture.temp_dir.path().join("wc2");
        let url = crate::uri::Uri::new(&fixture.url).unwrap();
        fixture
            .ctx
            .checkout(
                url,
                &wc2_path,
                &ClientTestFixture::default_checkout_options(),
            )
            .unwrap();

        // Change property in wc1 and commit
        fixture
            .ctx
            .propset(
                "svn:custom",
                Some(b"from_wc1"),
                test_file1.to_str().expect("path should be valid UTF-8"),
                &PropSetOptions::default(),
            )
            .unwrap();
        fixture.commit();

        // Change same property in wc2 (different value)
        let test_file2 = wc2_path.join("test.txt");
        fixture
            .ctx
            .propset(
                "svn:custom",
                Some(b"from_wc2"),
                test_file2.to_str().expect("path should be valid UTF-8"),
                &PropSetOptions::default(),
            )
            .unwrap();

        // Update wc2 - should create a property conflict
        let wc2_str = wc2_path.to_str().expect("path should be valid UTF-8");
        let _ = fixture
            .ctx
            .update(&[wc2_str], Revision::Head, &UpdateOptions::default());

        // Get conflict for the file
        let mut conflict = fixture.ctx.conflict_get(&test_file2).unwrap();

        // Test prop_get_propvals
        let propvals = conflict.prop_get_propvals("svn:custom").unwrap();
        assert!(propvals.1.is_some(), "Should have working propval");
        assert!(propvals.3.is_some(), "Should have incoming new propval");

        // Test prop_get_resolution_options
        let options = conflict
            .prop_get_resolution_options(&mut fixture.ctx)
            .unwrap();
        assert!(!options.is_empty(), "Should have resolution options");

        // Test prop_resolve_by_id
        conflict
            .prop_resolve_by_id(
                "svn:custom",
                crate::TextConflictChoice::MineConflict,
                &mut fixture.ctx,
            )
            .unwrap();

        // Verify resolution was applied
        let prop_res = conflict.prop_get_resolution("svn:custom").unwrap();
        assert_eq!(
            prop_res,
            crate::ClientConflictOptionId::WorkingTextWhereConflicted
        );
    }

    #[test]
    fn test_tree_conflict_with_move() {
        let mut fixture = ClientTestFixture::new();

        // Create a file and commit in wc1 (fixture's wc)
        let file1 = fixture.add_file("file.txt", "content\n");
        fixture.commit();

        // Checkout second working copy
        let wc2_path = fixture.temp_dir.path().join("wc2");
        let url = crate::uri::Uri::new(&fixture.url).unwrap();
        fixture
            .ctx
            .checkout(
                url,
                &wc2_path,
                &ClientTestFixture::default_checkout_options(),
            )
            .unwrap();

        // In wc1: move the file
        let file1_moved = fixture.wc_path.join("file_moved.txt");
        fixture
            .ctx
            .move_path(
                &[file1.to_str().expect("path should be valid UTF-8")],
                file1_moved.to_str().expect("path should be valid UTF-8"),
                &mut MoveOptions::default(),
            )
            .unwrap();
        fixture.commit();

        // In wc2: modify the file before updating
        let file2 = wc2_path.join("file.txt");
        std::fs::write(&file2, "modified content\n").unwrap();

        // Update wc2 - should create a tree conflict (incoming delete vs local edit)
        let wc2_str = wc2_path.to_str().expect("path should be valid UTF-8");
        let _ = fixture
            .ctx
            .update(&[wc2_str], Revision::Head, &UpdateOptions::default());

        // Get conflict for the file - this should exist after update with local mods
        let mut conflict = fixture.ctx.conflict_get(&file2).unwrap();

        // Check for tree conflict
        let (_text, _props, tree) = conflict.get_conflicted().unwrap();
        assert!(
            tree,
            "Should have tree conflict after deleting file with local modifications"
        );

        // Get tree conflict resolution options
        let options = conflict
            .tree_get_resolution_options(&mut fixture.ctx)
            .unwrap();
        assert!(
            !options.is_empty(),
            "Should have tree conflict resolution options"
        );

        // Verify we can get tree conflict details (this contacts the repository for more info)
        conflict.tree_get_details(&mut fixture.ctx).unwrap();

        // Try to find an option that supports move targets
        let mut has_move_option = false;
        for opt in &options {
            // Try to get move candidates - if this succeeds and returns data, we can test further
            if let Ok(candidates) = opt.get_moved_to_abspath_candidates() {
                if !candidates.is_empty() {
                    has_move_option = true;

                    // Test get_moved_to_repos_relpath_candidates
                    let relpath_candidates = opt.get_moved_to_repos_relpath_candidates().unwrap();
                    assert!(
                        !relpath_candidates.is_empty(),
                        "Should have repository relpath candidates"
                    );

                    // Create a mutable option to test set methods
                    let mut mutable_opt = ConflictOption {
                        ptr: opt.ptr,
                        merged_value_pool: None,
                    };

                    // Test set_moved_to_abspath (use index 0)
                    mutable_opt
                        .set_moved_to_abspath(0, &mut fixture.ctx)
                        .unwrap();

                    // Test set_moved_to_repos_relpath (use index 0)
                    // Note: Create a fresh option since set_moved_to_abspath modified the previous one
                    let mut mutable_opt2 = ConflictOption {
                        ptr: opt.ptr,
                        merged_value_pool: None,
                    };
                    mutable_opt2
                        .set_moved_to_repos_relpath(0, &mut fixture.ctx)
                        .unwrap();
                    break;
                }
            }
        }

        // At minimum, we should be able to resolve the tree conflict with one of the options
        // Even if move-specific options aren't available, we can use a basic resolution
        let first_option = &options[0];
        conflict
            .tree_resolve(first_option, &mut fixture.ctx)
            .unwrap();

        // If we found move options, verify that
        if !has_move_option {
            // The APIs were still tested, just not with move-specific options
            // This is acceptable as long as basic tree conflict resolution works
        }
    }

    #[test]
    fn test_merge_peg() {
        let mut fixture = ClientTestFixture::new();

        // Create a file in trunk and commit (r1)
        let trunk_file = fixture.add_file("file.txt", "line 1\n");
        fixture.commit();

        // Create a branch using copy (r2)
        let branch_url = format!("{}/branch", fixture.url);
        fixture
            .ctx
            .copy(
                &[(fixture.url.as_str(), Some(Revision::Head))],
                &branch_url,
                &mut CopyOptions::new(),
            )
            .unwrap();

        // Update the working copy to get the branch
        let wc_path_str = fixture.wc_path_str().to_string();
        let trunk_url = fixture.url.clone();
        fixture
            .ctx
            .update(&[&wc_path_str], Revision::Head, &UpdateOptions::default())
            .unwrap();

        // Switch to the branch
        fixture
            .ctx
            .switch(
                wc_path_str.as_str(),
                branch_url.as_str(),
                &SwitchOptions::default(),
            )
            .unwrap();

        // Make changes in the branch and commit (r3)
        std::fs::write(&trunk_file, "line 1\nline 2 from branch\n").unwrap();
        fixture.commit();

        // Switch back to trunk
        fixture
            .ctx
            .switch(
                wc_path_str.as_str(),
                trunk_url.as_str(),
                &SwitchOptions::default(),
            )
            .unwrap();

        // Now merge from branch to trunk using merge_peg
        let merge_opts = MergeSourcesOptions {
            ignore_mergeinfo: false,
            diff_ignore_ancestry: false,
            force_delete: false,
            record_only: false,
            dry_run: false,
            allow_mixed_rev: true,
            merge_options: None,
        };

        // Empty ranges means automatic merge - SVN figures out what to merge based on mergeinfo
        fixture
            .ctx
            .merge_peg(
                &branch_url,
                &[],
                &Revision::Head,
                &wc_path_str,
                Depth::Infinity,
                &merge_opts,
            )
            .unwrap();

        // Verify the merge worked
        let content = std::fs::read_to_string(&trunk_file).unwrap();
        assert!(
            content.contains("line 2 from branch"),
            "Merge should have brought in branch changes"
        );
    }

    #[test]
    fn test_conflict_option_set_merged_propval() {
        let mut fixture = ClientTestFixture::new();

        // Create and commit a file with property in wc1 (fixture's wc)
        let test_file1 = fixture.add_file("test.txt", "content\n");
        fixture
            .ctx
            .propset(
                "custom:prop",
                Some(b"value1"),
                test_file1.to_str().expect("path should be valid UTF-8"),
                &PropSetOptions::default(),
            )
            .unwrap();
        fixture.commit();

        // Checkout second working copy (use canonicalized path for conflict resolution)
        let wc2_path = fixture.temp_dir.path().canonicalize().unwrap().join("wc2");
        let url = crate::uri::Uri::new(&fixture.url).unwrap();
        fixture
            .ctx
            .checkout(
                url,
                &wc2_path,
                &ClientTestFixture::default_checkout_options(),
            )
            .unwrap();

        // Change property in wc1 and commit
        fixture
            .ctx
            .propset(
                "custom:prop",
                Some(b"value_from_wc1"),
                test_file1.to_str().expect("path should be valid UTF-8"),
                &PropSetOptions::default(),
            )
            .unwrap();
        fixture.commit();

        // Change same property in wc2 (different value)
        let test_file2 = wc2_path.join("test.txt");
        fixture
            .ctx
            .propset(
                "custom:prop",
                Some(b"value_from_wc2"),
                test_file2.to_str().expect("path should be valid UTF-8"),
                &PropSetOptions::default(),
            )
            .unwrap();

        // Update wc2 - should create a property conflict
        let wc2_str = wc2_path.to_str().expect("path should be valid UTF-8");
        let _ = fixture
            .ctx
            .update(&[wc2_str], Revision::Head, &UpdateOptions::default());

        // Get conflict for the file - use canonical path for conflict_get
        let test_file2_abs = test_file2.canonicalize().unwrap();
        let mut conflict = fixture.ctx.conflict_get(&test_file2_abs).unwrap();

        // Get property conflict resolution options
        let mut options = conflict
            .prop_get_resolution_options(&mut fixture.ctx)
            .unwrap();
        assert!(!options.is_empty(), "Should have resolution options");

        // Find the merged option
        let merged_option = options
            .iter_mut()
            .find(|opt| opt.get_id() == crate::ClientConflictOptionId::MergedText);

        if let Some(option) = merged_option {
            // Test set_merged_propval - set a custom merged value
            option.set_merged_propval(Some(b"custom_merged_value"));

            // Resolve the conflict using the customized option
            conflict
                .prop_resolve_with_option("custom:prop", option, &mut fixture.ctx)
                .unwrap();

            // Verify the merged value was applied by reading the property
            let mut found_prop = None;
            fixture
                .ctx
                .proplist(
                    test_file2_abs.to_str().expect("path should be valid UTF-8"),
                    &ProplistOptions {
                        peg_revision: Revision::Working,
                        revision: Revision::Working,
                        depth: Depth::Empty,
                        changelists: None,
                        get_target_inherited_props: false,
                    },
                    &mut |_path, props, _inherited| {
                        if let Some(val) = props.get("custom:prop") {
                            found_prop = Some(val.clone());
                        }
                        Ok(())
                    },
                )
                .unwrap();

            assert_eq!(
                found_prop.as_deref(),
                Some(b"custom_merged_value" as &[u8]),
                "Merged property value should be applied"
            );
        }
    }

    #[test]
    fn test_mergeinfo_get_merged() {
        let mut fixture = ClientTestFixture::new();

        // Create a file in trunk and commit
        fixture.add_file("test.txt", "initial content\n");
        fixture.commit();

        // Create a branch
        let branch_url = format!("{}/branch", fixture.url);
        fixture
            .ctx
            .copy(
                &[(fixture.url.as_str(), Some(Revision::Head))],
                &branch_url,
                &mut CopyOptions::new(),
            )
            .unwrap();

        // Checkout the branch
        let branch_wc = fixture.temp_dir.path().join("branch");
        let branch_url_obj = crate::uri::Uri::new(&branch_url).unwrap();
        fixture
            .ctx
            .checkout(
                branch_url_obj.clone(),
                &branch_wc,
                &ClientTestFixture::default_checkout_options(),
            )
            .unwrap();

        // Make a change in the branch - add a new file to avoid conflicts
        let branch_file2 = branch_wc.join("branch_file.txt");
        std::fs::write(&branch_file2, "new file from branch\n").unwrap();
        fixture.ctx.add(&branch_file2, &AddOptions::new()).unwrap();
        fixture
            .ctx
            .commit(
                &[branch_wc.to_str().expect("path should be valid UTF-8")],
                &CommitOptions::default(),
                std::collections::HashMap::new(),
                None,
                &mut |_| Ok(()),
            )
            .unwrap();

        // Update trunk before merging
        let wc_path_str = fixture.wc_path_str().to_string();
        fixture
            .ctx
            .update(&[&wc_path_str], Revision::Head, &UpdateOptions::default())
            .unwrap();

        // Use record-only merge to avoid conflicts - this only records mergeinfo
        fixture
            .ctx
            .merge_peg(
                branch_url_obj.as_str(),
                &[], // Empty ranges for automatic merge
                &Revision::Head,
                &wc_path_str,
                Depth::Infinity,
                &MergeSourcesOptions {
                    ignore_mergeinfo: false,
                    diff_ignore_ancestry: false,
                    force_delete: false,
                    record_only: true, // Record-only merge avoids conflicts
                    dry_run: false,
                    allow_mixed_rev: true,
                    merge_options: None,
                },
            )
            .unwrap();

        // Commit the merge
        fixture.commit();

        // Now test mergeinfo_get_merged
        let trunk_url = fixture.url.clone();
        let mergeinfo = fixture
            .ctx
            .mergeinfo_get_merged(&trunk_url, &Revision::Head)
            .unwrap();

        // After merging, we should have mergeinfo
        assert!(mergeinfo.is_some(), "Should have mergeinfo after merge");

        if let Some(mi) = mergeinfo {
            // The mergeinfo should contain the branch URL as a key
            let entries = mi.paths();
            assert!(!entries.is_empty(), "Mergeinfo should have entries");

            // Check that we can find the branch in the mergeinfo
            let has_branch = entries.keys().any(|k| k.contains("branch"));
            assert!(has_branch, "Mergeinfo should contain the branch URL");
        }

        // Test on a path with no mergeinfo
        let test_file_url = format!("{}/test.txt", trunk_url);
        let mergeinfo2 = fixture
            .ctx
            .mergeinfo_get_merged(&test_file_url, &Revision::Head)
            .unwrap();

        // Files may or may not inherit mergeinfo from their parent
        drop(mergeinfo2);
    }

    #[test]
    fn test_merge() {
        let mut fixture = ClientTestFixture::new();

        // Create a file in trunk and commit
        fixture.add_file("test.txt", "line 1\n");
        fixture.commit();

        let trunk_url = fixture.url.clone();

        // Create branch1
        let branch1_url = format!("{}/branch1", fixture.url);
        fixture
            .ctx
            .copy(
                &[(trunk_url.as_str(), Some(Revision::Head))],
                &branch1_url,
                &mut CopyOptions::new(),
            )
            .unwrap();

        // Checkout branch1
        let branch1_wc = fixture.temp_dir.path().join("branch1");
        fixture
            .ctx
            .checkout(
                crate::uri::Uri::new(&branch1_url).unwrap(),
                &branch1_wc,
                &ClientTestFixture::default_checkout_options(),
            )
            .unwrap();

        // Make a change in branch1
        let branch1_file = branch1_wc.join("test.txt");
        std::fs::write(&branch1_file, "line 1\nline 2 from branch1\n").unwrap();

        fixture
            .ctx
            .commit(
                &[branch1_wc.to_str().unwrap()],
                &CommitOptions::default(),
                std::collections::HashMap::new(),
                None,
                &mut |_| Ok(()),
            )
            .unwrap();

        // Create branch2
        let branch2_url = format!("{}/branch2", trunk_url);
        fixture
            .ctx
            .copy(
                &[(trunk_url.as_str(), Some(Revision::Head))],
                &branch2_url,
                &mut CopyOptions::new(),
            )
            .unwrap();

        // Checkout branch2
        let branch2_wc = fixture.temp_dir.path().join("branch2");
        fixture
            .ctx
            .checkout(
                branch2_url.as_str(),
                &branch2_wc,
                &ClientTestFixture::default_checkout_options(),
            )
            .unwrap();

        // Test merge() - merge changes between branch1 and trunk into branch2
        // This uses the non-peg variant which takes two sources
        fixture
            .ctx
            .merge(
                trunk_url.as_str(),           // source1 (trunk)
                &Revision::Head,              // revision1
                &branch1_url,                 // source2 (branch1)
                &Revision::Head,              // revision2
                branch2_wc.to_str().unwrap(), // target
                Depth::Infinity,
                &MergeSourcesOptions {
                    ignore_mergeinfo: false,
                    diff_ignore_ancestry: false,
                    force_delete: false,
                    record_only: false,
                    dry_run: false,
                    allow_mixed_rev: true,
                    merge_options: None,
                },
            )
            .unwrap();

        // Verify the file was updated
        let branch2_file = branch2_wc.join("test.txt");
        let content = std::fs::read_to_string(&branch2_file).unwrap();
        assert!(
            content.contains("line 2 from branch1"),
            "Merge should have brought in changes from branch1"
        );
    }

    #[test]
    fn test_get_merging_summary() {
        let mut fixture = ClientTestFixture::new();

        // Create a file in trunk and commit
        fixture.add_file("test.txt", "line 1\n");
        fixture.commit();

        let trunk_url = fixture.url.clone();

        // Create a branch
        let branch_url = format!("{}/branch", trunk_url);
        fixture
            .ctx
            .copy(
                &[(trunk_url.as_str(), Some(Revision::Head))],
                &branch_url,
                &mut CopyOptions::new(),
            )
            .unwrap();

        // Checkout the branch
        let branch_wc = fixture.temp_dir.path().join("branch");
        fixture
            .ctx
            .checkout(
                branch_url.as_str(),
                &branch_wc,
                &ClientTestFixture::default_checkout_options(),
            )
            .unwrap();

        // Make a change in the branch and commit
        let branch_file = branch_wc.join("test.txt");
        std::fs::write(&branch_file, "line 1\nline 2 from branch\n").unwrap();

        fixture
            .ctx
            .commit(
                &[branch_wc.to_str().unwrap()],
                &CommitOptions::default(),
                std::collections::HashMap::new(),
                None,
                &mut |_| Ok(()),
            )
            .unwrap();

        // Test get_merging_summary
        let wc_path_str = fixture.wc_path_str().to_string();
        let (
            needs_reintegration,
            yca_url,
            yca_rev,
            _base_url,
            base_rev,
            right_url,
            right_rev,
            _target_url,
            target_rev,
            repos_root,
        ) = fixture
            .ctx
            .get_merging_summary(
                &branch_url,
                &Revision::Head,
                &wc_path_str,
                &Revision::Working,
            )
            .unwrap();

        // Verify we got valid data
        assert!(!repos_root.is_empty(), "Should have repository root URL");
        assert!(
            repos_root.starts_with("file://"),
            "Repository root should be a file:// URL"
        );

        // YCA URL should be valid
        assert!(!yca_url.is_empty(), "Should have YCA URL");
        assert!(
            yca_url.starts_with(&fixture.url),
            "YCA URL should reference the repository"
        );

        // Right URL should reference the branch
        assert!(!right_url.is_empty(), "Should have right URL");
        assert!(
            right_url.contains("branch"),
            "Right URL should reference the branch"
        );

        // Revisions should be valid
        assert!(yca_rev >= 0, "YCA revision should be valid");
        assert!(base_rev >= 0, "Base revision should be valid");
        assert!(right_rev > 0, "Right revision should be > 0");
        assert!(target_rev >= 0, "Target revision should be valid");

        // For a fresh branch that hasn't been merged yet, needs_reintegration should be false
        assert!(
            !needs_reintegration,
            "Fresh branch should not need reintegration"
        );
    }

    #[test]
    fn test_conflict_text_get_contents() {
        // This test creates an actual text conflict and validates Conflict::text_get_contents
        let mut fixture = ClientTestFixture::new();

        // Create and commit a file in wc1 (fixture's wc)
        let file1 = fixture.add_file("test.txt", "Line 1\nLine 2\nLine 3\n");
        fixture.commit();

        // Checkout second working copy
        let wc2 = fixture.temp_dir.path().join("wc2");
        let url = crate::uri::Uri::new(&fixture.url).unwrap();
        fixture
            .ctx
            .checkout(url, &wc2, &ClientTestFixture::default_checkout_options())
            .unwrap();

        // Modify in wc1 and commit
        std::fs::write(&file1, "Line 1\nModified by WC1\nLine 3\n").unwrap();
        let wc1_str = fixture.wc_path_str().to_string();
        fixture
            .ctx
            .commit(
                &[&wc1_str],
                &CommitOptions::default(),
                std::collections::HashMap::new(),
                None,
                &mut |_| Ok(()),
            )
            .unwrap();

        // Modify same file in wc2
        let file2 = wc2.join("test.txt");
        std::fs::write(&file2, "Line 1\nModified by WC2\nLine 3\n").unwrap();

        // Update wc2 - creates conflict
        let _ = fixture.ctx.update(
            &[wc2.to_str().unwrap()],
            Revision::Head,
            &UpdateOptions::default(),
        );

        // Get the conflict
        let conflict = fixture.ctx.conflict_get(&file2).unwrap();

        // Test text_get_contents - this returns file PATHS, not content
        let (base_path, working_path, incoming_old, incoming_new_path) =
            conflict.text_get_contents().unwrap();

        // Verify we get actual paths, not None or wrong values
        assert!(base_path.is_some(), "Base path should be Some");
        assert!(working_path.is_some(), "Working path should be Some");
        assert!(
            incoming_new_path.is_some(),
            "Incoming new path should be Some"
        );

        let base = base_path.unwrap();
        let working = working_path.unwrap();
        let incoming_new = incoming_new_path.unwrap();

        // Verify paths are not empty strings or "xyzzy" placeholder
        assert_ne!(base, "", "Base path should not be empty");
        assert_ne!(base, "xyzzy", "Base path should not be placeholder");
        assert_ne!(working, "", "Working path should not be empty");
        assert_ne!(working, "xyzzy", "Working path should not be placeholder");
        assert_ne!(incoming_new, "", "Incoming path should not be empty");
        assert_ne!(
            incoming_new, "xyzzy",
            "Incoming path should not be placeholder"
        );

        // Verify files actually exist and have content
        assert!(
            std::path::Path::new(&base).exists(),
            "Base file should exist at path: {}",
            base
        );
        assert!(
            std::path::Path::new(&working).exists(),
            "Working file should exist at path: {}",
            working
        );
        assert!(
            std::path::Path::new(&incoming_new).exists(),
            "Incoming file should exist at path: {}",
            incoming_new
        );

        // Verify file contents match expected
        let base_content = std::fs::read_to_string(&base).unwrap();
        let working_content = std::fs::read_to_string(&working).unwrap();
        let incoming_content = std::fs::read_to_string(&incoming_new).unwrap();

        assert_eq!(
            base_content, "Line 1\nLine 2\nLine 3\n",
            "Base content should match original"
        );
        assert_eq!(
            working_content, "Line 1\nModified by WC2\nLine 3\n",
            "Working content should match WC2"
        );
        assert_eq!(
            incoming_content, "Line 1\nModified by WC1\nLine 3\n",
            "Incoming content should match WC1"
        );

        // Verify incoming_old exists (should be the base for the incoming change)
        assert!(
            incoming_old.is_some(),
            "Incoming old should exist for text conflicts"
        );
        let incoming_old_path = incoming_old.unwrap();
        assert_ne!(
            incoming_old_path, "",
            "Incoming old path should not be empty"
        );
        assert_ne!(
            incoming_old_path, "xyzzy",
            "Incoming old path should not be placeholder"
        );

        // Verify incoming_old file exists and has the base content
        assert!(
            std::path::Path::new(&incoming_old_path).exists(),
            "Incoming old file should exist"
        );
        let incoming_old_content = std::fs::read_to_string(&incoming_old_path).unwrap();
        assert_eq!(
            incoming_old_content, "Line 1\nLine 2\nLine 3\n",
            "Incoming old should be the base revision"
        );
    }

    #[test]
    fn test_conflict_option_get_description() {
        // Test that ConflictOption::get_description returns non-empty, non-placeholder strings
        let mut fixture = ClientTestFixture::new();

        // Create and commit initial file in wc1 (fixture's wc)
        let test_file1 = fixture.add_file("test.txt", "original\n");
        fixture.commit();

        // Checkout second working copy
        let wc2_path = fixture.temp_dir.path().join("wc2");
        let url = crate::uri::Uri::new(&fixture.url).unwrap();
        fixture
            .ctx
            .checkout(
                url,
                &wc2_path,
                &ClientTestFixture::default_checkout_options(),
            )
            .unwrap();

        // Modify and commit in wc1
        std::fs::write(&test_file1, "modified in wc1\n").unwrap();
        let wc1_str = fixture.wc_path_str().to_string();
        fixture
            .ctx
            .commit(
                &[&wc1_str],
                &CommitOptions::default(),
                std::collections::HashMap::new(),
                None,
                &mut |_| Ok(()),
            )
            .unwrap();

        // Modify in wc2 and update to create conflict
        let test_file2 = wc2_path.join("test.txt");
        std::fs::write(&test_file2, "modified in wc2\n").unwrap();
        let _ = fixture.ctx.update(
            &[wc2_path.to_str().unwrap()],
            Revision::Head,
            &UpdateOptions::default(),
        );

        let conflict = fixture.ctx.conflict_get(&test_file2).unwrap();
        let options = conflict
            .text_get_resolution_options(&mut fixture.ctx)
            .unwrap();

        // Should have multiple resolution options
        assert!(
            options.len() > 0,
            "Should have at least one resolution option"
        );

        // Test each option's description
        for option in options.iter() {
            let desc = option.get_description();

            // Verify description is not empty or placeholder
            assert_ne!(desc, "", "Description should not be empty string");
            assert_ne!(
                desc, "xyzzy",
                "Description should not be placeholder 'xyzzy'"
            );

            // Description should have meaningful content (more than just whitespace)
            assert!(
                desc.trim().len() > 3,
                "Description '{}' should be meaningful",
                desc
            );
        }
    }

    #[test]
    fn test_status_versioned() {
        // Test that Status::versioned returns true for versioned files
        let mut fixture = ClientTestFixture::new();

        // Create, add, and commit a file
        let test_file = fixture.add_file("test.txt", "content\n");
        fixture.commit();

        // Modify the file to trigger status reporting
        std::fs::write(&test_file, "modified content\n").unwrap();

        // Get status
        let mut got_status = false;
        let test_file_str = test_file.to_str().unwrap().to_string();
        let wc_path_str = fixture.wc_path_str().to_string();
        fixture
            .ctx
            .status(
                &wc_path_str,
                &StatusOptions::default(),
                &mut |path, status| {
                    if path == test_file_str {
                        // Versioned file should return true for versioned()
                        assert_eq!(
                            status.versioned(),
                            true,
                            "Versioned file should return true"
                        );
                        got_status = true;
                    }
                    Ok(())
                },
            )
            .unwrap();

        assert!(
            got_status,
            "Should have received status for the modified file"
        );
    }

    #[test]
    fn test_status_depth_option_affects_behavior() {
        // Test that StatusOptions::with_depth actually limits depth
        let mut fixture = ClientTestFixture::new();

        // Create directory structure: wc/subdir/file.txt
        let subdir = fixture.add_dir("subdir");
        let subfile = subdir.join("file.txt");
        std::fs::write(&subfile, "content").expect("should write file");
        fixture.ctx.add(&subfile, &AddOptions::default()).unwrap();
        fixture.commit();

        // Modify the file
        std::fs::write(&subfile, "modified").expect("should write file");

        // Test with Depth::Empty - should only report wc directory, not subdirectories
        let mut paths_empty = Vec::new();
        fixture
            .ctx
            .status(
                fixture
                    .wc_path
                    .to_str()
                    .expect("path should be valid UTF-8"),
                &StatusOptions::default().with_depth(Depth::Empty),
                &mut |path, _status| {
                    paths_empty.push(path.to_string());
                    Ok(())
                },
            )
            .unwrap();

        // Test with Depth::Infinity - should report all files including subdirectories
        let mut paths_infinity = Vec::new();
        fixture
            .ctx
            .status(
                fixture
                    .wc_path
                    .to_str()
                    .expect("path should be valid UTF-8"),
                &StatusOptions::default().with_depth(Depth::Infinity),
                &mut |path, _status| {
                    paths_infinity.push(path.to_string());
                    Ok(())
                },
            )
            .unwrap();

        // Depth::Empty should report fewer paths than Depth::Infinity
        assert!(
            paths_empty.len() < paths_infinity.len(),
            "Depth::Empty should report fewer paths ({}) than Depth::Infinity ({})",
            paths_empty.len(),
            paths_infinity.len()
        );

        // Depth::Infinity should include the subdirectory file
        let subfile_str = subfile.to_str().expect("path should be valid UTF-8");
        assert!(
            paths_infinity.iter().any(|p| p == subfile_str),
            "Depth::Infinity should include subdirectory file"
        );

        // Depth::Empty should NOT include the subdirectory file
        assert!(
            !paths_empty.iter().any(|p| p == subfile_str),
            "Depth::Empty should NOT include subdirectory file"
        );
    }

    #[test]
    fn test_status_changed_rev_returns_actual_revision() {
        // Test that Status::changed_rev() returns the actual revision, not Default::default()
        let mut fixture = ClientTestFixture::new();
        let test_file = fixture.add_file("test.txt", "initial content\n");

        // Capture the commit revision from the callback
        let mut commit_rev = None;
        fixture
            .ctx
            .commit(
                &[fixture
                    .wc_path
                    .to_str()
                    .expect("path should be valid UTF-8")],
                &CommitOptions::default(),
                std::collections::HashMap::new(),
                None,
                &mut |info| {
                    commit_rev = Some(info.revision());
                    Ok(())
                },
            )
            .unwrap();
        let commit_rev = commit_rev.expect("Commit callback should have been called");

        // Get status with get_all=true to include unmodified files
        let mut got_status = false;
        let test_file_str = test_file.to_str().expect("path should be valid UTF-8");
        let mut options = StatusOptions::default();
        options.get_all = true;
        fixture
            .ctx
            .status(
                fixture
                    .wc_path
                    .to_str()
                    .expect("path should be valid UTF-8"),
                &options,
                &mut |path, status| {
                    if path == test_file_str {
                        assert_eq!(
                            status.changed_rev(),
                            commit_rev,
                            "Status::changed_rev() should return the actual commit revision"
                        );
                        got_status = true;
                    }
                    Ok(())
                },
            )
            .unwrap();

        assert!(got_status, "Should have received status for the file");
    }

    #[test]
    fn test_add_with_add_parents_option() {
        // Test that AddOptions::with_add_parents actually adds parent directories
        let mut fixture = ClientTestFixture::new();

        // Create a nested directory structure that's not versioned
        let nested_dir = fixture.wc_path.join("subdir1").join("subdir2");
        std::fs::create_dir_all(&nested_dir).expect("should create dirs");
        let nested_file = nested_dir.join("test.txt");
        std::fs::write(&nested_file, "nested content\n").expect("should write file");

        // Add the file with add_parents option
        let options = AddOptions::default().with_add_parents(true);
        fixture.ctx.add(&nested_file, &options).unwrap();

        // Verify both the file and parent directories were added
        let mut added_paths = Vec::new();
        fixture
            .ctx
            .status(
                fixture
                    .wc_path
                    .to_str()
                    .expect("path should be valid UTF-8"),
                &StatusOptions::default(),
                &mut |path, status| {
                    if status.node_status() == crate::StatusKind::Added {
                        added_paths.push(path.to_string());
                    }
                    Ok(())
                },
            )
            .unwrap();

        // Should have added subdir1, subdir2, and the file
        let subdir1_path = fixture
            .wc_path
            .join("subdir1")
            .to_str()
            .expect("path should be valid UTF-8")
            .to_string();
        let subdir2_path = fixture
            .wc_path
            .join("subdir1")
            .join("subdir2")
            .to_str()
            .expect("path should be valid UTF-8")
            .to_string();
        let file_path = nested_file
            .to_str()
            .expect("path should be valid UTF-8")
            .to_string();

        assert!(
            added_paths.contains(&subdir1_path),
            "Parent directory subdir1 should be added"
        );
        assert!(
            added_paths.contains(&subdir2_path),
            "Parent directory subdir2 should be added"
        );
        assert!(added_paths.contains(&file_path), "File should be added");
    }

    #[test]
    fn test_revert_with_metadata_only_option() {
        // Test that RevertOptions::with_metadata_only only reverts metadata, not content
        let mut fixture = ClientTestFixture::new();
        let test_file = fixture.add_file("test.txt", "original content\n");
        fixture.commit();

        // Modify the file content AND set a property
        std::fs::write(&test_file, "modified content\n").expect("should write file");
        fixture
            .ctx
            .propset(
                "test-prop",
                Some("test-value".as_bytes()),
                test_file.to_str().expect("path should be valid UTF-8"),
                &crate::client::PropSetOptions::default(),
            )
            .unwrap();

        // Revert with metadata_only=true
        let options = RevertOptions::default().with_metadata_only(true);
        fixture
            .ctx
            .revert(
                &[test_file.to_str().expect("path should be valid UTF-8")],
                &options,
            )
            .unwrap();

        // Verify: property should be reverted (removed), but content should remain modified
        let content = std::fs::read_to_string(&test_file).expect("should read file");
        assert_eq!(
            content, "modified content\n",
            "With metadata_only=true, file content should NOT be reverted"
        );

        let prop_val = fixture
            .ctx
            .propget(
                "test-prop",
                test_file.to_str().expect("path should be valid UTF-8"),
                &crate::client::PropGetOptions::default(),
                None,
            )
            .unwrap();
        assert_eq!(
            prop_val.len(),
            0,
            "With metadata_only=true, property should be reverted"
        );

        // Now set the property again and revert with metadata_only=false
        fixture
            .ctx
            .propset(
                "test-prop",
                Some("test-value".as_bytes()),
                test_file.to_str().expect("path should be valid UTF-8"),
                &crate::client::PropSetOptions::default(),
            )
            .unwrap();

        let options = RevertOptions::default().with_metadata_only(false);
        fixture
            .ctx
            .revert(
                &[test_file.to_str().expect("path should be valid UTF-8")],
                &options,
            )
            .unwrap();

        // Verify: both content AND property should be reverted
        let content = std::fs::read_to_string(&test_file).expect("should read file");
        assert_eq!(
            content, "original content\n",
            "With metadata_only=false, file content should be reverted"
        );

        let prop_val = fixture
            .ctx
            .propget(
                "test-prop",
                test_file.to_str().expect("path should be valid UTF-8"),
                &crate::client::PropGetOptions::default(),
                None,
            )
            .unwrap();
        assert_eq!(
            prop_val.len(),
            0,
            "With metadata_only=false, property should be reverted"
        );
    }

    #[test]
    fn test_status_conflicted_returns_false_for_normal_files() {
        // Test that Status::conflicted() returns false for non-conflicted files, not always true
        let mut fixture = ClientTestFixture::new();
        let test_file = fixture.add_file("test.txt", "content\n");
        fixture.commit();

        // Modify the file (but don't create a conflict)
        std::fs::write(&test_file, "modified content\n").expect("should write file");

        // Get status and verify conflicted() returns false
        let mut got_status = false;
        let test_file_str = test_file.to_str().expect("path should be valid UTF-8");
        let mut options = StatusOptions::default();
        options.get_all = true;
        fixture
            .ctx
            .status(
                fixture
                    .wc_path
                    .to_str()
                    .expect("path should be valid UTF-8"),
                &options,
                &mut |path, status| {
                    if path == test_file_str {
                        assert_eq!(
                            status.conflicted(),
                            false,
                            "Non-conflicted file should return false from conflicted()"
                        );
                        got_status = true;
                    }
                    Ok(())
                },
            )
            .unwrap();

        assert!(got_status, "Should have received status for the file");
    }

    #[test]
    fn test_get_repos_root_returns_actual_values() {
        // Test that Context::get_repos_root returns actual repository root and UUID, not empty/xyzzy
        let mut fixture = ClientTestFixture::new();

        // Get repos root using working copy path
        let (root_url, uuid) = fixture
            .ctx
            .get_repos_root(
                fixture
                    .wc_path
                    .to_str()
                    .expect("path should be valid UTF-8"),
            )
            .unwrap();

        // Verify root_url is the actual repository URL, not empty or "xyzzy"
        assert_eq!(
            root_url, fixture.url,
            "Repository root should match the checkout URL"
        );

        // Verify UUID is not empty or "xyzzy"
        assert!(!uuid.is_empty(), "Repository UUID should not be empty");
        assert_ne!(
            uuid, "xyzzy",
            "Repository UUID should be an actual UUID, not 'xyzzy'"
        );

        // UUID should be in the format of a UUID (hyphenated hex)
        assert!(
            uuid.contains('-'),
            "Repository UUID should be in UUID format with hyphens"
        );
    }

    #[test]
    fn test_status_local_abspath_returns_actual_path() {
        // Test that Status::local_abspath() returns the actual file path, not "xyzzy"
        let mut fixture = ClientTestFixture::new();
        let test_file = fixture.add_file("myfile.txt", "content\n");

        // Get status and verify local_abspath() returns the actual path
        let mut got_status = false;
        let test_file_str = test_file.to_str().expect("path should be valid UTF-8");
        fixture
            .ctx
            .status(
                fixture
                    .wc_path
                    .to_str()
                    .expect("path should be valid UTF-8"),
                &StatusOptions::default(),
                &mut |path, status| {
                    if path == test_file_str {
                        let abspath = status.local_abspath_native();
                        assert_eq!(
                            abspath,
                            std::path::Path::new(test_file_str),
                            "Status::local_abspath_native() should return the actual file path"
                        );
                        assert_ne!(
                            status.local_abspath(),
                            "xyzzy",
                            "local_abspath should not be 'xyzzy'"
                        );
                        got_status = true;
                    }
                    Ok(())
                },
            )
            .unwrap();

        assert!(got_status, "Should have received status for the file");
    }

    #[test]
    fn test_status_versioned_returns_false_for_unversioned() {
        // Test that Status::versioned() returns false for unversioned files, not always true
        let mut fixture = ClientTestFixture::new();

        // Create an unversioned file (not added to version control)
        let unversioned_file = fixture.wc_path.join("unversioned.txt");
        std::fs::write(&unversioned_file, "not tracked\n").expect("should write file");

        // Get status and verify versioned() returns false for unversioned file
        let mut got_status = false;
        let unversioned_str = unversioned_file
            .to_str()
            .expect("path should be valid UTF-8");
        let mut options = StatusOptions::default();
        options.get_all = true; // Include unversioned files
        fixture
            .ctx
            .status(
                fixture
                    .wc_path
                    .to_str()
                    .expect("path should be valid UTF-8"),
                &options,
                &mut |path, status| {
                    if path == unversioned_str {
                        assert_eq!(
                            status.versioned(),
                            false,
                            "Unversioned file should return false from versioned()"
                        );
                        got_status = true;
                    }
                    Ok(())
                },
            )
            .unwrap();

        assert!(
            got_status,
            "Should have received status for unversioned file"
        );
    }

    #[test]
    fn test_status_wc_is_locked_returns_false_for_unlocked() {
        // Test that Status::wc_is_locked() returns false for unlocked working copies, not always true
        let mut fixture = ClientTestFixture::new();
        let test_file = fixture.add_file("test.txt", "content\n");

        // Get status and verify wc_is_locked() returns false
        let mut got_status = false;
        let test_file_str = test_file.to_str().expect("path should be valid UTF-8");
        fixture
            .ctx
            .status(
                fixture
                    .wc_path
                    .to_str()
                    .expect("path should be valid UTF-8"),
                &StatusOptions::default(),
                &mut |path, status| {
                    if path == test_file_str {
                        assert_eq!(
                            status.wc_is_locked(),
                            false,
                            "Normal unlocked working copy should return false from wc_is_locked()"
                        );
                        got_status = true;
                    }
                    Ok(())
                },
            )
            .unwrap();

        assert!(got_status, "Should have received status for the file");
    }

    #[test]
    fn test_status_file_external_returns_false_for_normal_files() {
        // Test that Status::file_external() returns false for normal files, not always true
        let mut fixture = ClientTestFixture::new();
        let test_file = fixture.add_file("normal.txt", "normal file\n");

        // Get status and verify file_external() returns false
        let mut got_status = false;
        let test_file_str = test_file.to_str().expect("path should be valid UTF-8");
        fixture
            .ctx
            .status(
                fixture
                    .wc_path
                    .to_str()
                    .expect("path should be valid UTF-8"),
                &StatusOptions::default(),
                &mut |path, status| {
                    if path == test_file_str {
                        assert_eq!(
                            status.file_external(),
                            false,
                            "Normal file should return false from file_external()"
                        );
                        got_status = true;
                    }
                    Ok(())
                },
            )
            .unwrap();

        assert!(got_status, "Should have received status for the file");
    }

    #[test]
    fn test_status_kind_returns_actual_kind() {
        // Test that Status::kind() returns the actual NodeKind, not Default::default()
        let mut fixture = ClientTestFixture::new();
        let test_file = fixture.add_file("test.txt", "test content\n");
        let test_dir = fixture.add_dir("test_dir");

        // Get status and verify kind() returns correct NodeKind for file
        let mut got_file_status = false;
        let test_file_str = test_file.to_str().expect("path should be valid UTF-8");
        fixture
            .ctx
            .status(
                fixture
                    .wc_path
                    .to_str()
                    .expect("path should be valid UTF-8"),
                &StatusOptions::default(),
                &mut |path, status| {
                    if path == test_file_str {
                        assert_eq!(
                            status.kind(),
                            crate::NodeKind::File,
                            "File should return NodeKind::File from kind()"
                        );
                        got_file_status = true;
                    }
                    Ok(())
                },
            )
            .unwrap();

        assert!(got_file_status, "Should have received status for the file");

        // Get status and verify kind() returns correct NodeKind for directory
        let mut got_dir_status = false;
        let test_dir_str = test_dir.to_str().expect("path should be valid UTF-8");
        fixture
            .ctx
            .status(
                fixture
                    .wc_path
                    .to_str()
                    .expect("path should be valid UTF-8"),
                &StatusOptions::default(),
                &mut |path, status| {
                    if path == test_dir_str {
                        assert_eq!(
                            status.kind(),
                            crate::NodeKind::Dir,
                            "Directory should return NodeKind::Dir from kind()"
                        );
                        got_dir_status = true;
                    }
                    Ok(())
                },
            )
            .unwrap();

        assert!(
            got_dir_status,
            "Should have received status for the directory"
        );
    }

    #[test]
    fn test_status_copied_returns_false_for_normal_files() {
        // Test that Status::copied() returns false for normal files, not always true
        let mut fixture = ClientTestFixture::new();
        let test_file = fixture.add_file("normal.txt", "normal file\n");

        // Get status and verify copied() returns false
        let mut got_status = false;
        let test_file_str = test_file.to_str().expect("path should be valid UTF-8");
        fixture
            .ctx
            .status(
                fixture
                    .wc_path
                    .to_str()
                    .expect("path should be valid UTF-8"),
                &StatusOptions::default(),
                &mut |path, status| {
                    if path == test_file_str {
                        assert_eq!(
                            status.copied(),
                            false,
                            "Normal (non-copied) file should return false from copied()"
                        );
                        got_status = true;
                    }
                    Ok(())
                },
            )
            .unwrap();

        assert!(got_status, "Should have received status for the file");
    }

    #[test]
    fn test_iter_logs() {
        let mut fixture = ClientTestFixture::new();
        fixture.add_file("file1.txt", "first");
        let rev1 = fixture.commit();

        fixture.add_file("file2.txt", "second");
        let rev2 = fixture.commit();

        let entries: Vec<_> = fixture
            .ctx
            .iter_logs(
                &[&fixture.url],
                &[RevisionRange::new(
                    Revision::Number(rev1),
                    Revision::Number(rev2),
                )],
                &LogOptions::new(),
            )
            .collect();

        assert_eq!(entries.len(), 2, "Should have 2 log entries");
        for entry in &entries {
            assert!(
                entry.is_ok(),
                "Entry should be Ok: {:?}",
                entry.as_ref().err()
            );
        }

        let revs: Vec<_> = entries
            .iter()
            .map(|e| e.as_ref().unwrap().revision().unwrap())
            .collect();
        assert!(revs.contains(&rev2));
        assert!(revs.contains(&rev1));
    }

    #[test]
    fn test_iter_logs_early_drop() {
        let mut fixture = ClientTestFixture::new();
        for i in 0..5 {
            fixture.add_file(&format!("file{}.txt", i), &format!("content {}", i));
            fixture.commit();
        }

        // Take only 2 entries and drop the iterator — should not hang or panic
        let entries: Vec<_> = fixture
            .ctx
            .iter_logs(
                &[&fixture.url],
                &[RevisionRange::new(
                    Revision::Number(Revnum(1)),
                    Revision::Head,
                )],
                &LogOptions::new(),
            )
            .take(2)
            .collect();

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

    #[test]
    fn test_iter_logs_error() {
        let mut fixture = ClientTestFixture::new();

        // Request logs for a non-existent revision range — should yield an error
        let mut iter = fixture.ctx.iter_logs(
            &[&fixture.url],
            &[RevisionRange::new(
                Revision::Number(Revnum(0)),
                Revision::Number(Revnum(1000)),
            )],
            &LogOptions::new(),
        );
        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
                );
            }
        }
    }
}
