//! Repository administration and server-side operations.
//!
//! This module provides the [`Repos`](crate::repos::Repos) type for creating, managing, and administering
//! Subversion repositories. It includes operations for repository creation, loading/dumping,
//! verification, and authorization checking.
//!
//! # Overview
//!
//! The repos layer provides server-side repository operations and administration functions.
//! This is distinct from the [`client`](crate::client) module which provides client-side
//! operations, and the [`fs`](crate::fs) module which provides low-level filesystem access.
//!
//! ## Key Operations
//!
//! - **Repository lifecycle**: Create, open, and recover repositories
//! - **Backup and restore**: Dump and load repository contents
//! - **Verification**: Check repository integrity
//! - **Authorization**: Path-based access control with authz
//! - **Lock management**: Repository-level lock operations
//! - **Hooks**: Execute repository hooks
//!
//! # Example
//!
//! ```no_run
//! use subversion::repos::Repos;
//!
//! // Create a new repository
//! let repo = Repos::create("/path/to/repo").unwrap();
//!
//! // Access the filesystem
//! let fs = repo.fs().unwrap();
//! let youngest = fs.youngest_rev().unwrap();
//! println!("Repository is at revision {}", youngest);
//! ```

use crate::{svn_result, with_tmp_pool, Error, Revnum};
use std::ffi::CString;
use std::marker::PhantomData;
use subversion_sys::{
    svn_repos_create, svn_repos_dump_fs4, svn_repos_find_root_path, svn_repos_load_fs6,
    svn_repos_recover4, svn_repos_t, svn_repos_verify_fs3,
};

// Helper functions for properly boxing callback batons
/// Specifies how to handle UUID during repository load operations.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum LoadUUID {
    #[default]
    /// Only update UUID if the repos has no revisions
    Default,

    /// Never update UUID
    Ignore,

    /// Always update UUID
    Force,
}

impl From<subversion_sys::svn_repos_load_uuid> for LoadUUID {
    fn from(load_uuid: subversion_sys::svn_repos_load_uuid) -> Self {
        match load_uuid {
            subversion_sys::svn_repos_load_uuid_svn_repos_load_uuid_default => Self::Default,
            subversion_sys::svn_repos_load_uuid_svn_repos_load_uuid_ignore => Self::Ignore,
            subversion_sys::svn_repos_load_uuid_svn_repos_load_uuid_force => Self::Force,
            _ => unreachable!(),
        }
    }
}

impl From<LoadUUID> for subversion_sys::svn_repos_load_uuid {
    fn from(load_uuid: LoadUUID) -> Self {
        match load_uuid {
            LoadUUID::Default => subversion_sys::svn_repos_load_uuid_svn_repos_load_uuid_default,
            LoadUUID::Ignore => subversion_sys::svn_repos_load_uuid_svn_repos_load_uuid_ignore,
            LoadUUID::Force => subversion_sys::svn_repos_load_uuid_svn_repos_load_uuid_force,
        }
    }
}

/// Options for repository dump operations.
#[derive(Default)]
pub struct DumpOptions<'a> {
    /// Starting revision (None for revision 0).
    pub start_rev: Option<Revnum>,
    /// Ending revision (None for HEAD).
    pub end_rev: Option<Revnum>,
    /// If true, produce incremental dump (only changes since start_rev).
    pub incremental: bool,
    /// If true, use deltas for file contents.
    pub use_deltas: bool,
    /// If true, include revision properties.
    pub include_revprops: bool,
    /// If true, include node changes.
    pub include_changes: bool,
    /// Optional notification callback.
    pub notify_func: Option<&'a dyn Fn(&Notify)>,
    /// Optional filter callback to control which paths are dumped.
    pub filter_func:
        Option<Box<dyn FnMut(&crate::fs::Root, &str) -> Result<bool, Error<'static>> + 'a>>,
    /// Optional cancellation callback.
    pub cancel_func: Option<&'a dyn Fn() -> Result<(), Error<'static>>>,
}

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

    /// Sets the starting revision.
    pub fn with_start_rev(mut self, rev: Revnum) -> Self {
        self.start_rev = Some(rev);
        self
    }

    /// Sets the ending revision.
    pub fn with_end_rev(mut self, rev: Revnum) -> Self {
        self.end_rev = Some(rev);
        self
    }

    /// Sets whether to produce an incremental dump.
    pub fn with_incremental(mut self, incremental: bool) -> Self {
        self.incremental = incremental;
        self
    }

    /// Sets whether to use deltas for file contents.
    pub fn with_use_deltas(mut self, use_deltas: bool) -> Self {
        self.use_deltas = use_deltas;
        self
    }

    /// Sets whether to include revision properties.
    pub fn with_include_revprops(mut self, include: bool) -> Self {
        self.include_revprops = include;
        self
    }

    /// Sets whether to include node changes.
    pub fn with_include_changes(mut self, include: bool) -> Self {
        self.include_changes = include;
        self
    }
}

/// Options for repository load operations.
#[derive(Default)]
pub struct LoadOptions<'a> {
    /// Starting revision (None for all revisions).
    pub start_rev: Option<Revnum>,
    /// Ending revision (None for all revisions).
    pub end_rev: Option<Revnum>,
    /// How to handle UUID from dump stream.
    pub uuid_action: LoadUUID,
    /// Parent directory to load into (None for root).
    pub parent_dir: Option<&'a str>,
    /// If true, run pre-commit hook for each loaded revision.
    pub use_pre_commit_hook: bool,
    /// If true, run post-commit hook for each loaded revision.
    pub use_post_commit_hook: bool,
    /// If true, validate properties.
    pub validate_props: bool,
    /// If true, ignore dates from dump and use current time.
    pub ignore_dates: bool,
    /// If true, normalize properties (e.g., line endings).
    pub normalize_props: bool,
    /// Optional notification callback.
    pub notify_func: Option<&'a dyn Fn(&Notify)>,
    /// Optional cancellation callback.
    pub cancel_func: Option<&'a dyn Fn() -> Result<(), Error<'static>>>,
}

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

    /// Sets the starting revision.
    pub fn with_start_rev(mut self, rev: Revnum) -> Self {
        self.start_rev = Some(rev);
        self
    }

    /// Sets the ending revision.
    pub fn with_end_rev(mut self, rev: Revnum) -> Self {
        self.end_rev = Some(rev);
        self
    }

    /// Sets how to handle UUID from dump stream.
    pub fn with_uuid_action(mut self, action: LoadUUID) -> Self {
        self.uuid_action = action;
        self
    }

    /// Sets the parent directory to load into.
    pub fn with_parent_dir(mut self, dir: &'a str) -> Self {
        self.parent_dir = Some(dir);
        self
    }

    /// Sets whether to run pre-commit hooks.
    pub fn with_use_pre_commit_hook(mut self, use_hook: bool) -> Self {
        self.use_pre_commit_hook = use_hook;
        self
    }

    /// Sets whether to run post-commit hooks.
    pub fn with_use_post_commit_hook(mut self, use_hook: bool) -> Self {
        self.use_post_commit_hook = use_hook;
        self
    }

    /// Sets whether to validate properties.
    pub fn with_validate_props(mut self, validate: bool) -> Self {
        self.validate_props = validate;
        self
    }

    /// Sets whether to ignore dates from dump.
    pub fn with_ignore_dates(mut self, ignore: bool) -> Self {
        self.ignore_dates = ignore;
        self
    }

    /// Sets whether to normalize properties.
    pub fn with_normalize_props(mut self, normalize: bool) -> Self {
        self.normalize_props = normalize;
        self
    }
}

/// Options for repository verify operations.
#[derive(Default)]
pub struct VerifyOptions<'a> {
    /// Starting revision to verify.
    pub start_rev: Revnum,
    /// Ending revision to verify.
    pub end_rev: Revnum,
    /// If true, check for normalization issues.
    pub check_normalization: bool,
    /// If true, only verify metadata (not file contents).
    pub metadata_only: bool,
    /// Optional notification callback.
    pub notify_func: Option<&'a dyn Fn(&Notify)>,
    /// Optional callback for verification errors.
    pub verify_callback: Option<&'a dyn Fn(Revnum, &Error) -> Result<(), Error<'static>>>,
    /// Optional cancellation callback.
    pub cancel_func: Option<&'a dyn Fn() -> Result<(), Error<'static>>>,
}

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

    /// Sets the starting revision.
    pub fn with_start_rev(mut self, rev: Revnum) -> Self {
        self.start_rev = rev;
        self
    }

    /// Sets the ending revision.
    pub fn with_end_rev(mut self, rev: Revnum) -> Self {
        self.end_rev = rev;
        self
    }

    /// Sets whether to check for normalization issues.
    pub fn with_check_normalization(mut self, check: bool) -> Self {
        self.check_normalization = check;
        self
    }

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

/// Authorization access levels
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AuthzAccess {
    /// No access
    None,
    /// Path can be read
    Read,
    /// Path can be altered
    Write,
    /// Both read and write access
    ReadWrite,
}

impl AuthzAccess {
    /// Convert from raw C enum value
    pub fn from_raw(access: subversion_sys::svn_repos_authz_access_t) -> Self {
        let read = subversion_sys::svn_repos_authz_access_t_svn_authz_read;
        let write = subversion_sys::svn_repos_authz_access_t_svn_authz_write;

        if access & (read | write) == (read | write) {
            AuthzAccess::ReadWrite
        } else if access & write != 0 {
            AuthzAccess::Write
        } else if access & read != 0 {
            AuthzAccess::Read
        } else {
            AuthzAccess::None
        }
    }
}

impl From<AuthzAccess> for subversion_sys::svn_repos_authz_access_t {
    fn from(access: AuthzAccess) -> Self {
        match access {
            AuthzAccess::None => subversion_sys::svn_repos_authz_access_t_svn_authz_none,
            AuthzAccess::Read => subversion_sys::svn_repos_authz_access_t_svn_authz_read,
            AuthzAccess::Write => subversion_sys::svn_repos_authz_access_t_svn_authz_write,
            AuthzAccess::ReadWrite => {
                subversion_sys::svn_repos_authz_access_t_svn_authz_read
                    | subversion_sys::svn_repos_authz_access_t_svn_authz_write
            }
        }
    }
}

/// Revision access levels for determining what revision information is visible
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RevisionAccessLevel {
    /// No access to revision properties and changed-paths information
    None,
    /// Access to some revision properties (svn:date, svn:author) and changed-paths for accessible paths
    Partial,
    /// Full access to all revision properties and changed-paths information
    Full,
}

impl From<subversion_sys::svn_repos_revision_access_level_t> for RevisionAccessLevel {
    fn from(level: subversion_sys::svn_repos_revision_access_level_t) -> Self {
        if level == subversion_sys::svn_repos_revision_access_level_t_svn_repos_revision_access_none
        {
            RevisionAccessLevel::None
        } else if level
            == subversion_sys::svn_repos_revision_access_level_t_svn_repos_revision_access_partial
        {
            RevisionAccessLevel::Partial
        } else {
            RevisionAccessLevel::Full
        }
    }
}

/// Authorization data structure
pub struct Authz {
    ptr: *mut subversion_sys::svn_authz_t,
    _pool: apr::Pool<'static>,
}

impl Authz {
    /// Read authz configuration from a file
    pub fn read(
        path: &std::path::Path,
        groups_path: Option<&std::path::Path>,
        must_exist: bool,
    ) -> Result<Self, Error<'static>> {
        let pool = apr::Pool::new();
        let path_cstr = std::ffi::CString::new(path.to_str().unwrap()).unwrap();
        let groups_path_cstr =
            groups_path.map(|p| std::ffi::CString::new(p.to_str().unwrap()).unwrap());

        let mut authz_ptr: *mut subversion_sys::svn_authz_t = std::ptr::null_mut();

        let ret = unsafe {
            subversion_sys::svn_repos_authz_read4(
                &mut authz_ptr,
                path_cstr.as_ptr(),
                groups_path_cstr
                    .as_ref()
                    .map(|p| p.as_ptr())
                    .unwrap_or(std::ptr::null()),
                must_exist.into(),
                std::ptr::null_mut(),
                None,
                std::ptr::null_mut(),
                pool.as_mut_ptr(),
                pool.as_mut_ptr(),
            )
        };

        svn_result(ret)?;

        Ok(Authz {
            ptr: authz_ptr,
            _pool: pool,
        })
    }

    /// Parse authz configuration from a string
    pub fn parse(contents: &str, groups_contents: Option<&str>) -> Result<Self, Error<'static>> {
        let pool = apr::Pool::new();
        let mut authz_ptr: *mut subversion_sys::svn_authz_t = std::ptr::null_mut();

        // Create svn_stream_t from the authz contents
        let mut contents_stream = crate::io::Stream::from(contents.as_bytes());

        // Create optional groups stream
        let mut groups_stream = groups_contents.map(|g| crate::io::Stream::from(g.as_bytes()));

        let ret = unsafe {
            subversion_sys::svn_repos_authz_parse2(
                &mut authz_ptr,
                contents_stream.as_mut_ptr(),
                groups_stream
                    .as_mut()
                    .map_or(std::ptr::null_mut(), |s| s.as_mut_ptr()),
                None,                 // authz_validate_func
                std::ptr::null_mut(), // authz_validate_baton
                pool.as_mut_ptr(),
                pool.as_mut_ptr(),
            )
        };

        svn_result(ret)?;

        Ok(Authz {
            ptr: authz_ptr,
            _pool: pool,
        })
    }

    /// Check if a user has the required access to a path
    pub fn check_access(
        &self,
        repos_name: Option<&str>,
        path: &str,
        user: Option<&str>,
        required_access: AuthzAccess,
    ) -> Result<bool, Error<'static>> {
        let repos_name_cstr = repos_name.map(|r| std::ffi::CString::new(r).unwrap());
        let path_cstr = std::ffi::CString::new(path).unwrap();
        let user_cstr = user.map(|u| std::ffi::CString::new(u).unwrap());

        let mut access_granted: subversion_sys::svn_boolean_t = 0;

        with_tmp_pool(|pool| {
            let ret = unsafe {
                subversion_sys::svn_repos_authz_check_access(
                    self.ptr,
                    repos_name_cstr
                        .as_ref()
                        .map(|r| r.as_ptr())
                        .unwrap_or(std::ptr::null()),
                    path_cstr.as_ptr(),
                    user_cstr
                        .as_ref()
                        .map(|u| u.as_ptr())
                        .unwrap_or(std::ptr::null()),
                    required_access.into(),
                    &mut access_granted,
                    pool.as_mut_ptr(),
                )
            };

            svn_result(ret)?;
            Ok(access_granted != 0)
        })
    }
}

/// Finds the root path of a repository containing the given path.
pub fn find_root_path(path: &std::path::Path) -> Option<std::path::PathBuf> {
    let path = std::ffi::CString::new(path.to_str().unwrap()).unwrap();
    with_tmp_pool(|pool| {
        let ret = unsafe { svn_repos_find_root_path(path.as_ptr(), pool.as_mut_ptr()) };
        if ret.is_null() {
            None
        } else {
            let root_path = unsafe { std::ffi::CStr::from_ptr(ret) };
            Some(std::path::PathBuf::from(root_path.to_str().unwrap()))
        }
    })
}

/// Repository handle with RAII cleanup
pub struct Repos {
    ptr: *mut svn_repos_t,
    _pool: apr::Pool<'static>,
    _phantom: PhantomData<*mut ()>, // !Send + !Sync
}

/// Compute differences between two repository trees and send to an editor
///
/// This function compares two trees in the repository and generates
/// edit operations that describe how to transform one into the other.
pub fn dir_delta2(
    src_root: &crate::fs::Root,
    src_parent_dir: &str,
    src_entry: &str,
    tgt_root: &crate::fs::Root,
    tgt_path: &str,
    editor: &crate::delta::WrapEditor,
    text_deltas: bool,
    depth: crate::Depth,
    entry_props: bool,
    ignore_ancestry: bool,
) -> Result<(), crate::Error<'static>> {
    let src_parent_dir_cstr = std::ffi::CString::new(src_parent_dir)?;
    let src_entry_cstr = std::ffi::CString::new(src_entry)?;
    let tgt_path_cstr = std::ffi::CString::new(tgt_path)?;

    with_tmp_pool(|pool| {
        let (editor_ptr, edit_baton) = editor.as_raw_parts();

        let err = unsafe {
            subversion_sys::svn_repos_dir_delta2(
                src_root.as_ptr() as *mut _,
                src_parent_dir_cstr.as_ptr(),
                src_entry_cstr.as_ptr(),
                tgt_root.as_ptr() as *mut _,
                tgt_path_cstr.as_ptr(),
                editor_ptr,
                edit_baton,
                None,                 // authz_read_func
                std::ptr::null_mut(), // authz_read_baton,
                if text_deltas { 1 } else { 0 },
                depth.into(),
                if entry_props { 1 } else { 0 },
                if ignore_ancestry { 1 } else { 0 },
                pool.as_mut_ptr(),
            )
        };

        svn_result(err)
    })
}

/// Replay the changes in `root` through `editor`.
///
/// Only paths under `base_dir` are replayed.  `low_water_mark` is the
/// oldest revision whose full tree data the editor is assumed to have —
/// pass `Revnum(SVN_INVALID_REVNUM)` (i.e. `Revnum(-1)`) if the editor
/// has no prior tree data at all.  When `send_deltas` is true, file
/// content changes are sent as deltas; otherwise only adds and deletes
/// are sent.
///
/// Authorization is not checked (no authz callback is used).
///
/// Wraps `svn_repos_replay2`.
#[cfg(feature = "delta")]
pub fn replay(
    root: &crate::fs::Root,
    base_dir: &str,
    low_water_mark: Revnum,
    send_deltas: bool,
    editor: &crate::delta::WrapEditor,
) -> Result<(), Error<'static>> {
    let base_dir_cstr = std::ffi::CString::new(base_dir)?;
    with_tmp_pool(|pool| {
        let (editor_ptr, edit_baton) = editor.as_raw_parts();
        let err = unsafe {
            subversion_sys::svn_repos_replay2(
                root.as_ptr() as *mut _,
                base_dir_cstr.as_ptr(),
                low_water_mark.0,
                if send_deltas { 1 } else { 0 },
                editor_ptr,
                edit_baton,
                None,                 // authz_read_func
                std::ptr::null_mut(), // authz_read_baton
                pool.as_mut_ptr(),
            )
        };
        svn_result(err)
    })
}

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

// Dropper functions for callback batons
unsafe fn drop_authz_commit_baton(baton: *mut std::ffi::c_void) {
    drop(Box::from_raw(
        baton
            as *mut Box<
                dyn FnMut(AuthzAccess, &crate::fs::Root, &str) -> Result<bool, Error<'static>>,
            >,
    ));
}

unsafe fn drop_authz_read_baton(baton: *mut std::ffi::c_void) {
    drop(Box::from_raw(
        baton as *mut Box<dyn FnMut(&crate::fs::Root, &str) -> Result<bool, Error<'static>>>,
    ));
}

impl Repos {
    /// Creates a new repository at the specified path.
    pub fn create(path: &std::path::Path) -> Result<Repos, Error<'static>> {
        Self::create_with_config(path, None, None)
    }

    /// Creates a new repository with configuration options.
    pub fn create_with_config(
        path: &std::path::Path,
        config: Option<&std::collections::HashMap<String, String>>,
        fs_config: Option<&std::collections::HashMap<String, String>>,
    ) -> Result<Repos, Error<'static>> {
        // Ensure SVN libraries are initialized
        crate::init::initialize()?;

        let path = std::ffi::CString::new(path.to_str().unwrap()).unwrap();
        let pool = apr::Pool::new();

        // Convert config HashMap to APR hash if provided
        let config_hash = if let Some(cfg) = config {
            let byte_pairs: Vec<_> = cfg
                .iter()
                .map(|(k, v)| (k.as_bytes(), v.as_bytes()))
                .collect();
            let mut hash = apr::hash::Hash::new(&pool);
            for (k, v) in byte_pairs.iter() {
                unsafe {
                    hash.insert(k, v.as_ptr() as *mut std::ffi::c_void);
                }
            }
            unsafe { hash.as_mut_ptr() }
        } else {
            std::ptr::null_mut()
        };

        // Convert fs_config HashMap to APR hash if provided
        let fs_config_hash = if let Some(cfg) = fs_config {
            let byte_pairs: Vec<_> = cfg
                .iter()
                .map(|(k, v)| (k.as_bytes(), v.as_bytes()))
                .collect();
            let mut hash = apr::hash::Hash::new(&pool);
            for (k, v) in byte_pairs.iter() {
                unsafe {
                    hash.insert(k, v.as_ptr() as *mut std::ffi::c_void);
                }
            }
            unsafe { hash.as_mut_ptr() }
        } else {
            std::ptr::null_mut()
        };

        unsafe {
            let mut repos: *mut svn_repos_t = std::ptr::null_mut();
            let ret = svn_repos_create(
                &mut repos,
                path.as_ptr(),
                std::ptr::null(),
                std::ptr::null(),
                config_hash,
                fs_config_hash,
                pool.as_mut_ptr(),
            );
            svn_result(ret)?;
            Ok(Repos {
                ptr: repos,
                _pool: pool,
                _phantom: PhantomData,
            })
        }
    }

    /// Opens an existing repository.
    pub fn open(path: &std::path::Path) -> Result<Repos, Error<'static>> {
        // Ensure SVN libraries are initialized
        crate::init::initialize()?;

        let path = std::ffi::CString::new(path.to_str().unwrap()).unwrap();
        let pool = apr::Pool::new();

        unsafe {
            let mut repos: *mut svn_repos_t = std::ptr::null_mut();
            let ret = subversion_sys::svn_repos_open(&mut repos, path.as_ptr(), pool.as_mut_ptr());
            svn_result(ret)?;
            Ok(Repos {
                ptr: repos,
                _pool: pool,
                _phantom: PhantomData,
            })
        }
    }

    /// Gets the capabilities of the repository.
    pub fn capabilities(&mut self) -> Result<std::collections::HashSet<String>, Error<'_>> {
        let pool = apr::Pool::new();
        let scratch_pool = apr::Pool::new();
        let mut capabilities: *mut apr_sys::apr_hash_t = std::ptr::null_mut();
        let ret = unsafe {
            subversion_sys::svn_repos_capabilities(
                &mut capabilities,
                self.ptr,
                pool.as_mut_ptr(),
                scratch_pool.as_mut_ptr(),
            )
        };
        Error::from_raw(ret)?;
        let capabilities_hash = unsafe { apr::hash::Hash::from_ptr(capabilities) };
        Ok(capabilities_hash
            .iter()
            .map(|(k, _)| String::from_utf8_lossy(k).to_string())
            .collect::<std::collections::HashSet<String>>())
    }

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

    /// Remembers client capabilities for this session.
    pub fn remember_client_capabilities(
        &mut self,
        capabilities: &[&str],
    ) -> Result<(), Error<'static>> {
        let pool = apr::Pool::new();
        let capabilities = capabilities
            .iter()
            .map(|c| std::ffi::CString::new(*c).unwrap())
            .collect::<Vec<_>>();
        let mut capabilities_array =
            apr::tables::TypedArray::<*const i8>::new(&pool, capabilities.len() as i32);
        for cap in capabilities.iter() {
            capabilities_array.push(cap.as_ptr());
        }
        let ret = unsafe {
            subversion_sys::svn_repos_remember_client_capabilities(
                self.ptr,
                capabilities_array.as_ptr(),
            )
        };
        Error::from_raw(ret)?;
        Ok(())
    }

    /// Gets the filesystem object for this repository.
    pub fn fs(&self) -> Option<crate::fs::Fs<'static>> {
        let fs_ptr = unsafe { subversion_sys::svn_repos_fs(self.ptr) };

        if fs_ptr.is_null() {
            None
        } else {
            // Create a new root pool for the Fs
            // The fs_ptr is owned by repos and valid as long as repos exists
            // Using a new root pool ensures the Fs can be safely returned
            let pool = apr::Pool::new();
            Some(unsafe { crate::fs::Fs::from_ptr_and_pool(fs_ptr, pool) })
        }
    }

    /// Gets the filesystem type of this repository.
    pub fn fs_type(&self) -> String {
        with_tmp_pool(|pool| {
            let ret = unsafe { subversion_sys::svn_repos_fs_type(self.ptr, pool.as_mut_ptr()) };
            let fs_type = unsafe { std::ffi::CStr::from_ptr(ret) };
            fs_type.to_str().unwrap().to_string()
        })
    }

    /// Gets the path to the repository.
    pub fn path(&mut self) -> std::path::PathBuf {
        with_tmp_pool(|pool| {
            let ret = unsafe { subversion_sys::svn_repos_path(self.ptr, pool.as_mut_ptr()) };
            let path = unsafe { std::ffi::CStr::from_ptr(ret) };
            std::path::PathBuf::from(path.to_str().unwrap())
        })
    }

    /// Gets the path to the database environment.
    pub fn db_env(&mut self) -> std::path::PathBuf {
        let pool = apr::Pool::new();
        let ret = unsafe { subversion_sys::svn_repos_db_env(self.ptr, pool.as_mut_ptr()) };
        let db_env = unsafe { std::ffi::CStr::from_ptr(ret) };
        std::path::PathBuf::from(db_env.to_str().unwrap())
    }

    /// Gets the path to the configuration directory.
    pub fn conf_dir(&mut self) -> std::path::PathBuf {
        let pool = apr::Pool::new();
        let ret = unsafe { subversion_sys::svn_repos_conf_dir(self.ptr, pool.as_mut_ptr()) };
        let conf_dir = unsafe { std::ffi::CStr::from_ptr(ret) };
        std::path::PathBuf::from(conf_dir.to_str().unwrap())
    }

    /// Gets the path to the svnserve configuration file.
    pub fn svnserve_conf(&mut self) -> std::path::PathBuf {
        let pool = apr::Pool::new();
        let ret = unsafe { subversion_sys::svn_repos_svnserve_conf(self.ptr, pool.as_mut_ptr()) };
        let svnserve_conf = unsafe { std::ffi::CStr::from_ptr(ret) };
        std::path::PathBuf::from(svnserve_conf.to_str().unwrap())
    }

    /// Gets the path to the lock directory.
    pub fn lock_dir(&mut self) -> std::path::PathBuf {
        let pool = apr::Pool::new();
        let ret = unsafe { subversion_sys::svn_repos_lock_dir(self.ptr, pool.as_mut_ptr()) };
        let lock_dir = unsafe { std::ffi::CStr::from_ptr(ret) };
        std::path::PathBuf::from(lock_dir.to_str().unwrap())
    }

    /// Gets the path to the database lock file.
    pub fn db_lockfile(&mut self) -> std::path::PathBuf {
        let pool = apr::Pool::new();
        let ret = unsafe { subversion_sys::svn_repos_db_lockfile(self.ptr, pool.as_mut_ptr()) };
        let db_lockfile = unsafe { std::ffi::CStr::from_ptr(ret) };
        std::path::PathBuf::from(db_lockfile.to_str().unwrap())
    }

    /// Gets the path to the database logs lock file.
    pub fn db_logs_lockfile(&mut self) -> std::path::PathBuf {
        let pool = apr::Pool::new();
        let ret =
            unsafe { subversion_sys::svn_repos_db_logs_lockfile(self.ptr, pool.as_mut_ptr()) };
        let logs_lockfile = unsafe { std::ffi::CStr::from_ptr(ret) };
        std::path::PathBuf::from(logs_lockfile.to_str().unwrap())
    }

    /// Gets the path to the repository's hooks directory.
    pub fn hook_dir(&mut self) -> std::path::PathBuf {
        let pool = apr::Pool::new();
        let ret = unsafe { subversion_sys::svn_repos_hook_dir(self.ptr, pool.as_mut_ptr()) };
        let hook_dir = unsafe { std::ffi::CStr::from_ptr(ret) };
        std::path::PathBuf::from(hook_dir.to_str().unwrap())
    }

    /// Loads a repository from a dump stream.
    pub fn load_fs(
        &self,
        dumpstream: &mut crate::io::Stream,
        start_rev: Option<Revnum>,
        end_rev: Option<Revnum>,
        uuid_action: LoadUUID,
        parent_dir: &std::path::Path,
        use_pre_commit_hook: bool,
        use_post_commit_hook: bool,
        cancel_check: Option<&impl Fn() -> bool>,
        validate_props: bool,
        ignore_dates: bool,
        normalize_props: bool,
        notify_func: Option<&impl Fn(&Notify)>,
    ) -> Result<(), Error<'static>> {
        let pool = apr::Pool::new();

        // Handle empty parent_dir properly to avoid segfault
        let parent_dir_cstring = if parent_dir.as_os_str().is_empty() {
            None
        } else {
            Some(std::ffi::CString::new(parent_dir.to_str().unwrap())?)
        };
        let parent_dir_cstr = match &parent_dir_cstring {
            Some(cstr) => cstr.as_ptr(),
            None => std::ptr::null(),
        };

        let ret = unsafe {
            subversion_sys::svn_repos_load_fs6(
                self.ptr,
                dumpstream.as_mut_ptr(),
                start_rev.map(|r| r.0).unwrap_or(-1), // SVN_INVALID_REVNUM
                end_rev.map(|r| r.0).unwrap_or(-1),   // SVN_INVALID_REVNUM
                uuid_action.into(),
                parent_dir_cstr,
                use_pre_commit_hook.into(),
                use_post_commit_hook.into(),
                validate_props.into(),
                ignore_dates.into(),
                normalize_props.into(),
                if notify_func.is_some() {
                    Some(wrap_notify_func)
                } else {
                    None
                },
                notify_func
                    .map(|notify_func| {
                        let boxed: Box<dyn FnMut(&Notify)> = Box::new(move |n| notify_func(n));
                        Box::into_raw(Box::new(boxed)) as *mut std::ffi::c_void
                    })
                    .unwrap_or(std::ptr::null_mut()),
                if cancel_check.is_some() {
                    Some(crate::wrap_cancel_func)
                } else {
                    None
                },
                cancel_check
                    .map(|cancel_check| {
                        let boxed: Box<dyn Fn() -> Result<(), Error<'static>>> =
                            Box::new(move || {
                                if cancel_check() {
                                    Err(Error::from_message("Operation cancelled"))
                                } else {
                                    Ok(())
                                }
                            });
                        Box::into_raw(Box::new(boxed)) as *mut std::ffi::c_void
                    })
                    .unwrap_or(std::ptr::null_mut()),
                pool.as_mut_ptr(),
            )
        };
        Error::from_raw(ret)?;
        Ok(())
    }

    /// Load revision properties from a dumpstream into this repository.
    ///
    /// This is similar to [`load_fs`](Repos::load_fs) but only loads revision
    /// properties from the dumpstream, not path changes.
    ///
    /// Wraps `svn_repos_load_fs_revprops`.
    pub fn load_fs_revprops(
        &self,
        dumpstream: &mut crate::io::Stream,
        start_rev: Option<Revnum>,
        end_rev: Option<Revnum>,
        validate_props: bool,
        ignore_dates: bool,
        normalize_props: bool,
        notify_func: Option<&impl Fn(&Notify)>,
        cancel_check: Option<&impl Fn() -> bool>,
    ) -> Result<(), Error<'static>> {
        let pool = apr::Pool::new();
        let ret = unsafe {
            subversion_sys::svn_repos_load_fs_revprops(
                self.ptr,
                dumpstream.as_mut_ptr(),
                start_rev.map(|r| r.0).unwrap_or(-1),
                end_rev.map(|r| r.0).unwrap_or(-1),
                validate_props.into(),
                ignore_dates.into(),
                normalize_props.into(),
                if notify_func.is_some() {
                    Some(wrap_notify_func)
                } else {
                    None
                },
                notify_func
                    .map(|notify_func| {
                        let boxed: Box<dyn FnMut(&Notify)> = Box::new(move |n| notify_func(n));
                        Box::into_raw(Box::new(boxed)) as *mut std::ffi::c_void
                    })
                    .unwrap_or(std::ptr::null_mut()),
                if cancel_check.is_some() {
                    Some(crate::wrap_cancel_func)
                } else {
                    None
                },
                cancel_check
                    .map(|cancel_check| {
                        let boxed: Box<dyn Fn() -> Result<(), Error<'static>>> =
                            Box::new(move || {
                                if cancel_check() {
                                    Err(Error::from_message("Operation cancelled"))
                                } else {
                                    Ok(())
                                }
                            });
                        Box::into_raw(Box::new(boxed)) as *mut std::ffi::c_void
                    })
                    .unwrap_or(std::ptr::null_mut()),
                pool.as_mut_ptr(),
            )
        };
        Error::from_raw(ret)?;
        Ok(())
    }

    /// Lock a single path in the repository filesystem, running pre/post lock hooks.
    ///
    /// Returns the resulting [`crate::Lock`] on success. If `token` is `None`, a new lock
    /// token will be generated. If `current_rev` is `Revnum::INVALID`, no out-of-dateness
    /// check is performed.
    ///
    /// Wraps `svn_repos_fs_lock`.
    pub fn fs_lock(
        &mut self,
        path: &str,
        token: Option<&str>,
        comment: Option<&str>,
        is_dav_comment: bool,
        expiration_date: Option<i64>,
        current_rev: Revnum,
        steal_lock: bool,
    ) -> Result<crate::Lock<'static>, Error<'static>> {
        let path_cstr = std::ffi::CString::new(path).unwrap();
        let token_cstr = token.map(|t| std::ffi::CString::new(t).unwrap());
        let token_ptr = token_cstr.as_ref().map_or(std::ptr::null(), |t| t.as_ptr());
        let comment_cstr = comment.map(|c| std::ffi::CString::new(c).unwrap());
        let comment_ptr = comment_cstr
            .as_ref()
            .map_or(std::ptr::null(), |c| c.as_ptr());

        let pool = apr::Pool::new();
        let mut lock_ptr: *mut subversion_sys::svn_lock_t = std::ptr::null_mut();

        let ret = unsafe {
            subversion_sys::svn_repos_fs_lock(
                &mut lock_ptr,
                self.ptr,
                path_cstr.as_ptr(),
                token_ptr,
                comment_ptr,
                is_dav_comment as i32,
                expiration_date.unwrap_or(0),
                current_rev.0,
                steal_lock as i32,
                pool.as_mut_ptr(),
            )
        };
        Error::from_raw(ret)?;
        let pool_handle = apr::PoolHandle::owned(pool);
        Ok(crate::Lock::from_raw(lock_ptr, pool_handle))
    }

    /// Lock multiple paths in the repository filesystem, running pre/post lock hooks.
    ///
    /// `targets` is a slice of `(path, token, current_rev)` tuples. If `token` is `None`, a
    /// new token is generated for that path. If `current_rev` is `Revnum::INVALID`, no
    /// out-of-dateness check is performed for that path.
    ///
    /// For each path (or error) `callback` is invoked with `(path, error_or_none)`.
    ///
    /// Wraps `svn_repos_fs_lock_many`.
    pub fn fs_lock_many(
        &mut self,
        targets: &[(&str, Option<&str>, Revnum)],
        comment: Option<&str>,
        is_dav_comment: bool,
        expiration_date: Option<i64>,
        steal_lock: bool,
        mut callback: impl FnMut(&str, Option<Error<'_>>),
    ) -> Result<(), Error<'static>> {
        struct Baton<'a> {
            func: &'a mut dyn FnMut(&str, Option<Error<'_>>),
        }

        unsafe extern "C" fn lock_callback(
            baton: *mut std::ffi::c_void,
            path: *const std::os::raw::c_char,
            _lock: *const subversion_sys::svn_lock_t,
            fs_err: *mut subversion_sys::svn_error_t,
            _scratch_pool: *mut apr_sys::apr_pool_t,
        ) -> *mut subversion_sys::svn_error_t {
            let baton = &mut *(baton as *mut Baton<'_>);
            let path_str = std::ffi::CStr::from_ptr(path).to_str().unwrap_or("");
            let error = if fs_err.is_null() {
                None
            } else {
                crate::svn_result(fs_err).err()
            };
            (baton.func)(path_str, error);
            std::ptr::null_mut()
        }

        let result_pool = apr::Pool::new();
        let scratch_pool = apr::Pool::new();
        let result_pool_ptr = result_pool.as_mut_ptr();
        let scratch_pool_ptr = scratch_pool.as_mut_ptr();

        let comment_cstr = comment.map(|c| std::ffi::CString::new(c).unwrap());
        let comment_ptr = comment_cstr
            .as_ref()
            .map_or(std::ptr::null(), |c| c.as_ptr());

        let path_cstrings: Vec<std::ffi::CString> = targets
            .iter()
            .map(|(p, _, _)| std::ffi::CString::new(*p).unwrap())
            .collect();
        let token_cstrings: Vec<Option<std::ffi::CString>> = targets
            .iter()
            .map(|(_, t, _)| t.map(|t| std::ffi::CString::new(t).unwrap()))
            .collect();

        unsafe {
            let hash = apr_sys::apr_hash_make(scratch_pool_ptr);
            for (i, (_, _, rev)) in targets.iter().enumerate() {
                let token_ptr = token_cstrings[i]
                    .as_ref()
                    .map_or(std::ptr::null(), |t| t.as_ptr());
                let target =
                    subversion_sys::svn_fs_lock_target_create(token_ptr, rev.0, result_pool_ptr);
                apr_sys::apr_hash_set(
                    hash,
                    path_cstrings[i].as_ptr() as *const std::ffi::c_void,
                    apr_sys::APR_HASH_KEY_STRING as isize,
                    target as *mut std::ffi::c_void,
                );
            }

            let mut baton = Baton {
                func: &mut callback,
            };
            let err = subversion_sys::svn_repos_fs_lock_many(
                self.ptr,
                hash,
                comment_ptr,
                is_dav_comment as i32,
                expiration_date.unwrap_or(0),
                steal_lock as i32,
                Some(lock_callback),
                &mut baton as *mut Baton<'_> as *mut std::ffi::c_void,
                result_pool_ptr,
                scratch_pool_ptr,
            );
            Error::from_raw(err)?;
        }
        Ok(())
    }

    /// Unlock a single path in the repository filesystem, running pre/post unlock hooks.
    ///
    /// Wraps `svn_repos_fs_unlock`.
    pub fn fs_unlock(
        &mut self,
        path: &str,
        token: &str,
        break_lock: bool,
    ) -> Result<(), Error<'static>> {
        let path_cstr = std::ffi::CString::new(path).unwrap();
        let token_cstr = std::ffi::CString::new(token).unwrap();
        let pool = apr::Pool::new();
        let ret = unsafe {
            subversion_sys::svn_repos_fs_unlock(
                self.ptr,
                path_cstr.as_ptr(),
                token_cstr.as_ptr(),
                break_lock as i32,
                pool.as_mut_ptr(),
            )
        };
        Error::from_raw(ret)?;
        Ok(())
    }

    /// Unlock multiple paths in the repository filesystem, running pre/post unlock hooks.
    ///
    /// `targets` is a slice of `(path, token)` pairs. For each path (or error) `callback`
    /// is invoked with `(path, error_or_none)`.
    ///
    /// Wraps `svn_repos_fs_unlock_many`.
    pub fn fs_unlock_many(
        &mut self,
        targets: &[(&str, &str)],
        break_lock: bool,
        mut callback: impl FnMut(&str, Option<Error<'_>>),
    ) -> Result<(), Error<'static>> {
        struct Baton<'a> {
            func: &'a mut dyn FnMut(&str, Option<Error<'_>>),
        }

        unsafe extern "C" fn unlock_callback(
            baton: *mut std::ffi::c_void,
            path: *const std::os::raw::c_char,
            _lock: *const subversion_sys::svn_lock_t,
            fs_err: *mut subversion_sys::svn_error_t,
            _scratch_pool: *mut apr_sys::apr_pool_t,
        ) -> *mut subversion_sys::svn_error_t {
            let baton = &mut *(baton as *mut Baton<'_>);
            let path_str = std::ffi::CStr::from_ptr(path).to_str().unwrap_or("");
            let error = if fs_err.is_null() {
                None
            } else {
                crate::svn_result(fs_err).err()
            };
            (baton.func)(path_str, error);
            std::ptr::null_mut()
        }

        let result_pool = apr::Pool::new();
        let scratch_pool = apr::Pool::new();
        let result_pool_ptr = result_pool.as_mut_ptr();
        let scratch_pool_ptr = scratch_pool.as_mut_ptr();

        let path_cstrings: Vec<std::ffi::CString> = targets
            .iter()
            .map(|(p, _)| std::ffi::CString::new(*p).unwrap())
            .collect();
        let token_cstrings: Vec<std::ffi::CString> = targets
            .iter()
            .map(|(_, t)| std::ffi::CString::new(*t).unwrap())
            .collect();

        unsafe {
            let hash = apr_sys::apr_hash_make(scratch_pool_ptr);
            for (i, _) in targets.iter().enumerate() {
                apr_sys::apr_hash_set(
                    hash,
                    path_cstrings[i].as_ptr() as *const std::ffi::c_void,
                    apr_sys::APR_HASH_KEY_STRING as isize,
                    token_cstrings[i].as_ptr() as *mut std::ffi::c_void,
                );
            }

            let mut baton = Baton {
                func: &mut callback,
            };
            let err = subversion_sys::svn_repos_fs_unlock_many(
                self.ptr,
                hash,
                break_lock as i32,
                Some(unlock_callback),
                &mut baton as *mut Baton<'_> as *mut std::ffi::c_void,
                result_pool_ptr,
                scratch_pool_ptr,
            );
            Error::from_raw(err)?;
        }
        Ok(())
    }

    /// Get all locks on paths at or under `path` in the repository filesystem.
    ///
    /// `depth` controls how deeply to recurse. The optional `authz_read_func` can filter
    /// which paths are accessible; pass `None` to allow all paths.
    ///
    /// Returns a `Vec` of [`crate::Lock`] objects.
    ///
    /// Wraps `svn_repos_fs_get_locks2`.
    pub fn fs_get_locks(
        &self,
        path: &str,
        depth: crate::Depth,
        authz_read_func: Option<
            Box<dyn FnMut(&crate::fs::Root, &str) -> Result<bool, Error<'static>>>,
        >,
    ) -> Result<Vec<crate::Lock<'static>>, Error<'static>> {
        let path_cstr = std::ffi::CString::new(path).unwrap();
        let pool = apr::Pool::new();

        let has_authz = authz_read_func.is_some();
        let authz_baton = authz_read_func
            .map(|callback| Box::into_raw(Box::new(callback)) as *mut std::ffi::c_void)
            .unwrap_or(std::ptr::null_mut());

        extern "C" fn wrap_authz_read_func(
            allowed: *mut i32,
            root: *mut subversion_sys::svn_fs_root_t,
            path: *const i8,
            baton: *mut std::ffi::c_void,
            pool: *mut apr_sys::apr_pool_t,
        ) -> *mut subversion_sys::svn_error_t {
            if baton.is_null() || allowed.is_null() {
                return std::ptr::null_mut();
            }
            let callback = unsafe {
                &mut *(baton
                    as *mut Box<dyn FnMut(&crate::fs::Root, &str) -> Result<bool, Error<'static>>>)
            };
            let path_str = unsafe {
                if path.is_null() {
                    ""
                } else {
                    std::ffi::CStr::from_ptr(path).to_str().unwrap_or("")
                }
            };
            let fs_root = unsafe { crate::fs::Root::from_raw(root, pool) };
            match callback(&fs_root, path_str) {
                Ok(is_allowed) => {
                    unsafe {
                        *allowed = if is_allowed { 1 } else { 0 };
                    }
                    std::ptr::null_mut()
                }
                Err(mut e) => unsafe { e.detach() },
            }
        }

        let mut locks_hash: *mut apr_sys::apr_hash_t = std::ptr::null_mut();

        let ret = unsafe {
            subversion_sys::svn_repos_fs_get_locks2(
                &mut locks_hash,
                self.ptr,
                path_cstr.as_ptr(),
                depth.into(),
                if has_authz {
                    Some(wrap_authz_read_func)
                } else {
                    None
                },
                authz_baton,
                pool.as_mut_ptr(),
            )
        };
        Error::from_raw(ret)?;

        let mut locks = Vec::new();
        if !locks_hash.is_null() {
            unsafe {
                let mut hi = apr_sys::apr_hash_first(pool.as_mut_ptr(), locks_hash);
                while !hi.is_null() {
                    let mut val_ptr: *mut std::ffi::c_void = std::ptr::null_mut();
                    apr_sys::apr_hash_this(
                        hi,
                        std::ptr::null_mut(),
                        std::ptr::null_mut(),
                        &mut val_ptr,
                    );
                    if !val_ptr.is_null() {
                        let lock_ptr = val_ptr as *const subversion_sys::svn_lock_t;
                        // Duplicate the lock into its own pool so it outlives the scratch pool
                        let lock_pool = apr::Pool::new();
                        let dup_ptr =
                            subversion_sys::svn_lock_dup(lock_ptr, lock_pool.as_mut_ptr());
                        if !dup_ptr.is_null() {
                            let pool_handle = apr::PoolHandle::owned(lock_pool);
                            locks.push(crate::Lock::from_raw(dup_ptr, pool_handle));
                        }
                    }
                    hi = apr_sys::apr_hash_next(hi);
                }
            }
        }
        Ok(locks)
    }

    /// Get the inherited properties for a path in a repository filesystem root.
    ///
    /// Returns the properties inherited by `path` in `root`, optionally filtered
    /// to a single property name via `propname`.  Each entry in the returned vector
    /// contains:
    /// * The path (relative to the repository root) from which the properties are
    ///   inherited, e.g. `""` for the root, `"trunk"` for a parent directory.
    /// * A [`HashMap`] of property name → value pairs for that ancestor.
    ///
    /// Entries are ordered from the repository root outward to `path`.
    ///
    /// Wraps `svn_repos_fs_get_inherited_props`.
    pub fn fs_get_inherited_props(
        &self,
        root: &mut crate::fs::Root,
        path: &str,
        propname: Option<&str>,
    ) -> Result<Vec<(String, std::collections::HashMap<String, Vec<u8>>)>, Error<'static>> {
        let path_cstr = std::ffi::CString::new(path).unwrap();
        let propname_cstr =
            propname.map(|p| std::ffi::CString::new(p).expect("propname must be valid UTF-8"));

        let result_pool = apr::Pool::new();
        let scratch_pool = apr::Pool::new();

        let mut inherited_props: *mut apr_sys::apr_array_header_t = std::ptr::null_mut();

        let err = unsafe {
            subversion_sys::svn_repos_fs_get_inherited_props(
                &mut inherited_props,
                root.as_mut_ptr(),
                path_cstr.as_ptr(),
                propname_cstr
                    .as_ref()
                    .map_or(std::ptr::null(), |c| c.as_ptr()),
                None,                 // authz_read_func
                std::ptr::null_mut(), // authz_read_baton
                result_pool.as_mut_ptr(),
                scratch_pool.as_mut_ptr(),
            )
        };
        svn_result(err)?;

        let mut result = Vec::new();
        if !inherited_props.is_null() {
            let array = unsafe {
                apr::tables::TypedArray::<*mut subversion_sys::svn_prop_inherited_item_t>::from_ptr(
                    inherited_props,
                )
            };
            for item_ptr in array.iter() {
                if item_ptr.is_null() {
                    continue;
                }
                unsafe {
                    let item = *item_ptr;
                    let path_or_url = 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((path_or_url, props));
                }
            }
        }
        Ok(result)
    }

    /// Verifies the repository filesystem.
    pub fn verify_fs(
        &self,
        start_rev: Revnum,
        end_rev: Revnum,
        check_normalization: bool,
        metadata_only: bool,
        notify_func: Option<&impl Fn(&Notify)>,
        callback_func: &impl Fn(Revnum, &Error) -> Result<(), Error<'static>>,
        cancel_func: Option<&impl Fn() -> Result<(), Error<'static>>>,
    ) -> Result<(), Error<'static>> {
        extern "C" fn verify_callback(
            baton: *mut std::ffi::c_void,
            revision: subversion_sys::svn_revnum_t,
            verify_err: *mut subversion_sys::svn_error_t,
            _pool: *mut apr_sys::apr_pool_t,
        ) -> *mut subversion_sys::svn_error_t {
            let baton = unsafe {
                &mut *(baton as *mut Box<dyn FnMut(Revnum, &Error) -> Result<(), Error<'static>>>)
            };
            let verify_err = Error::from_raw(verify_err).unwrap_err();
            match baton(Revnum::from_raw(revision).unwrap(), &verify_err) {
                Ok(()) => std::ptr::null_mut(),
                Err(e) => unsafe { e.into_raw() },
            }
        }
        let pool = apr::Pool::new();
        let ret = unsafe {
            subversion_sys::svn_repos_verify_fs3(
                self.ptr,
                start_rev.0,
                end_rev.0,
                check_normalization.into(),
                metadata_only.into(),
                if notify_func.is_some() {
                    Some(wrap_notify_func)
                } else {
                    None
                },
                notify_func
                    .map(|notify_func| {
                        let boxed: Box<dyn FnMut(&Notify)> = Box::new(move |n| notify_func(n));
                        Box::into_raw(Box::new(boxed)) as *mut std::ffi::c_void
                    })
                    .unwrap_or(std::ptr::null_mut()),
                Some(verify_callback),
                Box::into_raw(Box::new(callback_func)) as *mut std::ffi::c_void,
                if cancel_func.is_some() {
                    Some(crate::wrap_cancel_func)
                } else {
                    None
                },
                cancel_func
                    .map(|cancel_func| {
                        let boxed: Box<dyn Fn() -> Result<(), Error<'static>>> =
                            Box::new(cancel_func);
                        Box::into_raw(Box::new(boxed)) as *mut std::ffi::c_void
                    })
                    .unwrap_or(std::ptr::null_mut()),
                pool.as_mut_ptr(),
            )
        };
        Error::from_raw(ret)?;
        Ok(())
    }

    /// Packs the repository filesystem.
    pub fn pack_fs(
        &self,
        notify_func: Option<&impl Fn(&Notify)>,
        cancel_func: Option<&impl Fn() -> Result<(), Error<'static>>>,
    ) -> Result<(), Error<'static>> {
        let pool = apr::Pool::new();
        let ret = unsafe {
            subversion_sys::svn_repos_fs_pack2(
                self.ptr,
                if notify_func.is_some() {
                    Some(wrap_notify_func)
                } else {
                    None
                },
                notify_func
                    .map(|notify_func| {
                        let boxed: Box<dyn FnMut(&Notify)> = Box::new(move |n| notify_func(n));
                        Box::into_raw(Box::new(boxed)) as *mut std::ffi::c_void
                    })
                    .unwrap_or(std::ptr::null_mut()),
                if cancel_func.is_some() {
                    Some(crate::wrap_cancel_func)
                } else {
                    None
                },
                cancel_func
                    .map(|cancel_func| {
                        let boxed: Box<dyn Fn() -> Result<(), Error<'static>>> =
                            Box::new(cancel_func);
                        Box::into_raw(Box::new(boxed)) as *mut std::ffi::c_void
                    })
                    .unwrap_or(std::ptr::null_mut()),
                pool.as_mut_ptr(),
            )
        };
        Error::from_raw(ret)?;
        Ok(())
    }

    /// Get the UUID of the repository
    pub fn uuid(&self) -> Result<String, Error<'static>> {
        let pool = apr::Pool::new();
        let mut uuid_ptr = std::ptr::null();
        let ret = unsafe {
            subversion_sys::svn_fs_get_uuid(
                subversion_sys::svn_repos_fs(self.ptr),
                &mut uuid_ptr,
                pool.as_mut_ptr(),
            )
        };
        Error::from_raw(ret)?;

        if uuid_ptr.is_null() {
            Ok(String::new())
        } else {
            let uuid_cstr = unsafe { std::ffi::CStr::from_ptr(uuid_ptr) };
            Ok(uuid_cstr.to_str()?.to_string())
        }
    }

    /// Set the UUID of the repository
    pub fn set_uuid(&self, uuid: &str) -> Result<(), Error<'static>> {
        let pool = apr::Pool::new();
        let uuid_cstr = std::ffi::CString::new(uuid)?;
        let ret = unsafe {
            subversion_sys::svn_fs_set_uuid(
                subversion_sys::svn_repos_fs(self.ptr),
                uuid_cstr.as_ptr(),
                pool.as_mut_ptr(),
            )
        };
        Error::from_raw(ret)?;
        Ok(())
    }

    /// Get the youngest revision in the repository
    pub fn youngest_rev(&self) -> Result<Revnum, Error<'static>> {
        let pool = apr::Pool::new();
        let mut revnum: subversion_sys::svn_revnum_t = 0;
        let ret = unsafe {
            subversion_sys::svn_fs_youngest_rev(
                &mut revnum,
                subversion_sys::svn_repos_fs(self.ptr),
                pool.as_mut_ptr(),
            )
        };
        Error::from_raw(ret)?;
        Ok(Revnum(revnum))
    }

    /// Begin a transaction
    pub fn begin_txn_for_commit(
        &self,
        revnum: Revnum,
        author: &str,
        log_msg: &str,
    ) -> Result<crate::fs::Transaction<'_>, Error<'static>> {
        let pool = apr::Pool::new();
        let mut txn_ptr = std::ptr::null_mut();

        // Create revision properties
        let mut revprop_table = apr::hash::Hash::new(&pool);

        // Add author
        let author_key = b"svn:author";
        let author_val = crate::svn_string_helpers::svn_string_ncreate(author.as_bytes(), &pool);
        unsafe {
            revprop_table.insert(author_key, author_val as *mut std::ffi::c_void);
        }

        // Add log message
        let log_key = b"svn:log";
        let log_val = crate::svn_string_helpers::svn_string_ncreate(log_msg.as_bytes(), &pool);
        unsafe {
            revprop_table.insert(log_key, log_val as *mut std::ffi::c_void);
        }

        let ret = unsafe {
            subversion_sys::svn_repos_fs_begin_txn_for_commit2(
                &mut txn_ptr,
                self.ptr,
                revnum.0,
                revprop_table.as_mut_ptr(),
                pool.as_mut_ptr(),
            )
        };
        Error::from_raw(ret)?;

        Ok(unsafe { crate::fs::Transaction::from_ptr_and_pool(txn_ptr, pool) })
    }

    /// Commit a transaction through the repository layer, running pre-commit and
    /// post-commit hooks.
    ///
    /// Returns `(new_rev, conflict_path)` where `conflict_path` is `Some` if a
    /// conflict prevented the commit (in that case `new_rev` is
    /// `SVN_INVALID_REVNUM`).
    ///
    /// Wraps `svn_repos_fs_commit_txn`.
    pub fn commit_txn(
        &self,
        txn: crate::fs::Transaction<'_>,
    ) -> Result<(Revnum, Option<String>), Error<'static>> {
        let pool = apr::Pool::new();
        let mut new_rev: subversion_sys::svn_revnum_t = -1; // SVN_INVALID_REVNUM
        let mut conflict_p: *const std::os::raw::c_char = std::ptr::null();
        let err = unsafe {
            subversion_sys::svn_repos_fs_commit_txn(
                &mut conflict_p,
                self.ptr,
                &mut new_rev,
                txn.as_ptr(),
                pool.as_mut_ptr(),
            )
        };
        svn_result(err)?;
        let conflict = if conflict_p.is_null() {
            None
        } else {
            Some(
                unsafe { std::ffi::CStr::from_ptr(conflict_p) }
                    .to_string_lossy()
                    .into_owned(),
            )
        };
        Ok((Revnum(new_rev), conflict))
    }

    /// Get revision properties
    pub fn rev_proplist(
        &self,
        revnum: Revnum,
    ) -> Result<std::collections::HashMap<String, Vec<u8>>, Error<'_>> {
        let pool = apr::Pool::new();
        let mut props_ptr = std::ptr::null_mut();

        // Use authz_read_func that always allows access for simplicity
        extern "C" fn authz_read_func(
            _allowed: *mut i32,
            _root: *mut subversion_sys::svn_fs_root_t,
            _path: *const std::ffi::c_char,
            _baton: *mut std::ffi::c_void,
            _pool: *mut apr_sys::apr_pool_t,
        ) -> *mut subversion_sys::svn_error_t {
            unsafe {
                *_allowed = 1; // Always allow
            }
            std::ptr::null_mut() // No error
        }

        let ret = unsafe {
            subversion_sys::svn_repos_fs_revision_proplist(
                &mut props_ptr,
                self.ptr,
                revnum.0,
                Some(authz_read_func),
                std::ptr::null_mut(),
                pool.as_mut_ptr(),
            )
        };
        Error::from_raw(ret)?;

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

    /// Get a single revision property.  Returns `None` if the property is not
    /// set on this revision.
    ///
    /// Wraps `svn_repos_fs_revision_prop`.
    pub fn rev_prop(&self, revnum: Revnum, name: &str) -> Result<Option<Vec<u8>>, Error<'static>> {
        let name_cstr = std::ffi::CString::new(name)?;

        // Use an always-allow authz function so properties are not suppressed.
        extern "C" fn authz_always_allow(
            allowed: *mut subversion_sys::svn_boolean_t,
            _root: *mut subversion_sys::svn_fs_root_t,
            _path: *const std::os::raw::c_char,
            _baton: *mut std::ffi::c_void,
            _pool: *mut apr_sys::apr_pool_t,
        ) -> *mut subversion_sys::svn_error_t {
            unsafe { *allowed = 1 };
            std::ptr::null_mut()
        }

        with_tmp_pool(|pool| {
            let mut value_p: *mut subversion_sys::svn_string_t = std::ptr::null_mut();
            let err = unsafe {
                subversion_sys::svn_repos_fs_revision_prop(
                    &mut value_p,
                    self.ptr,
                    revnum.0,
                    name_cstr.as_ptr(),
                    Some(authz_always_allow),
                    std::ptr::null_mut(),
                    pool.as_mut_ptr(),
                )
            };
            svn_result(err)?;
            if value_p.is_null() {
                return Ok(None);
            }
            let data = unsafe {
                let s = &*value_p;
                std::slice::from_raw_parts(s.data as *const u8, s.len).to_vec()
            };
            Ok(Some(data))
        })
    }

    /// Change a revision property
    pub fn change_rev_prop(
        &self,
        revnum: Revnum,
        name: &str,
        value: Option<&[u8]>,
    ) -> Result<(), Error<'static>> {
        let pool = apr::Pool::new();
        let name_cstr = std::ffi::CString::new(name)?;

        let value_ptr = if let Some(val) = value {
            crate::svn_string_helpers::svn_string_ncreate(val, &pool)
        } else {
            std::ptr::null_mut()
        };

        // Use authz_read_func that always allows access
        extern "C" fn authz_read_func(
            _allowed: *mut i32,
            _root: *mut subversion_sys::svn_fs_root_t,
            _path: *const std::ffi::c_char,
            _baton: *mut std::ffi::c_void,
            _pool: *mut apr_sys::apr_pool_t,
        ) -> *mut subversion_sys::svn_error_t {
            unsafe {
                *_allowed = 1; // Always allow
            }
            std::ptr::null_mut()
        }

        let ret = unsafe {
            subversion_sys::svn_repos_fs_change_rev_prop4(
                self.ptr,
                revnum.0,
                std::ptr::null(), // author
                name_cstr.as_ptr(),
                std::ptr::null_mut(), // old_value_p
                value_ptr,
                1, // use_pre_revprop_change_hook
                1, // use_post_revprop_change_hook
                Some(authz_read_func),
                std::ptr::null_mut(),
                pool.as_mut_ptr(),
            )
        };
        Error::from_raw(ret)?;
        Ok(())
    }

    /// Dump repository contents to a stream for backup
    pub fn dump(
        &self,
        stream: &mut crate::io::Stream,
        options: &mut DumpOptions,
    ) -> Result<(), Error<'static>> {
        let pool = apr::Pool::new();
        let notify_baton = options
            .notify_func
            .map(|notify_func| {
                let boxed: Box<dyn FnMut(&Notify)> = Box::new(move |n| notify_func(n));
                Box::into_raw(Box::new(boxed)) as *mut std::ffi::c_void
            })
            .unwrap_or(std::ptr::null_mut());
        let has_filter = options.filter_func.is_some();
        let filter_baton = options
            .filter_func
            .take()
            .map(|f| Box::into_raw(Box::new(f)) as *mut std::ffi::c_void)
            .unwrap_or(std::ptr::null_mut());
        let cancel_baton = options
            .cancel_func
            .map(|cancel_func| {
                let boxed: Box<dyn Fn() -> Result<(), Error<'static>>> = Box::new(cancel_func);
                Box::into_raw(Box::new(boxed)) as *mut std::ffi::c_void
            })
            .unwrap_or(std::ptr::null_mut());

        let ret = unsafe {
            svn_repos_dump_fs4(
                self.ptr,
                stream.as_mut_ptr(),
                options.start_rev.map(|r| r.into()).unwrap_or(-1),
                options.end_rev.map(|r| r.into()).unwrap_or(-1),
                options.incremental.into(),
                options.use_deltas.into(),
                options.include_revprops.into(),
                options.include_changes.into(),
                if options.notify_func.is_some() {
                    Some(wrap_notify_func)
                } else {
                    None
                },
                notify_baton,
                if has_filter {
                    Some(wrap_filter_func)
                } else {
                    None
                },
                filter_baton,
                if options.cancel_func.is_some() {
                    Some(crate::wrap_cancel_func)
                } else {
                    None
                },
                cancel_baton,
                pool.as_mut_ptr(),
            )
        };

        // Free callback batons
        if !notify_baton.is_null() {
            unsafe { drop(Box::from_raw(notify_baton as *mut Box<dyn Fn(&Notify)>)) };
        }
        if !filter_baton.is_null() {
            unsafe {
                drop(Box::from_raw(
                    filter_baton
                        as *mut Box<
                            dyn FnMut(&crate::fs::Root, &str) -> Result<bool, Error<'static>>,
                        >,
                ));
            }
        }
        if !cancel_baton.is_null() {
            unsafe {
                drop(Box::from_raw(
                    cancel_baton as *mut Box<dyn Fn() -> Result<(), Error<'static>>>,
                ))
            };
        }

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

    /// Load repository contents from a dump stream for restoration
    pub fn load(
        &self,
        dumpstream: &mut crate::io::Stream,
        options: &LoadOptions,
    ) -> Result<(), Error<'static>> {
        let pool = apr::Pool::new();
        let parent_dir_cstr = options
            .parent_dir
            .map(|p| std::ffi::CString::new(p).unwrap());
        let parent_dir_ptr = parent_dir_cstr
            .as_ref()
            .map(|s| s.as_ptr())
            .unwrap_or(std::ptr::null());

        let notify_baton = options
            .notify_func
            .map(|notify_func| {
                let boxed: Box<dyn FnMut(&Notify)> = Box::new(move |n| notify_func(n));
                Box::into_raw(Box::new(boxed)) as *mut std::ffi::c_void
            })
            .unwrap_or(std::ptr::null_mut());
        let cancel_baton = options
            .cancel_func
            .map(|cancel_func| {
                let boxed: Box<dyn Fn() -> Result<(), Error<'static>>> = Box::new(cancel_func);
                Box::into_raw(Box::new(boxed)) as *mut std::ffi::c_void
            })
            .unwrap_or(std::ptr::null_mut());

        let ret = unsafe {
            svn_repos_load_fs6(
                self.ptr,
                dumpstream.as_mut_ptr(),
                options.start_rev.map(|r| r.into()).unwrap_or(-1),
                options.end_rev.map(|r| r.into()).unwrap_or(-1),
                options.uuid_action.into(),
                parent_dir_ptr,
                options.use_pre_commit_hook.into(),
                options.use_post_commit_hook.into(),
                options.validate_props.into(),
                options.ignore_dates.into(),
                options.normalize_props.into(),
                if options.notify_func.is_some() {
                    Some(wrap_notify_func)
                } else {
                    None
                },
                notify_baton,
                if options.cancel_func.is_some() {
                    Some(crate::wrap_cancel_func)
                } else {
                    None
                },
                cancel_baton,
                pool.as_mut_ptr(),
            )
        };

        // Free callback batons
        if !notify_baton.is_null() {
            unsafe { drop(Box::from_raw(notify_baton as *mut Box<dyn Fn(&Notify)>)) };
        }
        if !cancel_baton.is_null() {
            unsafe {
                drop(Box::from_raw(
                    cancel_baton as *mut Box<dyn Fn() -> Result<(), Error<'static>>>,
                ))
            };
        }

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

    /// Verify repository integrity
    pub fn verify(&self, options: &VerifyOptions) -> Result<(), Error<'static>> {
        extern "C" fn verify_error_callback(
            baton: *mut std::ffi::c_void,
            revision: subversion_sys::svn_revnum_t,
            verify_err: *mut subversion_sys::svn_error_t,
            _pool: *mut apr_sys::apr_pool_t,
        ) -> *mut subversion_sys::svn_error_t {
            let baton = unsafe {
                &mut *(baton as *mut Box<dyn FnMut(Revnum, &Error) -> Result<(), Error<'static>>>)
            };
            let verify_err = Error::from_raw(verify_err).unwrap_err();
            match baton(Revnum::from_raw(revision).unwrap(), &verify_err) {
                Ok(()) => std::ptr::null_mut(),
                Err(e) => unsafe { e.into_raw() },
            }
        }

        let pool = apr::Pool::new();
        let notify_baton = options
            .notify_func
            .map(|notify_func| {
                let boxed: Box<dyn FnMut(&Notify)> = Box::new(move |n| notify_func(n));
                Box::into_raw(Box::new(boxed)) as *mut std::ffi::c_void
            })
            .unwrap_or(std::ptr::null_mut());
        let verify_baton = options
            .verify_callback
            .map(|callback| Box::into_raw(Box::new(callback)) as *mut std::ffi::c_void)
            .unwrap_or(std::ptr::null_mut());
        let cancel_baton = options
            .cancel_func
            .map(|cancel_func| {
                let boxed: Box<dyn Fn() -> Result<(), Error<'static>>> = Box::new(cancel_func);
                Box::into_raw(Box::new(boxed)) as *mut std::ffi::c_void
            })
            .unwrap_or(std::ptr::null_mut());

        let ret = unsafe {
            svn_repos_verify_fs3(
                self.ptr,
                options.start_rev.into(),
                options.end_rev.into(),
                options.check_normalization.into(),
                options.metadata_only.into(),
                if options.notify_func.is_some() {
                    Some(wrap_notify_func)
                } else {
                    None
                },
                notify_baton,
                if options.verify_callback.is_some() {
                    Some(verify_error_callback)
                } else {
                    None
                },
                verify_baton,
                if options.cancel_func.is_some() {
                    Some(crate::wrap_cancel_func)
                } else {
                    None
                },
                cancel_baton,
                pool.as_mut_ptr(),
            )
        };

        // Free callback batons
        if !notify_baton.is_null() {
            unsafe { drop(Box::from_raw(notify_baton as *mut Box<dyn Fn(&Notify)>)) };
        }
        if !verify_baton.is_null() {
            unsafe {
                drop(Box::from_raw(
                    verify_baton as *mut Box<dyn Fn(Revnum, &Error) -> Result<(), Error<'static>>>,
                ))
            };
        }
        if !cancel_baton.is_null() {
            unsafe {
                drop(Box::from_raw(
                    cancel_baton as *mut Box<dyn Fn() -> Result<(), Error<'static>>>,
                ))
            };
        }

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

    /// Recover repository after corruption  
    pub fn recover(
        &mut self,
        nonblocking: bool,
        notify_func: Option<&dyn Fn(&Notify)>,
        cancel_func: Option<&dyn Fn() -> Result<(), Error<'static>>>,
    ) -> Result<(), Error<'static>> {
        let path = self.path();
        let path_cstr = std::ffi::CString::new(path.to_str().unwrap()).unwrap();
        let pool = apr::Pool::new();
        let notify_baton = notify_func
            .map(|notify_func| {
                let boxed: Box<dyn FnMut(&Notify)> = Box::new(move |n| notify_func(n));
                Box::into_raw(Box::new(boxed)) as *mut std::ffi::c_void
            })
            .unwrap_or(std::ptr::null_mut());
        let cancel_baton = cancel_func
            .map(|cancel_func| {
                let boxed: Box<dyn Fn() -> Result<(), Error<'static>>> = Box::new(cancel_func);
                Box::into_raw(Box::new(boxed)) as *mut std::ffi::c_void
            })
            .unwrap_or(std::ptr::null_mut());

        let ret = unsafe {
            svn_repos_recover4(
                path_cstr.as_ptr(),
                nonblocking.into(),
                if notify_func.is_some() {
                    Some(wrap_notify_func)
                } else {
                    None
                },
                notify_baton,
                if cancel_func.is_some() {
                    Some(crate::wrap_cancel_func)
                } else {
                    None
                },
                cancel_baton,
                pool.as_mut_ptr(),
            )
        };

        // Free callback batons
        if !notify_baton.is_null() {
            unsafe { drop(Box::from_raw(notify_baton as *mut Box<dyn Fn(&Notify)>)) };
        }
        if !cancel_baton.is_null() {
            unsafe {
                drop(Box::from_raw(
                    cancel_baton as *mut Box<dyn Fn() -> Result<(), Error<'static>>>,
                ))
            };
        }

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

/// Recovers a repository after an interrupted operation.
pub fn recover(
    path: &str,
    nonblocking: bool,
    notify_func: Option<&impl Fn(&Notify)>,
    cance_func: Option<&impl Fn() -> Result<(), Error<'static>>>,
) -> Result<(), Error<'static>> {
    let path = std::ffi::CString::new(path).unwrap();
    let pool = apr::Pool::new();
    let ret = unsafe {
        subversion_sys::svn_repos_recover4(
            path.as_ptr(),
            nonblocking.into(),
            if notify_func.is_some() {
                Some(wrap_notify_func)
            } else {
                None
            },
            notify_func
                .map(|notify_func| {
                    let boxed: Box<dyn FnMut(&Notify)> = Box::new(move |n| notify_func(n));
                    Box::into_raw(Box::new(boxed)) as *mut std::ffi::c_void
                })
                .unwrap_or(std::ptr::null_mut()),
            if cance_func.is_some() {
                Some(crate::wrap_cancel_func)
            } else {
                None
            },
            cance_func
                .map(|cancel_func| {
                    let boxed: Box<dyn Fn() -> Result<(), Error<'static>>> = Box::new(cancel_func);
                    Box::into_raw(Box::new(boxed)) as *mut std::ffi::c_void
                })
                .unwrap_or(std::ptr::null_mut()),
            pool.as_mut_ptr(),
        )
    };
    Error::from_raw(ret)?;
    Ok(())
}

extern "C" fn wrap_freeze_func(
    baton: *mut std::ffi::c_void,
    _pool: *mut apr_sys::apr_pool_t,
) -> *mut subversion_sys::svn_error_t {
    let freeze_func = unsafe { &*(baton as *const Box<dyn Fn() -> Result<(), Error<'static>>>) };
    match freeze_func() {
        Ok(()) => std::ptr::null_mut(),
        Err(e) => unsafe { e.into_raw() },
    }
}

/// Freezes a repository to allow safe backup or administrative operations.
///
/// This function takes an exclusive lock on each of the repositories in `paths`
/// to prevent commits, and then while holding all locks invokes `freeze_func`.
/// The callback is invoked once while all repositories are locked, not once per repository.
pub fn freeze(
    paths: &[&str],
    freeze_func: Option<&impl Fn() -> Result<(), Error<'static>>>,
) -> Result<(), Error<'static>> {
    let paths = paths
        .iter()
        .map(|p| std::ffi::CString::new(*p).unwrap())
        .collect::<Vec<_>>();
    let pool = apr::Pool::new();
    let mut paths_array = apr::tables::TypedArray::<*const i8>::new(&pool, paths.len() as i32);
    for path in paths.iter() {
        paths_array.push(path.as_ptr());
    }
    let ret = unsafe {
        subversion_sys::svn_repos_freeze(
            paths_array.as_ptr(),
            if freeze_func.is_some() {
                Some(wrap_freeze_func)
            } else {
                None
            },
            freeze_func
                .map(|freeze_func| {
                    let boxed: Box<dyn Fn() -> Result<(), Error<'static>>> = Box::new(freeze_func);
                    Box::into_raw(Box::new(boxed)) as *mut std::ffi::c_void
                })
                .unwrap_or(std::ptr::null_mut()),
            pool.as_mut_ptr(),
        )
    };
    Error::from_raw(ret)?;
    Ok(())
}

/// Notify handle - borrowed from callback
#[allow(dead_code)]
pub struct Notify<'a> {
    ptr: *const subversion_sys::svn_repos_notify_t,
    _phantom: PhantomData<&'a ()>,
}

impl<'a> Notify<'a> {
    unsafe fn from_ptr(ptr: *const subversion_sys::svn_repos_notify_t) -> Self {
        Self {
            ptr,
            _phantom: PhantomData,
        }
    }

    /// Get the revision number from the notification
    pub fn revision(&self) -> Revnum {
        unsafe { Revnum((*self.ptr).revision) }
    }

    /// Get the notification action type
    pub fn action(&self) -> u32 {
        unsafe { (*self.ptr).action }
    }

    /// Check if this is a verify_rev_end notification
    pub fn is_verify_rev_end(&self) -> bool {
        // svn_repos_notify_verify_rev_end value
        self.action() == subversion_sys::svn_repos_notify_action_t_svn_repos_notify_verify_rev_end
    }
}

/// Wrapper for dump filter callbacks
extern "C" fn wrap_filter_func(
    include: *mut i32,
    root: *mut subversion_sys::svn_fs_root_t,
    path: *const i8,
    baton: *mut std::ffi::c_void,
    pool: *mut apr_sys::apr_pool_t,
) -> *mut subversion_sys::svn_error_t {
    if baton.is_null() || include.is_null() || root.is_null() {
        return std::ptr::null_mut();
    }

    let callback = unsafe {
        &mut *(baton as *mut Box<dyn FnMut(&crate::fs::Root, &str) -> Result<bool, Error<'static>>>)
    };

    let path_str = if path.is_null() {
        ""
    } else {
        unsafe { std::ffi::CStr::from_ptr(path).to_str().unwrap_or("") }
    };

    let fs_root = unsafe { crate::fs::Root::from_raw(root, pool) };

    match callback(&fs_root, path_str) {
        Ok(should_include) => {
            unsafe {
                *include = if should_include { 1 } else { 0 };
            }
            std::ptr::null_mut()
        }
        Err(mut e) => unsafe { e.detach() },
    }
}

extern "C" fn wrap_notify_func(
    baton: *mut std::ffi::c_void,
    notify: *const subversion_sys::svn_repos_notify_t,
    _pool: *mut apr_sys::apr_pool_t,
) {
    let baton = unsafe { &mut *(baton as *mut Box<dyn FnMut(&Notify)>) };
    unsafe {
        baton(&Notify::from_ptr(notify));
    }
}

/// Upgrades a repository to the latest filesystem format.
pub fn upgrade(
    path: &std::path::Path,
    nonblocking: bool,
    notify_func: Option<&mut dyn FnMut(&Notify)>,
) -> Result<(), Error<'static>> {
    let path = std::ffi::CString::new(path.to_str().unwrap()).unwrap();
    let notify_func = notify_func.map(|_notify_func| wrap_notify_func as _);
    let notify_baton = Box::into_raw(Box::new(notify_func)).cast();
    let pool = apr::Pool::new();
    let ret = unsafe {
        subversion_sys::svn_repos_upgrade2(
            path.as_ptr(),
            nonblocking as i32,
            notify_func,
            notify_baton,
            pool.as_mut_ptr(),
        )
    };
    Error::from_raw(ret)?;
    Ok(())
}

/// Deletes a repository.
pub fn delete(path: &std::path::Path) -> Result<(), Error<'static>> {
    let path = std::ffi::CString::new(path.to_str().unwrap()).unwrap();
    let pool = apr::Pool::new();
    let ret = unsafe { subversion_sys::svn_repos_delete(path.as_ptr(), pool.as_mut_ptr()) };
    Error::from_raw(ret)?;
    Ok(())
}

/// Gets the version of the repository library.
pub fn version() -> crate::Version {
    unsafe { crate::Version(subversion_sys::svn_repos_version()) }
}

/// Creates a hot copy of a repository.
pub fn hotcopy(
    src_path: &std::path::Path,
    dst_path: &std::path::Path,
    clean_logs: bool,
    incremental: bool,
    notify_func: Option<&impl Fn(&Notify)>,
    cancel_func: Option<&impl Fn() -> Result<(), Error<'static>>>,
) -> Result<(), Error<'static>> {
    let src_path = std::ffi::CString::new(src_path.to_str().unwrap()).unwrap();
    let dst_path = std::ffi::CString::new(dst_path.to_str().unwrap()).unwrap();
    let pool = apr::Pool::new();
    let ret = unsafe {
        subversion_sys::svn_repos_hotcopy3(
            src_path.as_ptr(),
            dst_path.as_ptr(),
            clean_logs.into(),
            incremental.into(),
            if notify_func.is_some() {
                Some(wrap_notify_func)
            } else {
                None
            },
            notify_func
                .map(|notify_func| {
                    let boxed: Box<dyn FnMut(&Notify)> = Box::new(move |n| notify_func(n));
                    Box::into_raw(Box::new(boxed)) as *mut std::ffi::c_void
                })
                .unwrap_or(std::ptr::null_mut()),
            if cancel_func.is_some() {
                Some(crate::wrap_cancel_func)
            } else {
                None
            },
            cancel_func
                .map(|cancel_func| {
                    let boxed: Box<dyn Fn() -> Result<(), Error<'static>>> = Box::new(cancel_func);
                    Box::into_raw(Box::new(boxed)) as *mut std::ffi::c_void
                })
                .unwrap_or(std::ptr::null_mut()),
            pool.as_mut_ptr(),
        )
    };
    Error::from_raw(ret)?;
    Ok(())
}

#[cfg(test)]
mod tests {
    use super::*;
    #[test]
    fn test_create_delete() {
        let td = tempfile::tempdir().unwrap();
        super::Repos::create(td.path()).unwrap();
        super::Repos::open(td.path()).unwrap();
        super::delete(td.path()).unwrap();
    }

    #[test]
    fn test_capabilities() {
        let td = tempfile::tempdir().unwrap();
        let mut repos = super::Repos::create(td.path()).unwrap();
        assert!(repos.capabilities().unwrap().contains("mergeinfo"));
        assert!(!repos.capabilities().unwrap().contains("mergeinfo2"));
        assert!(repos.has_capability("mergeinfo").unwrap());
        assert!(repos.has_capability("unknown").is_err());
    }

    #[test]
    fn test_load_fs_segfault_reproduction() {
        // Create a test repository
        let temp_dir = tempfile::tempdir().unwrap();
        let repo_path = temp_dir.path().join("test_repo");
        let repo = Repos::create(&repo_path).unwrap();

        // Create a simple dump data to load
        let dump_data = b"SVN-fs-dump-format-version: 3\nUUID: test-uuid-1234\n\nRevision-number: 0\nProp-content-length: 56\nContent-length: 56\n\nK 8\nsvn:date\nV 27\n2023-01-01T12:00:00.000000Z\nPROPS-END\n\n";
        let mut stream = crate::io::Stream::from(dump_data.as_slice());

        // Use None for both start_rev and end_rev to load all revisions
        let result = repo.load_fs(
            &mut stream,
            None,
            None,
            LoadUUID::Default,
            std::path::Path::new(""),
            false,
            false,
            None::<&fn() -> bool>,
            true,
            false,
            false,
            None::<&fn(&Notify)>,
        );

        println!("load_fs result: {:?}", result);
    }

    #[test]
    fn test_version() {
        assert!(super::version().major() >= 1);
    }

    #[test]
    fn test_create_with_config() {
        let td = tempfile::tempdir().unwrap();
        let config = std::collections::HashMap::from([(
            "repository.compression".to_string(),
            "6".to_string(),
        )]);
        let fs_config =
            std::collections::HashMap::from([("fsfs.compression".to_string(), "zlib".to_string())]);

        let repos = super::Repos::create_with_config(td.path(), Some(&config), Some(&fs_config));
        assert!(repos.is_ok(), "Failed to create repository with config");

        // Test that we can open the created repository
        let repos = super::Repos::open(td.path());
        assert!(
            repos.is_ok(),
            "Failed to open repository created with config"
        );
    }

    #[test]
    fn test_create_with_empty_config() {
        let td = tempfile::tempdir().unwrap();
        let empty_config = std::collections::HashMap::new();

        let repos = super::Repos::create_with_config(td.path(), Some(&empty_config), None);
        assert!(
            repos.is_ok(),
            "Failed to create repository with empty config"
        );
    }

    #[test]
    fn test_dump_basic() {
        let td = tempfile::tempdir().unwrap();
        let repos = super::Repos::create(td.path()).unwrap();

        // Create a string buffer to capture dump output
        let mut buffer = Vec::new();
        let mut stream = crate::io::wrap_write(&mut buffer).unwrap();

        // Dump revision 0 only (empty repo)
        let mut options = DumpOptions {
            start_rev: Some(crate::Revnum(0)),
            end_rev: Some(crate::Revnum(0)),
            include_revprops: true,
            include_changes: true,
            ..Default::default()
        };
        let result = repos.dump(&mut stream, &mut options);
        result.unwrap();

        // Verify that we got some dump output
        let dump_str = String::from_utf8_lossy(&buffer);
        assert!(
            dump_str.contains("SVN-fs-dump-format-version"),
            "Dump output should contain format version header"
        );
    }

    #[test]
    fn test_dump_all_revisions() {
        let td = tempfile::tempdir().unwrap();
        let repos = super::Repos::create(td.path()).unwrap();

        // Create a string buffer to capture dump output
        let mut buffer = Vec::new();
        let mut stream = crate::io::wrap_write(&mut buffer).unwrap();

        // Dump all revisions (None means use SVN_INVALID_REVNUM = -1)
        let mut options = DumpOptions {
            include_revprops: true,
            include_changes: true,
            ..Default::default()
        };
        let result = repos.dump(&mut stream, &mut options);
        result.unwrap();
    }

    #[test]
    fn test_dump_with_filter() {
        let td = tempfile::tempdir().unwrap();
        let repos = super::Repos::create(td.path()).unwrap();

        // Create a file in the repository using the filesystem API
        let fs = repos.fs().unwrap();
        let mut txn = fs.begin_txn(crate::Revnum(0), 0).unwrap();
        let mut txn_root = txn.root().unwrap();
        txn_root.make_file("/test.txt").unwrap();

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

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

        // Create a string buffer to capture dump output
        let mut buffer = Vec::new();
        let mut stream = crate::io::wrap_write(&mut buffer).unwrap();

        // Track what paths were filtered
        let filtered_paths = std::sync::Arc::new(std::sync::Mutex::new(Vec::new()));
        let filtered_paths_clone = filtered_paths.clone();

        // Dump with a filter that accepts all paths but records them
        let mut options = DumpOptions {
            start_rev: Some(crate::Revnum(1)),
            end_rev: Some(crate::Revnum(1)),
            include_revprops: true,
            include_changes: true,
            filter_func: Some(Box::new(move |_root, path| {
                filtered_paths_clone.lock().unwrap().push(path.to_string());
                Ok(true) // Accept all paths
            })),
            ..Default::default()
        };
        let result = repos.dump(&mut stream, &mut options);
        result.unwrap();

        // Verify that the filter was called for the file we created
        let paths = filtered_paths.lock().unwrap();
        assert!(!paths.is_empty(), "Filter function should have been called");
        // The path might be "/test.txt" with leading slash
        assert!(
            paths.iter().any(|p| p == "test.txt" || p == "/test.txt"),
            "Filter should have been called for test.txt, got paths: {:?}",
            paths
        );
    }

    #[test]
    fn test_verify_basic() {
        let td = tempfile::tempdir().unwrap();
        let repos = super::Repos::create(td.path()).unwrap();

        let options = VerifyOptions::new();
        let result = repos.verify(&options);
        result.unwrap();
    }

    #[test]
    fn test_recover_basic() {
        let td = tempfile::tempdir().unwrap();
        let mut repos = super::Repos::create(td.path()).unwrap();

        repos
            .recover(
                true, // nonblocking
                None, // notify_func
                None, // cancel_func
            )
            .unwrap();
    }

    #[test]
    fn test_load_basic() {
        let td = tempfile::tempdir().unwrap();
        let repos = super::Repos::create(td.path()).unwrap();

        // Create a minimal dump stream content
        let dump_content = b"SVN-fs-dump-format-version: 2\n\n\
                           UUID: 00000000-0000-0000-0000-000000000000\n\n\
                           Revision-number: 0\n\
                           Prop-content-length: 56\n\
                           Content-length: 56\n\n\
                           K 8\n\
                           svn:date\n\
                           V 27\n\
                           2024-01-01T00:00:00.000000Z\n\
                           PROPS-END\n\n";

        let mut dump_vec = dump_content.to_vec();
        let mut stream = crate::io::wrap_write(&mut dump_vec).unwrap();

        // Test loading from dump stream (may fail with invalid dump format)
        // Note: SVN requires both start_rev and end_rev to be either valid or both invalid
        let options = LoadOptions::new();
        let result = repos.load(&mut stream, &options);

        // The test may fail due to dump format issues, but should not crash
        // The important thing is that the API is correctly implemented
        match result {
            Ok(_) => println!("Load succeeded"),
            Err(e) => println!("Load failed as expected: {}", e),
        }
    }

    #[test]
    fn test_load_fs_revprops() {
        let td = tempfile::tempdir().unwrap();
        let repos = super::Repos::create(td.path()).unwrap();

        // First dump only the revprops of revision 0
        let mut dump_buf = Vec::new();
        {
            let mut stream = crate::io::wrap_write(&mut dump_buf).unwrap();
            let mut options = super::DumpOptions {
                start_rev: Some(crate::Revnum(0)),
                end_rev: Some(crate::Revnum(0)),
                include_revprops: true,
                include_changes: false,
                ..Default::default()
            };
            repos.dump(&mut stream, &mut options).unwrap();
        }

        // Load the revprops back from the dump
        let mut read_stream =
            crate::io::Stream::from_reader(std::io::Cursor::new(dump_buf)).unwrap();
        let result = repos.load_fs_revprops(
            &mut read_stream,
            None,
            None,
            false,
            false,
            false,
            None::<&fn(&Notify)>,
            None::<&fn() -> bool>,
        );

        // The load should succeed (or gracefully fail if dump format is incompatible)
        match result {
            Ok(()) => {}
            Err(e) => {
                // Some dump format incompatibilities are acceptable
                let msg = e.to_string();
                assert!(
                    msg.contains("format") || msg.contains("invalid") || msg.contains("parse"),
                    "unexpected error: {}",
                    msg
                );
            }
        }
    }
}

/// Pack the filesystem of a repository to improve performance
/// This is useful for FSFS repositories to consolidate revision files
pub fn fs_pack(
    path: &std::path::Path,
    notify_func: Option<&dyn Fn(&Notify)>,
    cancel_func: Option<&dyn Fn() -> Result<(), Error<'static>>>,
) -> Result<(), Error<'static>> {
    let pool = apr::Pool::new();

    let notify_baton = notify_func
        .map(|notify_func| {
            let boxed: Box<dyn FnMut(&Notify)> = Box::new(move |n| notify_func(n));
            Box::into_raw(Box::new(boxed)) as *mut std::ffi::c_void
        })
        .unwrap_or(std::ptr::null_mut());

    let cancel_baton = cancel_func
        .map(|cancel_func| {
            let boxed: Box<dyn Fn() -> Result<(), Error<'static>>> = Box::new(cancel_func);
            Box::into_raw(Box::new(boxed)) as *mut std::ffi::c_void
        })
        .unwrap_or(std::ptr::null_mut());

    // First open the repository to get the repos handle
    let repos = Repos::open(path)?;

    let ret = unsafe {
        subversion_sys::svn_repos_fs_pack2(
            repos.ptr,
            if notify_func.is_some() {
                Some(wrap_notify_func)
            } else {
                None
            },
            notify_baton,
            if cancel_func.is_some() {
                Some(crate::wrap_cancel_func)
            } else {
                None
            },
            cancel_baton,
            pool.as_mut_ptr(),
        )
    };

    // Free callback batons
    if !notify_baton.is_null() {
        unsafe { drop(Box::from_raw(notify_baton as *mut Box<dyn Fn(&Notify)>)) };
    }
    if !cancel_baton.is_null() {
        unsafe {
            drop(Box::from_raw(
                cancel_baton as *mut Box<dyn Fn() -> Result<(), Error<'static>>>,
            ))
        };
    }

    Error::from_raw(ret)
}

impl Repos {
    /// Get a commit editor for making commits directly to the repository
    /// This provides low-level access for creating commits without a working copy
    ///
    /// # Arguments
    /// * `repos_url` - The repository URL (required)
    /// * `base_path` - The base path within the repository (e.g., "/trunk")
    /// * `log_msg` - The commit log message
    /// * `author` - Optional author name
    /// * `revprops` - Optional additional revision properties (svn:log and svn:author are set automatically)
    /// * `commit_callback` - Optional callback to be called on successful commit
    /// * `authz_callback` - Optional authorization callback to check write access for each path
    ///
    /// The returned editor borrows from the repository and must not outlive it.
    #[cfg(feature = "delta")]
    pub fn get_commit_editor<'s>(
        &'s self,
        repos_url: &str,
        base_path: &str,
        log_msg: &str,
        author: Option<&str>,
        revprops: Option<&std::collections::HashMap<String, Vec<u8>>>,
        commit_callback: Option<&dyn Fn(crate::Revnum, &str, &std::ffi::CStr)>,
        authz_callback: Option<
            Box<dyn FnMut(AuthzAccess, &crate::fs::Root, &str) -> Result<bool, Error<'static>>>,
        >,
        txn: Option<&mut crate::fs::Transaction<'_>>,
    ) -> Result<crate::delta::WrapEditor<'s>, Error<'s>> {
        let repos_url_cstr = std::ffi::CString::new(repos_url)?;
        let base_path_cstr = std::ffi::CString::new(base_path)?;
        let log_msg_cstr = std::ffi::CString::new(log_msg)?;
        let author_cstr = author.map(std::ffi::CString::new).transpose()?;
        // Use a boxed pool to keep it alive with the editor
        let pool = Box::new(apr::Pool::new());
        let pool_ptr = pool.as_ref().as_mut_ptr();

        // Build revprop table with log message and author
        let revprops_hash = unsafe {
            let hash = apr_sys::apr_hash_make(pool_ptr);

            // Add log message
            // Allocate the key in the pool so it lives as long as the hash
            let log_key = apr_sys::apr_pstrdup(pool_ptr, c"svn:log".as_ptr());
            let log_value = subversion_sys::svn_string_ncreate(
                log_msg_cstr.as_ptr() as *const std::os::raw::c_char,
                log_msg.len(),
                pool_ptr,
            );
            apr_sys::apr_hash_set(
                hash,
                log_key as *const std::ffi::c_void,
                apr_sys::APR_HASH_KEY_STRING as isize,
                log_value as *mut std::ffi::c_void,
            );

            // Add author if provided
            if let Some(ref author_c) = author_cstr {
                // Allocate the key in the pool so it lives as long as the hash
                let author_key = apr_sys::apr_pstrdup(pool_ptr, c"svn:author".as_ptr());
                let author_value = subversion_sys::svn_string_create(author_c.as_ptr(), pool_ptr);
                apr_sys::apr_hash_set(
                    hash,
                    author_key as *const std::ffi::c_void,
                    apr_sys::APR_HASH_KEY_STRING as isize,
                    author_value as *mut std::ffi::c_void,
                );
            }

            // Add any additional revprops
            if let Some(revprops) = revprops {
                for (key, value) in revprops {
                    // Allocate key in the pool
                    let key_cstr = std::ffi::CString::new(key.as_str())?;
                    let pool_key = apr_sys::apr_pstrdup(pool_ptr, key_cstr.as_ptr());
                    let svn_string = subversion_sys::svn_string_ncreate(
                        value.as_ptr() as *const std::os::raw::c_char,
                        value.len(),
                        pool_ptr,
                    );
                    apr_sys::apr_hash_set(
                        hash,
                        pool_key as *const std::ffi::c_void,
                        apr_sys::APR_HASH_KEY_STRING as isize,
                        svn_string as *mut std::ffi::c_void,
                    );
                }
            }

            hash
        };

        let commit_baton = commit_callback
            .map(|callback| Box::into_raw(Box::new(callback)) as *mut std::ffi::c_void)
            .unwrap_or(std::ptr::null_mut());

        let has_authz = authz_callback.is_some();
        let authz_baton = authz_callback
            .map(|callback| Box::into_raw(Box::new(callback)) as *mut std::ffi::c_void)
            .unwrap_or(std::ptr::null_mut());

        let mut editor_ptr = std::ptr::null();
        let mut edit_baton = std::ptr::null_mut();

        // Authz callback wrapper
        extern "C" fn wrap_authz_callback(
            required: subversion_sys::svn_repos_authz_access_t,
            allowed: *mut i32,
            root: *mut subversion_sys::svn_fs_root_t,
            path: *const i8,
            baton: *mut std::ffi::c_void,
            pool: *mut apr_sys::apr_pool_t,
        ) -> *mut subversion_sys::svn_error_t {
            if baton.is_null() || allowed.is_null() {
                return std::ptr::null_mut();
            }

            let callback = unsafe {
                &mut *(baton
                    as *mut Box<
                        dyn FnMut(
                            AuthzAccess,
                            &crate::fs::Root,
                            &str,
                        ) -> Result<bool, Error<'static>>,
                    >)
            };

            let path_str = unsafe {
                if path.is_null() {
                    ""
                } else {
                    std::ffi::CStr::from_ptr(path).to_str().unwrap_or("")
                }
            };

            let fs_root = unsafe { crate::fs::Root::from_raw(root, pool) };

            let access = AuthzAccess::from_raw(required);

            match callback(access, &fs_root, path_str) {
                Ok(is_allowed) => {
                    unsafe {
                        *allowed = if is_allowed { 1 } else { 0 };
                    }
                    std::ptr::null_mut()
                }
                Err(mut e) => unsafe { e.detach() },
            }
        }

        // Commit callback wrapper that matches expected signature
        extern "C" fn wrap_commit_callback(
            commit_info: *const subversion_sys::svn_commit_info_t,
            baton: *mut std::ffi::c_void,
            _pool: *mut apr_sys::apr_pool_t,
        ) -> *mut subversion_sys::svn_error_t {
            if baton.is_null() || commit_info.is_null() {
                return std::ptr::null_mut();
            }

            let callback =
                unsafe { &*(baton as *const Box<dyn Fn(crate::Revnum, &str, &std::ffi::CStr)>) };

            let info = unsafe { &*commit_info };
            let date_str = if !info.date.is_null() {
                unsafe { std::ffi::CStr::from_ptr(info.date).to_string_lossy() }
            } else {
                std::borrow::Cow::Borrowed("")
            };

            let author_cstr = if !info.author.is_null() {
                unsafe { std::ffi::CStr::from_ptr(info.author) }
            } else {
                c""
            };

            callback(
                crate::Revnum::from(info.revision as u64),
                &date_str,
                author_cstr,
            );
            std::ptr::null_mut()
        }

        let ret = unsafe {
            subversion_sys::svn_repos_get_commit_editor5(
                &mut editor_ptr,
                &mut edit_baton,
                self.ptr,
                txn.map(|t| t.as_ptr()).unwrap_or(std::ptr::null_mut()),
                repos_url_cstr.as_ptr(),
                base_path_cstr.as_ptr(),
                revprops_hash,
                if commit_callback.is_some() {
                    Some(wrap_commit_callback)
                } else {
                    None
                },
                commit_baton,
                if has_authz {
                    Some(wrap_authz_callback)
                } else {
                    None
                },
                authz_baton,
                pool_ptr,
            )
        };

        // Clean up batons if there was an error
        if !ret.is_null() {
            if !commit_baton.is_null() {
                unsafe {
                    let _ = Box::from_raw(
                        commit_baton as *mut Box<dyn Fn(crate::Revnum, &str, &std::ffi::CStr)>,
                    );
                }
            }
            if !authz_baton.is_null() {
                unsafe {
                    let _ = Box::from_raw(
                        authz_baton
                            as *mut Box<
                                dyn FnMut(
                                    AuthzAccess,
                                    &crate::fs::Root,
                                    &str,
                                )
                                    -> Result<bool, Error<'static>>,
                            >,
                    );
                }
            }
        }

        Error::from_raw(ret)?;

        // Store authz callback baton with dropper so it's properly cleaned up
        let mut batons = Vec::new();
        if !authz_baton.is_null() {
            batons.push((
                authz_baton,
                drop_authz_commit_baton as crate::delta::DropperFn,
            ));
        }

        let editor = crate::delta::WrapEditor {
            editor: editor_ptr,
            baton: edit_baton,
            _pool: apr::PoolHandle::owned(*pool),
            callback_batons: batons,
        };
        Ok(editor)
    }

    /// Begin a repository report (for update/switch operations)
    ///
    /// # Arguments
    /// * `revnum` - The target revision
    /// * `fs_base` - Absolute path in the filesystem at which comparison should be rooted
    /// * `target` - Single path component to limit scope, or "" for all of fs_base
    /// * `tgt_path` - Optional target path when switching (None to preserve current path)
    /// * `text_deltas` - Whether to generate text deltas
    /// * `depth` - Requested depth of the editor drive
    /// * `ignore_ancestry` - Whether to ignore node ancestry
    /// * `send_copyfrom_args` - Whether to send copyfrom arguments
    /// * `editor` - The delta editor
    /// * `editor_baton` - The editor baton
    /// * `authz_read_func` - Optional authorization callback to check read access for each path
    ///
    /// # Safety
    ///
    /// The caller must ensure that `editor` and `editor_baton` are valid and compatible.
    /// The editor pointer must remain valid for the lifetime of the returned Report.
    pub unsafe fn begin_report(
        &self,
        revnum: Revnum,
        fs_base: &str,
        target: &str,           // Not optional - use "" for all of fs_base
        tgt_path: Option<&str>, // Can be NULL for same path
        text_deltas: bool,
        depth: crate::Depth,
        ignore_ancestry: bool,
        send_copyfrom_args: bool,
        editor: *const subversion_sys::svn_delta_editor_t,
        editor_baton: *mut std::ffi::c_void,
        authz_read_func: Option<
            Box<dyn FnMut(&crate::fs::Root, &str) -> Result<bool, Error<'static>>>,
        >,
    ) -> Result<Report, Error<'static>> {
        let fs_base_cstr = std::ffi::CString::new(fs_base).unwrap();
        let target_cstr = std::ffi::CString::new(target).unwrap();
        let tgt_path_cstr = tgt_path.map(|t| std::ffi::CString::new(t).unwrap());

        let pool = apr::Pool::new();
        let mut report_baton: *mut std::ffi::c_void = std::ptr::null_mut();

        let has_authz = authz_read_func.is_some();
        let authz_baton = authz_read_func
            .map(|callback| Box::into_raw(Box::new(callback)) as *mut std::ffi::c_void)
            .unwrap_or(std::ptr::null_mut());

        // Authz read callback wrapper
        extern "C" fn wrap_authz_read_func(
            allowed: *mut i32,
            root: *mut subversion_sys::svn_fs_root_t,
            path: *const i8,
            baton: *mut std::ffi::c_void,
            pool: *mut apr_sys::apr_pool_t,
        ) -> *mut subversion_sys::svn_error_t {
            if baton.is_null() || allowed.is_null() {
                return std::ptr::null_mut();
            }

            let callback = unsafe {
                &mut *(baton
                    as *mut Box<dyn FnMut(&crate::fs::Root, &str) -> Result<bool, Error<'static>>>)
            };

            let path_str = unsafe {
                if path.is_null() {
                    ""
                } else {
                    std::ffi::CStr::from_ptr(path).to_str().unwrap_or("")
                }
            };

            let fs_root = unsafe { crate::fs::Root::from_raw(root, pool) };

            match callback(&fs_root, path_str) {
                Ok(is_allowed) => {
                    unsafe {
                        *allowed = if is_allowed { 1 } else { 0 };
                    }
                    std::ptr::null_mut()
                }
                Err(mut e) => unsafe { e.detach() },
            }
        }

        let ret = unsafe {
            subversion_sys::svn_repos_begin_report3(
                &mut report_baton,
                revnum.into(),
                self.ptr,
                fs_base_cstr.as_ptr(),
                target_cstr.as_ptr(),
                tgt_path_cstr
                    .as_ref()
                    .map(|t| t.as_ptr())
                    .unwrap_or(std::ptr::null()),
                text_deltas.into(),
                depth.into(),
                ignore_ancestry.into(),
                send_copyfrom_args.into(),
                editor,
                editor_baton,
                if has_authz {
                    Some(wrap_authz_read_func)
                } else {
                    None
                },
                authz_baton,
                0, // zero_copy_limit
                pool.as_mut_ptr(),
            )
        };

        svn_result(ret)?;

        // Store authz callback baton with dropper so it's properly cleaned up
        let mut batons = Vec::new();
        if !authz_baton.is_null() {
            batons.push((
                authz_baton,
                drop_authz_read_baton as crate::delta::DropperFn,
            ));
        }

        Ok(Report {
            baton: report_baton,
            pool,
            callback_batons: batons,
        })
    }
}

/// Repository report handle for update/switch operations
pub struct Report {
    baton: *mut std::ffi::c_void,
    pool: apr::Pool<'static>,
    // Callback batons with their dropper functions
    callback_batons: Vec<(*mut std::ffi::c_void, crate::delta::DropperFn)>,
}

impl Drop for Report {
    fn drop(&mut self) {
        // Clean up callback batons using their type-erased droppers
        // IMPORTANT: These must be freed AFTER any operations that might use them,
        // but BEFORE the pool is destroyed (since the pool field is dropped after this)
        for (baton, dropper) in &self.callback_batons {
            if !baton.is_null() {
                unsafe {
                    dropper(*baton);
                }
            }
        }
        self.callback_batons.clear();
        // Pool is automatically dropped after this, which cleans up the report_baton
    }
}

impl Report {
    /// Record the presence of a path in the current tree
    pub fn set_path(
        &self,
        path: &str,
        revision: Revnum,
        depth: crate::Depth,
        start_empty: bool,
        lock_token: Option<&str>,
    ) -> Result<(), Error<'static>> {
        let path_cstr = std::ffi::CString::new(path).unwrap();
        let lock_token_cstr = lock_token.map(|t| std::ffi::CString::new(t).unwrap());

        with_tmp_pool(|pool| {
            let ret = unsafe {
                subversion_sys::svn_repos_set_path3(
                    self.baton,
                    path_cstr.as_ptr(),
                    revision.into(),
                    depth.into(),
                    start_empty.into(),
                    lock_token_cstr
                        .as_ref()
                        .map(|t| t.as_ptr())
                        .unwrap_or(std::ptr::null()),
                    pool.as_mut_ptr(),
                )
            };
            svn_result(ret)
        })
    }

    /// Record the presence of a path as a link to another path
    pub fn link_path(
        &self,
        path: &str,
        link_path: &str,
        revision: Revnum,
        depth: crate::Depth,
        start_empty: bool,
        lock_token: Option<&str>,
    ) -> Result<(), Error<'static>> {
        let path_cstr = std::ffi::CString::new(path).unwrap();
        let link_path_cstr = std::ffi::CString::new(link_path).unwrap();
        let lock_token_cstr = lock_token.map(|t| std::ffi::CString::new(t).unwrap());

        with_tmp_pool(|pool| {
            let ret = unsafe {
                subversion_sys::svn_repos_link_path3(
                    self.baton,
                    path_cstr.as_ptr(),
                    link_path_cstr.as_ptr(),
                    revision.into(),
                    depth.into(),
                    start_empty.into(),
                    lock_token_cstr
                        .as_ref()
                        .map(|t| t.as_ptr())
                        .unwrap_or(std::ptr::null()),
                    pool.as_mut_ptr(),
                )
            };
            svn_result(ret)
        })
    }

    /// Record the non-existence of a path in the current tree
    pub fn delete_path(&self, path: &str) -> Result<(), Error<'static>> {
        let path_cstr = std::ffi::CString::new(path).unwrap();

        with_tmp_pool(|pool| {
            let ret = unsafe {
                subversion_sys::svn_repos_delete_path(
                    self.baton,
                    path_cstr.as_ptr(),
                    pool.as_mut_ptr(),
                )
            };
            svn_result(ret)
        })
    }

    /// Finish the report and drive the editor
    pub fn finish(self) -> Result<(), Error<'static>> {
        // Use the report's pool, not a temporary one
        let pool_ptr = self.pool.as_mut_ptr();
        let ret = unsafe { subversion_sys::svn_repos_finish_report(self.baton, pool_ptr) };
        svn_result(ret)
    }

    /// Abort the report
    pub fn abort(self) -> Result<(), Error<'static>> {
        // Extract the pool pointer before self is consumed
        let pool_ptr = self.pool.as_mut_ptr();
        let ret = unsafe { subversion_sys::svn_repos_abort_report(self.baton, pool_ptr) };
        svn_result(ret)
    }
}

impl Repos {
    /// Set the environment for hook scripts by providing a path to an environment file
    pub fn hooks_setenv(&mut self, hooks_env_path: &str) -> Result<(), Error<'static>> {
        let pool = apr::Pool::new();
        let path_cstr = CString::new(hooks_env_path).unwrap();

        let ret = unsafe {
            subversion_sys::svn_repos_hooks_setenv(self.ptr, path_cstr.as_ptr(), pool.as_mut_ptr())
        };

        svn_result(ret)
    }

    /// Get the path to the start-commit hook script
    pub fn start_commit_hook_path(&self) -> std::path::PathBuf {
        let pool = apr::Pool::new();
        let path_ptr =
            unsafe { subversion_sys::svn_repos_start_commit_hook(self.ptr, pool.as_mut_ptr()) };

        let path = unsafe { std::ffi::CStr::from_ptr(path_ptr) }
            .to_string_lossy()
            .into_owned();
        std::path::PathBuf::from(path)
    }

    /// Get the path to the pre-commit hook script
    pub fn pre_commit_hook_path(&self) -> std::path::PathBuf {
        let pool = apr::Pool::new();
        let path_ptr =
            unsafe { subversion_sys::svn_repos_pre_commit_hook(self.ptr, pool.as_mut_ptr()) };

        let path = unsafe { std::ffi::CStr::from_ptr(path_ptr) }
            .to_string_lossy()
            .into_owned();
        std::path::PathBuf::from(path)
    }

    /// Get the path to the post-commit hook script  
    pub fn post_commit_hook_path(&self) -> std::path::PathBuf {
        let pool = apr::Pool::new();
        let path_ptr =
            unsafe { subversion_sys::svn_repos_post_commit_hook(self.ptr, pool.as_mut_ptr()) };

        let path = unsafe { std::ffi::CStr::from_ptr(path_ptr) }
            .to_string_lossy()
            .into_owned();
        std::path::PathBuf::from(path)
    }

    /// Get the path to the pre-revprop-change hook script
    pub fn pre_revprop_change_hook_path(&self) -> std::path::PathBuf {
        let pool = apr::Pool::new();
        let path_ptr = unsafe {
            subversion_sys::svn_repos_pre_revprop_change_hook(self.ptr, pool.as_mut_ptr())
        };

        let path = unsafe { std::ffi::CStr::from_ptr(path_ptr) }
            .to_string_lossy()
            .into_owned();
        std::path::PathBuf::from(path)
    }

    /// Get the path to the post-revprop-change hook script
    pub fn post_revprop_change_hook_path(&self) -> std::path::PathBuf {
        let pool = apr::Pool::new();
        let path_ptr = unsafe {
            subversion_sys::svn_repos_post_revprop_change_hook(self.ptr, pool.as_mut_ptr())
        };

        let path = unsafe { std::ffi::CStr::from_ptr(path_ptr) }
            .to_string_lossy()
            .into_owned();
        std::path::PathBuf::from(path)
    }

    /// Get the path to the pre-lock hook script
    pub fn pre_lock_hook_path(&self) -> std::path::PathBuf {
        let pool = apr::Pool::new();
        let path_ptr =
            unsafe { subversion_sys::svn_repos_pre_lock_hook(self.ptr, pool.as_mut_ptr()) };

        let path = unsafe { std::ffi::CStr::from_ptr(path_ptr) }
            .to_string_lossy()
            .into_owned();
        std::path::PathBuf::from(path)
    }

    /// Get the path to the post-lock hook script
    pub fn post_lock_hook_path(&self) -> std::path::PathBuf {
        let pool = apr::Pool::new();
        let path_ptr =
            unsafe { subversion_sys::svn_repos_post_lock_hook(self.ptr, pool.as_mut_ptr()) };

        let path = unsafe { std::ffi::CStr::from_ptr(path_ptr) }
            .to_string_lossy()
            .into_owned();
        std::path::PathBuf::from(path)
    }

    /// Get the path to the pre-unlock hook script
    pub fn pre_unlock_hook_path(&self) -> std::path::PathBuf {
        let pool = apr::Pool::new();
        let path_ptr =
            unsafe { subversion_sys::svn_repos_pre_unlock_hook(self.ptr, pool.as_mut_ptr()) };

        let path = unsafe { std::ffi::CStr::from_ptr(path_ptr) }
            .to_string_lossy()
            .into_owned();
        std::path::PathBuf::from(path)
    }

    /// Get the path to the post-unlock hook script
    pub fn post_unlock_hook_path(&self) -> std::path::PathBuf {
        let pool = apr::Pool::new();
        let path_ptr =
            unsafe { subversion_sys::svn_repos_post_unlock_hook(self.ptr, pool.as_mut_ptr()) };

        let path = unsafe { std::ffi::CStr::from_ptr(path_ptr) }
            .to_string_lossy()
            .into_owned();
        std::path::PathBuf::from(path)
    }

    /// Begin a transaction for update
    pub fn begin_txn_for_update(
        &self,
        revnum: Revnum,
        author: &str,
    ) -> Result<crate::fs::Transaction<'_>, Error<'static>> {
        let pool = apr::Pool::new();
        let mut txn_ptr = std::ptr::null_mut();
        let author_cstr = std::ffi::CString::new(author)?;

        let ret = unsafe {
            subversion_sys::svn_repos_fs_begin_txn_for_update(
                &mut txn_ptr,
                self.ptr,
                revnum.0,
                author_cstr.as_ptr(),
                pool.as_mut_ptr(),
            )
        };
        Error::from_raw(ret)?;

        Ok(unsafe { crate::fs::Transaction::from_ptr_and_pool(txn_ptr, pool) })
    }

    /// Check the access level for a revision based on authorization rules
    ///
    /// This determines what revision metadata and changed-paths information
    /// should be visible to a user based on their access to paths in the revision.
    ///
    /// - Full access: User can read all paths changed in the revision
    /// - Partial access: User can read some but not all paths
    /// - No access: User cannot read any paths changed in the revision
    ///
    /// If `authz` is None, returns Full access (no authorization checks).
    pub fn check_revision_access(
        &self,
        revision: Revnum,
        authz: Option<&Authz>,
        _user: Option<&str>,
    ) -> Result<RevisionAccessLevel, Error<'static>> {
        let pool = apr::Pool::new();

        // If no authz provided, grant full access
        if authz.is_none() {
            return Ok(RevisionAccessLevel::Full);
        }

        let authz = authz.unwrap();

        // Create authz callback function
        extern "C" fn authz_read_func(
            allowed: *mut subversion_sys::svn_boolean_t,
            _root: *mut subversion_sys::svn_fs_root_t,
            path: *const std::os::raw::c_char,
            baton: *mut std::ffi::c_void,
            _pool: *mut apr_sys::apr_pool_t,
        ) -> *mut subversion_sys::svn_error_t {
            let authz = unsafe { &*(baton as *const Authz) };
            let path_str = unsafe { std::ffi::CStr::from_ptr(path) }.to_str().unwrap();

            match authz.check_access(None, path_str, None, AuthzAccess::Read) {
                Ok(access) => {
                    unsafe { *allowed = if access { 1 } else { 0 } };
                    std::ptr::null_mut()
                }
                Err(e) => unsafe { e.into_raw() },
            }
        }

        let mut access_level: subversion_sys::svn_repos_revision_access_level_t = 0;

        let ret = unsafe {
            subversion_sys::svn_repos_check_revision_access(
                &mut access_level,
                self.ptr,
                revision.0,
                Some(authz_read_func),
                authz as *const Authz as *mut std::ffi::c_void,
                pool.as_mut_ptr(),
            )
        };

        svn_result(ret)?;
        Ok(RevisionAccessLevel::from(access_level))
    }

    /// Return the revision number in this repository closest to the given
    /// date `tm` (expressed as APR microseconds since the epoch).
    ///
    /// Wraps `svn_repos_dated_revision`.
    pub fn dated_revision(&self, tm: apr::apr_time_t) -> Result<Revnum, Error<'static>> {
        with_tmp_pool(|pool| {
            let mut revision: subversion_sys::svn_revnum_t = -1; // SVN_INVALID_REVNUM
            let err = unsafe {
                subversion_sys::svn_repos_dated_revision(
                    &mut revision,
                    self.ptr,
                    tm,
                    pool.as_mut_ptr(),
                )
            };
            svn_result(err)?;
            Ok(Revnum(revision))
        })
    }

    /// Given a `path` that exists at revision `start` in this repository's
    /// filesystem, return the first revision at which `path` was deleted,
    /// within the inclusive range `start..=end`.  Returns `None` if `path`
    /// was not deleted in that range.
    ///
    /// Wraps `svn_repos_deleted_rev`.
    pub fn deleted_rev(
        &self,
        path: &str,
        start: Revnum,
        end: Revnum,
    ) -> Result<Option<Revnum>, Error<'static>> {
        let path_cstr = std::ffi::CString::new(path)?;
        let fs_ptr = unsafe { subversion_sys::svn_repos_fs(self.ptr) };
        with_tmp_pool(|pool| {
            let mut deleted: subversion_sys::svn_revnum_t = -1; // SVN_INVALID_REVNUM
            let err = unsafe {
                subversion_sys::svn_repos_deleted_rev(
                    fs_ptr,
                    path_cstr.as_ptr(),
                    start.0,
                    end.0,
                    &mut deleted,
                    pool.as_mut_ptr(),
                )
            };
            svn_result(err)?;
            if deleted < 0 {
                Ok(None)
            } else {
                Ok(Some(Revnum(deleted)))
            }
        })
    }

    /// Walk the history of `path` in this repository's filesystem, calling
    /// `history_func` for each interesting `(path, revision)` pair, from
    /// newest (`end`) to oldest (`start`).  If `cross_copies` is `true`,
    /// history will cross copy boundaries.
    ///
    /// The callback returns `Ok(())` to continue or `Err(...)` to abort
    /// iteration early; any error from the callback is propagated as the
    /// return value of this function.
    ///
    /// Wraps `svn_repos_history2`.
    pub fn history(
        &self,
        path: &str,
        start: Revnum,
        end: Revnum,
        cross_copies: bool,
        history_func: &mut dyn FnMut(&str, Revnum) -> Result<(), Error<'static>>,
    ) -> Result<(), Error<'static>> {
        let path_cstr = std::ffi::CString::new(path)?;
        let fs_ptr = unsafe { subversion_sys::svn_repos_fs(self.ptr) };

        struct Baton<'a> {
            func: &'a mut dyn FnMut(&str, Revnum) -> Result<(), Error<'static>>,
            error: Option<Error<'static>>,
        }

        unsafe extern "C" fn history_trampoline(
            baton: *mut std::ffi::c_void,
            path: *const std::os::raw::c_char,
            revision: subversion_sys::svn_revnum_t,
            _pool: *mut apr_sys::apr_pool_t,
        ) -> *mut subversion_sys::svn_error_t {
            let baton = &mut *(baton as *mut Baton<'_>);
            let path_str = std::ffi::CStr::from_ptr(path).to_str().unwrap_or("");
            match (baton.func)(path_str, Revnum(revision)) {
                Ok(()) => std::ptr::null_mut(),
                Err(e) => {
                    baton.error = Some(e);
                    subversion_sys::svn_error_create(
                        subversion_sys::svn_errno_t_SVN_ERR_CEASE_INVOCATION as i32,
                        std::ptr::null_mut(),
                        std::ptr::null(),
                    )
                }
            }
        }

        let mut baton = Baton {
            func: history_func,
            error: None,
        };

        with_tmp_pool(|pool| {
            let err = unsafe {
                subversion_sys::svn_repos_history2(
                    fs_ptr,
                    path_cstr.as_ptr(),
                    Some(history_trampoline),
                    &mut baton as *mut Baton<'_> as *mut std::ffi::c_void,
                    None,                 // authz_read_func
                    std::ptr::null_mut(), // authz_read_baton
                    start.0,
                    end.0,
                    cross_copies as i32,
                    pool.as_mut_ptr(),
                )
            };
            if let Some(e) = baton.error.take() {
                return Err(e);
            }
            svn_result(err)
        })
    }

    /// Retrieve log entries for `paths` between `start` and `end`, calling
    /// `log_receiver` for each entry.
    ///
    /// - `limit`: maximum entries to return (0 = unlimited)
    /// - `discover_changed_paths`: populate `LogEntry::changed_paths()`
    /// - `strict_node_history`: do not cross copy boundaries
    /// - `include_merged_revisions`: include log entries for merged revisions
    /// - `revprops`: revision property names to retrieve (e.g. `["svn:log", "svn:author"]`)
    ///
    /// Unlike the RA equivalent, this operates directly on the repository
    /// with no network round-trips.  No authz function is applied.
    ///
    /// The callback returns `Ok(())` to continue or `Err(...)` to abort
    /// early; any error is propagated as the return value.
    ///
    /// Wraps `svn_repos_get_logs4`.
    pub fn get_logs(
        &self,
        paths: &[&str],
        start: Revnum,
        end: Revnum,
        limit: usize,
        discover_changed_paths: bool,
        strict_node_history: bool,
        include_merged_revisions: bool,
        revprops: &[&str],
        log_receiver: &mut dyn FnMut(&crate::LogEntry) -> Result<(), Error<'static>>,
    ) -> Result<(), Error<'static>> {
        let pool = apr::Pool::new();

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

        let revprop_cstrings: Vec<std::ffi::CString> = revprops
            .iter()
            .map(|p| std::ffi::CString::new(*p).unwrap())
            .collect();
        let mut revprops_array = apr::tables::TypedArray::<*const std::os::raw::c_char>::new(
            &pool,
            revprops.len() as i32,
        );
        for cstr in &revprop_cstrings {
            revprops_array.push(cstr.as_ptr());
        }

        let baton = Box::into_raw(Box::new(log_receiver)) as *mut std::ffi::c_void;

        let err = unsafe {
            subversion_sys::svn_repos_get_logs4(
                self.ptr,
                paths_array.as_ptr(),
                start.0,
                end.0,
                limit as i32,
                discover_changed_paths as i32,
                strict_node_history as i32,
                include_merged_revisions as i32,
                revprops_array.as_ptr(),
                None,                 // authz_read_func
                std::ptr::null_mut(), // authz_read_baton
                Some(crate::wrap_log_entry_receiver),
                baton,
                pool.as_mut_ptr(),
            )
        };

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

        svn_result(err)
    }

    /// Trace the location of a node (file or directory) across multiple revisions.
    ///
    /// Given a node identified by `fs_path` at `peg_revision`, returns a map from
    /// each revision in `location_revisions` to the path at which that node lived in
    /// that revision.  If the node did not exist at a particular revision, that
    /// revision will be absent from the returned map.
    ///
    /// If `authz_read_func` is provided it is called to check read access; paths
    /// for which access is denied are omitted from the result without error.
    ///
    /// Wraps `svn_repos_trace_node_locations`.
    pub fn trace_node_locations(
        &self,
        fs: &mut crate::fs::Fs,
        fs_path: &str,
        peg_revision: Revnum,
        location_revisions: &[Revnum],
    ) -> Result<std::collections::HashMap<Revnum, String>, Error<'static>> {
        let pool = apr::Pool::new();
        let path_cstr = std::ffi::CString::new(fs_path).unwrap();

        // Build apr_array_header_t of svn_revnum_t for the requested revisions.
        let mut revs_array = apr::tables::TypedArray::<subversion_sys::svn_revnum_t>::new(
            &pool,
            location_revisions.len() as i32,
        );
        for rev in location_revisions {
            revs_array.push(rev.0);
        }
        let revs_arr_ptr: *const apr_sys::apr_array_header_t = unsafe { revs_array.as_ptr() };

        let mut locations: *mut apr_sys::apr_hash_t = std::ptr::null_mut();

        let err = unsafe {
            subversion_sys::svn_repos_trace_node_locations(
                fs.as_mut_ptr(),
                &mut locations,
                path_cstr.as_ptr(),
                peg_revision.0,
                revs_arr_ptr,
                None,                 // authz_read_func
                std::ptr::null_mut(), // authz_read_baton
                pool.as_mut_ptr(),
            )
        };
        svn_result(err)?;

        // Convert the hash: svn_revnum_t* → const char*
        let mut result = std::collections::HashMap::new();
        if !locations.is_null() {
            unsafe {
                let mut hi = apr_sys::apr_hash_first(pool.as_mut_ptr(), locations);
                while !hi.is_null() {
                    let mut key_ptr: *const std::ffi::c_void = std::ptr::null();
                    let mut val_ptr: *mut std::ffi::c_void = std::ptr::null_mut();
                    apr_sys::apr_hash_this(hi, &mut key_ptr, std::ptr::null_mut(), &mut val_ptr);
                    if !key_ptr.is_null() && !val_ptr.is_null() {
                        let rev = *(key_ptr as *const subversion_sys::svn_revnum_t);
                        let path = std::ffi::CStr::from_ptr(val_ptr as *const i8)
                            .to_string_lossy()
                            .into_owned();
                        result.insert(Revnum(rev), path);
                    }
                    hi = apr_sys::apr_hash_next(hi);
                }
            }
        }
        Ok(result)
    }

    /// Get the last committed revision, date, and author for a path.
    ///
    /// Returns a tuple of (revision, date, author) representing the last change
    /// to `path` in the filesystem `root`.
    ///
    /// Wraps `svn_repos_get_committed_info`.
    pub fn get_committed_info(
        root: &mut crate::fs::Root,
        path: &str,
    ) -> Result<(Revnum, Option<String>, Option<String>), Error<'static>> {
        let pool = apr::Pool::new();
        let path_cstr = std::ffi::CString::new(path)?;

        let mut committed_rev: subversion_sys::svn_revnum_t = -1;
        let mut committed_date: *const i8 = std::ptr::null();
        let mut last_author: *const i8 = std::ptr::null();

        let err = unsafe {
            subversion_sys::svn_repos_get_committed_info(
                &mut committed_rev,
                &mut committed_date,
                &mut last_author,
                root.as_mut_ptr(),
                path_cstr.as_ptr(),
                pool.as_mut_ptr(),
            )
        };
        svn_result(err)?;

        let date = if committed_date.is_null() {
            None
        } else {
            Some(
                unsafe { std::ffi::CStr::from_ptr(committed_date) }
                    .to_string_lossy()
                    .into_owned(),
            )
        };

        let author = if last_author.is_null() {
            None
        } else {
            Some(
                unsafe { std::ffi::CStr::from_ptr(last_author) }
                    .to_string_lossy()
                    .into_owned(),
            )
        };

        Ok((Revnum(committed_rev), date, author))
    }

    /// Get file revisions for a path over a range of revisions.
    ///
    /// Calls `handler` for each revision in which the file was changed, from oldest
    /// to newest. The handler receives the path, revision number, revision properties,
    /// whether the revision is from a merge, and property diffs relative to the
    /// previous revision of the file.
    ///
    /// Wraps `svn_repos_get_file_revs2`.
    pub fn get_file_revs(
        &self,
        path: &str,
        start: Revnum,
        end: Revnum,
        include_merged_revisions: bool,
        mut handler: impl FnMut(
            &str,
            Revnum,
            &std::collections::HashMap<String, Vec<u8>>,
            bool,
            &std::collections::HashMap<String, Vec<u8>>,
        ) -> Result<(), Error<'static>>,
    ) -> Result<(), Error<'static>> {
        let path_cstr = std::ffi::CString::new(path)?;

        struct Baton<'a> {
            func: &'a mut dyn FnMut(
                &str,
                Revnum,
                &std::collections::HashMap<String, Vec<u8>>,
                bool,
                &std::collections::HashMap<String, Vec<u8>>,
            ) -> Result<(), Error<'static>>,
        }

        unsafe extern "C" fn file_rev_trampoline(
            baton: *mut std::ffi::c_void,
            path: *const std::os::raw::c_char,
            rev: subversion_sys::svn_revnum_t,
            rev_props: *mut apr_sys::apr_hash_t,
            result_of_merge: subversion_sys::svn_boolean_t,
            txdelta_handler: *mut subversion_sys::svn_txdelta_window_handler_t,
            txdelta_baton: *mut *mut std::ffi::c_void,
            prop_diffs: *mut apr_sys::apr_array_header_t,
            _pool: *mut apr_sys::apr_pool_t,
        ) -> *mut subversion_sys::svn_error_t {
            let baton = &mut *(baton as *mut Baton<'_>);
            let path_str = std::ffi::CStr::from_ptr(path).to_str().unwrap_or("");

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

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

            // Set txdelta handler to NULL — we don't process file content
            if !txdelta_handler.is_null() {
                *txdelta_handler = None;
            }
            if !txdelta_baton.is_null() {
                *txdelta_baton = std::ptr::null_mut();
            }

            match (baton.func)(
                path_str,
                Revnum(rev),
                &rev_props_map,
                result_of_merge != 0,
                &prop_diffs_map,
            ) {
                Ok(()) => std::ptr::null_mut(),
                Err(e) => e.into_raw(),
            }
        }

        let mut baton = Baton { func: &mut handler };

        with_tmp_pool(|pool| {
            let err = unsafe {
                subversion_sys::svn_repos_get_file_revs2(
                    self.ptr,
                    path_cstr.as_ptr(),
                    start.0,
                    end.0,
                    include_merged_revisions as i32,
                    None,
                    std::ptr::null_mut(),
                    Some(file_rev_trampoline),
                    &mut baton as *mut Baton<'_> as *mut std::ffi::c_void,
                    pool.as_mut_ptr(),
                )
            };
            svn_result(err)
        })
    }

    /// Return information about the node at `path` in `root`.
    ///
    /// Returns `None` if the path does not exist.
    ///
    /// Wraps `svn_repos_stat`.
    pub fn stat(
        &self,
        root: &crate::fs::Root,
        path: &str,
    ) -> Result<Option<RepoDirEntry>, Error<'static>> {
        let path_cstr = std::ffi::CString::new(path)?;
        with_tmp_pool(|pool| {
            let mut dirent: *mut subversion_sys::svn_dirent_t = std::ptr::null_mut();
            let err = unsafe {
                subversion_sys::svn_repos_stat(
                    &mut dirent,
                    root.as_ptr() as *mut _,
                    path_cstr.as_ptr(),
                    pool.as_mut_ptr(),
                )
            };
            svn_result(err)?;
            if dirent.is_null() {
                return Ok(None);
            }
            Ok(Some(unsafe { RepoDirEntry::from_raw(dirent) }))
        })
    }

    /// Return the repository format and the minimum Subversion version that
    /// can read it.
    ///
    /// Returns `(format, (major, minor, patch))`.
    ///
    /// Wraps `svn_repos_info_format`.
    pub fn info_format(&self) -> Result<(i32, (i32, i32, i32)), Error<'static>> {
        with_tmp_pool(|pool| {
            let mut repos_format: std::os::raw::c_int = 0;
            let mut supports_version: *mut subversion_sys::svn_version_t = std::ptr::null_mut();
            let err = unsafe {
                subversion_sys::svn_repos_info_format(
                    &mut repos_format,
                    &mut supports_version,
                    self.ptr,
                    pool.as_mut_ptr(),
                    pool.as_mut_ptr(),
                )
            };
            svn_result(err)?;
            let version = if supports_version.is_null() {
                (0, 0, 0)
            } else {
                unsafe {
                    let v = &*supports_version;
                    (v.major, v.minor, v.patch)
                }
            };
            Ok((repos_format as i32, version))
        })
    }

    /// Walk the history of `path` at `peg_revision`, reporting location
    /// segments for each contiguous range of revisions in which the path
    /// existed at a consistent location.
    ///
    /// The callback receives `(path, range_start, range_end)` for each segment,
    /// where `path` is `None` for gaps (the node didn't exist in that range).
    ///
    /// Wraps `svn_repos_node_location_segments`.
    pub fn node_location_segments(
        &self,
        path: &str,
        peg_revision: Revnum,
        start_rev: Revnum,
        end_rev: Revnum,
        mut receiver: impl FnMut(Option<&str>, Revnum, Revnum) -> Result<(), Error<'static>>,
    ) -> Result<(), Error<'static>> {
        let path_cstr = std::ffi::CString::new(path)?;

        struct Baton<'a> {
            func: &'a mut dyn FnMut(Option<&str>, Revnum, Revnum) -> Result<(), Error<'static>>,
        }

        unsafe extern "C" fn segment_trampoline(
            segment: *mut subversion_sys::svn_location_segment_t,
            baton: *mut std::ffi::c_void,
            _pool: *mut apr_sys::apr_pool_t,
        ) -> *mut subversion_sys::svn_error_t {
            let baton = &mut *(baton as *mut Baton<'_>);
            if segment.is_null() {
                return std::ptr::null_mut();
            }
            let seg = &*segment;
            let path_opt = if seg.path.is_null() {
                None
            } else {
                Some(std::ffi::CStr::from_ptr(seg.path).to_str().unwrap_or(""))
            };
            match (baton.func)(path_opt, Revnum(seg.range_start), Revnum(seg.range_end)) {
                Ok(()) => std::ptr::null_mut(),
                Err(e) => e.into_raw(),
            }
        }

        let mut baton = Baton {
            func: &mut receiver,
        };

        with_tmp_pool(|pool| {
            let err = unsafe {
                subversion_sys::svn_repos_node_location_segments(
                    self.ptr,
                    path_cstr.as_ptr(),
                    peg_revision.0,
                    start_rev.0,
                    end_rev.0,
                    Some(segment_trampoline),
                    &mut baton as *mut Baton<'_> as *mut std::ffi::c_void,
                    None,
                    std::ptr::null_mut(),
                    pool.as_mut_ptr(),
                )
            };
            svn_result(err)
        })
    }

    /// Retrieve mergeinfo for multiple paths at a given revision.
    ///
    /// For each path that has mergeinfo, `receiver` is called with
    /// `(path, mergeinfo)`.
    ///
    /// `paths` is the list of absolute paths to query.  If `revision` is
    /// `Revnum(-1)` (`SVN_INVALID_REVNUM`), the youngest revision is used.
    ///
    /// Authorization is not checked (no authz callback is used).
    ///
    /// Wraps `svn_repos_fs_get_mergeinfo2`.
    pub fn fs_get_mergeinfo(
        &self,
        paths: &[&str],
        revision: Revnum,
        inherit: crate::mergeinfo::MergeinfoInheritance,
        include_descendants: bool,
        mut receiver: impl FnMut(&str, crate::mergeinfo::Mergeinfo) -> Result<(), Error<'static>>,
    ) -> Result<(), Error<'static>> {
        struct Baton<'a> {
            func:
                &'a mut dyn FnMut(&str, crate::mergeinfo::Mergeinfo) -> Result<(), Error<'static>>,
        }

        unsafe extern "C" fn mergeinfo_trampoline(
            path: *const std::os::raw::c_char,
            mergeinfo: subversion_sys::svn_mergeinfo_t,
            baton: *mut std::ffi::c_void,
            _scratch_pool: *mut apr_sys::apr_pool_t,
        ) -> *mut subversion_sys::svn_error_t {
            let baton = &mut *(baton as *mut Baton<'_>);
            let path_str = std::ffi::CStr::from_ptr(path).to_str().unwrap_or("");
            // Duplicate the mergeinfo so our Rust wrapper owns it
            let pool = apr::Pool::new();
            let dup = subversion_sys::svn_mergeinfo_dup(mergeinfo, pool.as_mut_ptr());
            let mi = crate::mergeinfo::Mergeinfo::from_ptr_and_pool(dup, pool);
            match (baton.func)(path_str, mi) {
                Ok(()) => std::ptr::null_mut(),
                Err(e) => e.into_raw(),
            }
        }

        with_tmp_pool(|pool| {
            let path_cstrings: Vec<std::ffi::CString> = paths
                .iter()
                .map(|p| std::ffi::CString::new(*p))
                .collect::<Result<_, _>>()?;
            let mut arr = apr::tables::TypedArray::<*const std::os::raw::c_char>::new(
                pool,
                path_cstrings.len() as i32,
            );
            for cstr in &path_cstrings {
                arr.push(cstr.as_ptr());
            }

            let mut baton = Baton {
                func: &mut receiver,
            };

            let err = unsafe {
                subversion_sys::svn_repos_fs_get_mergeinfo2(
                    self.ptr,
                    arr.as_ptr(),
                    revision.0,
                    inherit.into(),
                    if include_descendants { 1 } else { 0 },
                    None,                 // authz_read_func
                    std::ptr::null_mut(), // authz_read_baton
                    Some(mergeinfo_trampoline),
                    &mut baton as *mut Baton<'_> as *mut std::ffi::c_void,
                    pool.as_mut_ptr(),
                )
            };
            svn_result(err)
        })
    }
}

/// Information about a node in a repository, as returned by [`Repos::stat`].
pub struct RepoDirEntry {
    kind: crate::NodeKind,
    size: i64,
    has_props: bool,
    created_rev: crate::Revnum,
    time: apr::apr_time_t,
    last_author: Option<String>,
}

impl RepoDirEntry {
    unsafe fn from_raw(ptr: *const subversion_sys::svn_dirent_t) -> Self {
        let d = &*ptr;
        Self {
            kind: d.kind.into(),
            size: d.size,
            has_props: d.has_props != 0,
            created_rev: crate::Revnum(d.created_rev),
            time: d.time,
            last_author: if d.last_author.is_null() {
                None
            } else {
                Some(
                    std::ffi::CStr::from_ptr(d.last_author)
                        .to_string_lossy()
                        .into_owned(),
                )
            },
        }
    }

    /// The kind of this node (file, directory, etc.)
    pub fn kind(&self) -> crate::NodeKind {
        self.kind
    }

    /// The size of the file, or -1 for directories.
    pub fn size(&self) -> i64 {
        self.size
    }

    /// Whether this node has properties.
    pub fn has_props(&self) -> bool {
        self.has_props
    }

    /// The revision in which this node was last changed.
    pub fn created_rev(&self) -> crate::Revnum {
        self.created_rev
    }

    /// The timestamp of the last change.
    pub fn time(&self) -> apr::apr_time_t {
        self.time
    }

    /// The author of the last change.
    pub fn last_author(&self) -> Option<&str> {
        self.last_author.as_deref()
    }
}

/// List directory entries at `path` in `root`, calling `receiver` for each entry found.
///
/// The `depth` controls how deep into subdirectories to recurse.
/// If `path_info_only` is `true`, only the node kind is valid in the
/// [`RepoDirEntry`] passed to `receiver`.
///
/// The `patterns` slice, if non-empty, restricts results to paths whose last
/// component matches one of the glob patterns.
///
/// Wraps `svn_repos_list`.
pub fn list(
    root: &crate::fs::Root,
    path: &str,
    patterns: &[&str],
    depth: crate::Depth,
    path_info_only: bool,
    mut receiver: impl FnMut(&str, RepoDirEntry) -> Result<(), Error<'static>>,
) -> Result<(), Error<'static>> {
    let path_cstr = std::ffi::CString::new(path)?;

    struct Baton<'a> {
        func: &'a mut dyn FnMut(&str, RepoDirEntry) -> Result<(), Error<'static>>,
    }

    unsafe extern "C" fn list_trampoline(
        path: *const std::os::raw::c_char,
        dirent: *mut subversion_sys::svn_dirent_t,
        baton: *mut std::ffi::c_void,
        _scratch_pool: *mut apr_sys::apr_pool_t,
    ) -> *mut subversion_sys::svn_error_t {
        let baton = &mut *(baton as *mut Baton<'_>);
        let path_str = std::ffi::CStr::from_ptr(path).to_str().unwrap_or("");
        let entry = RepoDirEntry::from_raw(dirent);
        match (baton.func)(path_str, entry) {
            Ok(()) => std::ptr::null_mut(),
            Err(e) => e.into_raw(),
        }
    }

    let mut baton = Baton {
        func: &mut receiver,
    };

    with_tmp_pool(|pool| {
        // Build patterns array if any
        let pattern_cstrings: Vec<std::ffi::CString> = patterns
            .iter()
            .map(|p| std::ffi::CString::new(*p))
            .collect::<Result<_, _>>()?;
        let patterns_ptr: *const apr_sys::apr_array_header_t = if pattern_cstrings.is_empty() {
            std::ptr::null()
        } else {
            let mut arr = apr::tables::TypedArray::<*const std::os::raw::c_char>::new(
                pool,
                pattern_cstrings.len() as i32,
            );
            for cstr in &pattern_cstrings {
                arr.push(cstr.as_ptr());
            }
            unsafe { arr.as_ptr() }
        };

        let err = unsafe {
            subversion_sys::svn_repos_list(
                root.as_ptr() as *mut _,
                path_cstr.as_ptr(),
                patterns_ptr,
                depth.into(),
                path_info_only as i32,
                None,
                std::ptr::null_mut(),
                Some(list_trampoline),
                &mut baton as *mut Baton<'_> as *mut std::ffi::c_void,
                None,
                std::ptr::null_mut(),
                pool.as_mut_ptr(),
            )
        };
        svn_result(err)
    })
}

#[cfg(test)]
mod additional_tests {
    use super::*;
    use std::path::Path;

    #[test]
    fn test_fs_pack_invalid_path() {
        let invalid_path = Path::new("/non/existent/repo");

        // Test fs_pack with invalid repository path (should fail gracefully)
        let result = fs_pack(invalid_path, None, None);

        // Should return error for invalid repository
        assert!(result.is_err());
    }

    #[test]
    #[cfg(feature = "delta")]
    fn test_get_commit_editor_basic() {
        // Create a temporary repository for testing
        let temp_dir = tempfile::tempdir().unwrap();
        let repo_path = temp_dir.path().join("test_repo");

        // Create repository using the correct function
        let repo = Repos::create(&repo_path).unwrap();

        // Build the repository URL from the path
        let repo_url = format!("file://{}", repo_path.to_string_lossy());

        // Test get_commit_editor with basic parameters
        let result = repo.get_commit_editor(
            &repo_url,
            "", // base path (repository root - empty string in fspath format)
            "Test commit message",
            Some("test_author"),
            None, // no revprops
            None, // no commit callback
            None, // no authz callback
            None, // no txn (create new transaction)
        );

        // Should succeed in getting a commit editor
        result.unwrap();
    }

    #[test]
    fn test_authz_basic() {
        // Test creating an authz file
        let temp_dir = tempfile::tempdir().unwrap();
        let authz_path = temp_dir.path().join("authz");

        // Write a simple authz file
        std::fs::write(&authz_path, "[/]\n* = rw\n").unwrap();

        // Test reading the authz file
        let authz = Authz::read(&authz_path, None, true).unwrap();

        // Test check_access - should have read access
        let has_access = authz
            .check_access(Some("testrepo"), "/", Some("testuser"), AuthzAccess::Read)
            .unwrap();
        assert!(
            has_access,
            "Should have read access with wildcard permission"
        );
    }

    #[test]
    fn test_authz_no_access() {
        // Test with empty authz (no access)
        let temp_dir = tempfile::tempdir().unwrap();
        let authz_path = temp_dir.path().join("authz");

        // Write an empty authz file
        std::fs::write(&authz_path, "").unwrap();

        let authz = Authz::read(&authz_path, None, true).unwrap();

        // Test check_access - should have no access
        let has_access = authz
            .check_access(Some("testrepo"), "/", Some("testuser"), AuthzAccess::Read)
            .unwrap();
        assert!(!has_access, "Should have no access with empty authz");
    }

    #[test]
    fn test_authz_parse() {
        // Test parsing authz from string
        let authz_content = "[/]\n* = r\n\n[/trunk]\ntestuser = rw\n";

        let authz = Authz::parse(authz_content, None).unwrap();

        // Test read access to root (should be granted to all)
        assert!(
            authz
                .check_access(None, "/", Some("anyone"), AuthzAccess::Read)
                .unwrap(),
            "Should have read access to root"
        );

        // Test write access to trunk for testuser
        assert!(
            authz
                .check_access(None, "/trunk", Some("testuser"), AuthzAccess::Write)
                .unwrap(),
            "testuser should have write access to /trunk"
        );

        // Test write access to trunk for other user (should fail)
        assert!(
            !authz
                .check_access(None, "/trunk", Some("otheruser"), AuthzAccess::Write)
                .unwrap(),
            "otheruser should not have write access to /trunk"
        );
    }

    #[test]
    fn test_authz_parse_with_groups() {
        // Test parsing authz with groups file
        let groups_content = "[groups]\nadmins = alice, bob\n";
        let authz_content = "[/]\n@admins = rw\n* = r\n";

        let authz = Authz::parse(authz_content, Some(groups_content)).unwrap();

        // Test that group members have write access
        assert!(
            authz
                .check_access(None, "/", Some("alice"), AuthzAccess::Write)
                .unwrap(),
            "alice (admin) should have write access"
        );

        assert!(
            authz
                .check_access(None, "/", Some("bob"), AuthzAccess::Write)
                .unwrap(),
            "bob (admin) should have write access"
        );

        // Test that non-group members only have read access
        assert!(
            !authz
                .check_access(None, "/", Some("charlie"), AuthzAccess::Write)
                .unwrap(),
            "charlie should not have write access"
        );
    }

    #[test]
    fn test_authz_parse_complex_rules() {
        // Test more complex authorization rules
        let authz_content = r#"[/]
* = r

[/trunk]
dev-team = rw
* = r

[/trunk/secret]
admin = rw
* =

[/branches]
* = rw
"#;

        let authz = Authz::parse(authz_content, None).unwrap();

        // Test various access scenarios
        // Everyone can read root
        assert!(authz
            .check_access(None, "/", Some("anyone"), AuthzAccess::Read)
            .unwrap());

        // dev-team can write to trunk
        assert!(authz
            .check_access(None, "/trunk", Some("dev-team"), AuthzAccess::ReadWrite)
            .unwrap());

        // Others can only read trunk
        assert!(authz
            .check_access(None, "/trunk", Some("other"), AuthzAccess::Read)
            .unwrap());
        assert!(!authz
            .check_access(None, "/trunk", Some("other"), AuthzAccess::Write)
            .unwrap());

        // Only admin can access /trunk/secret
        assert!(authz
            .check_access(None, "/trunk/secret", Some("admin"), AuthzAccess::ReadWrite)
            .unwrap());
        assert!(!authz
            .check_access(None, "/trunk/secret", Some("other"), AuthzAccess::Read)
            .unwrap());

        // Everyone can read/write to branches
        assert!(authz
            .check_access(None, "/branches", Some("anyone"), AuthzAccess::ReadWrite)
            .unwrap());
    }

    #[test]
    fn test_authz_parse_invalid() {
        // Test parsing invalid authz content
        let invalid_authz = "this is not valid authz format";

        let result = Authz::parse(invalid_authz, None);
        assert!(result.is_err(), "Should fail to parse invalid authz");
    }

    #[test]
    fn test_check_revision_access() {
        // Create a temporary repository
        let temp_dir = tempfile::tempdir().unwrap();
        let repo_path = temp_dir.path().join("test_repo");
        let repo = Repos::create(&repo_path).unwrap();

        // Create authz rules: allow read to /trunk, deny /secret
        let authz_content = "[/trunk]\n* = r\n\n[/secret]\n* =\n";
        let authz = Authz::parse(authz_content, None).unwrap();

        // Create a revision with changes to both /trunk and /secret
        let fs = repo.fs().unwrap();
        let mut txn = fs.begin_txn(Revnum(0), 0).unwrap();
        let mut root = txn.root().unwrap();
        root.make_dir("/trunk").unwrap();
        root.make_dir("/secret").unwrap();
        root.make_file("/trunk/file.txt").unwrap();
        root.make_file("/secret/password.txt").unwrap();
        txn.commit().unwrap();

        // Check access with no authz (should be full)
        let access = repo.check_revision_access(Revnum(1), None, None).unwrap();
        assert_eq!(access, RevisionAccessLevel::Full);

        // Check access with authz that allows some paths (should be partial)
        let access = repo
            .check_revision_access(Revnum(1), Some(&authz), Some("user"))
            .unwrap();
        assert_eq!(access, RevisionAccessLevel::Partial);

        // Create a revision with only allowed paths
        let mut txn = fs.begin_txn(Revnum(1), 0).unwrap();
        let mut root = txn.root().unwrap();
        root.make_file("/trunk/another.txt").unwrap();
        txn.commit().unwrap();

        // Should have full access since all paths are readable
        let access = repo
            .check_revision_access(Revnum(2), Some(&authz), Some("user"))
            .unwrap();
        assert_eq!(access, RevisionAccessLevel::Full);

        // Create a revision with only denied paths
        let mut txn = fs.begin_txn(Revnum(2), 0).unwrap();
        let mut root = txn.root().unwrap();
        root.make_file("/secret/key.txt").unwrap();
        txn.commit().unwrap();

        // Should have no access since all paths are denied
        let access = repo
            .check_revision_access(Revnum(3), Some(&authz), Some("user"))
            .unwrap();
        assert_eq!(access, RevisionAccessLevel::None);
    }

    #[test]
    fn test_report_with_commit_editor() {
        // Create a temporary repository for testing
        let temp_dir = tempfile::tempdir().unwrap();
        let repo_path = temp_dir.path().join("test_repo");

        // Create repository
        let repo = Repos::create(&repo_path).unwrap();

        // Build the repository URL from the path
        let repo_url = format!("file://{}", repo_path.to_string_lossy());

        // Get a commit editor which we can use for testing reports
        let editor_result = repo.get_commit_editor(
            &repo_url,
            "/",
            "Test commit for report testing",
            Some("test_author"),
            None,
            None,
            None, // no authz callback
            None, // no txn
        );

        assert!(
            editor_result.is_ok(),
            "Failed to get commit editor: {:?}",
            editor_result.err()
        );

        if let Ok(editor_box) = editor_result {
            // Use the Editor trait's as_raw_parts method to access raw pointers
            let (editor_ptr, baton_ptr) = editor_box.as_raw_parts();

            // Test begin_report with the commit editor
            let report_result = unsafe {
                repo.begin_report(
                    crate::Revnum::from_raw(0).unwrap(),
                    "/",
                    "",   // Empty string means all of fs_base
                    None, // No path switching
                    false,
                    crate::Depth::Infinity,
                    false,
                    false,
                    editor_ptr,
                    baton_ptr,
                    None, // no authz callback
                )
            };

            // begin_report should succeed
            assert!(
                report_result.is_ok(),
                "Failed to begin report: {:?}",
                report_result.err()
            );

            if let Ok(report) = report_result {
                // Test abort (this should work and not crash)
                let abort_result = report.abort();
                assert!(
                    abort_result.is_ok(),
                    "Failed to abort report: {:?}",
                    abort_result.err()
                );
            }
        }
    }

    #[test]
    fn test_report_operations() {
        // Create a temporary repository for testing
        let temp_dir = tempfile::tempdir().unwrap();
        let repo_path = temp_dir.path().join("test_repo");

        // Create repository
        let repo = Repos::create(&repo_path).unwrap();

        // Build the repository URL
        let repo_url = format!("file://{}", repo_path.to_string_lossy());

        // Get a commit editor using the public API
        let editor_box = repo
            .get_commit_editor(
                &repo_url,
                "/",
                "Test commit for report operations",
                Some("test_author"),
                None,
                None,
                None, // no authz callback
                None, // no txn
            )
            .unwrap();

        // Create a report using the editor's raw pointers
        let (editor_ptr, baton_ptr) = editor_box.as_raw_parts();
        let report = unsafe {
            repo.begin_report(
                crate::Revnum::from_raw(0).unwrap(),
                "/",
                "",   // Empty string means all of fs_base
                None, // No path switching
                false,
                crate::Depth::Infinity,
                false,
                false,
                editor_ptr,
                baton_ptr,
                None, // no authz callback
            )
        }
        .unwrap();

        report
            .set_path(
                "",
                crate::Revnum::from_raw(0).unwrap(),
                crate::Depth::Infinity,
                false,
                None,
            )
            .unwrap();

        report.delete_path("nonexistent_path").unwrap();

        report
            .link_path(
                "link_target",
                "link_source",
                crate::Revnum::from_raw(0).unwrap(),
                crate::Depth::Files,
                false,
                None,
            )
            .unwrap();

        report.abort().unwrap();
    }

    #[test]
    fn test_hook_paths() {
        // Create a temporary repository for testing
        let temp_dir = tempfile::tempdir().unwrap();
        let repo_path = temp_dir.path().join("test_repo");

        // Create repository
        let repo = Repos::create(&repo_path).unwrap();

        // Test getting hook paths - they should all return valid paths
        let start_commit_hook = repo.start_commit_hook_path();
        assert!(start_commit_hook.to_str().unwrap().contains("start-commit"));
        assert!(start_commit_hook.to_str().unwrap().contains("hooks"));

        let pre_commit_hook = repo.pre_commit_hook_path();
        assert!(pre_commit_hook.to_str().unwrap().contains("pre-commit"));

        let post_commit_hook = repo.post_commit_hook_path();
        assert!(post_commit_hook.to_str().unwrap().contains("post-commit"));

        let pre_revprop_hook = repo.pre_revprop_change_hook_path();
        assert!(pre_revprop_hook
            .to_str()
            .unwrap()
            .contains("pre-revprop-change"));

        let post_revprop_hook = repo.post_revprop_change_hook_path();
        assert!(post_revprop_hook
            .to_str()
            .unwrap()
            .contains("post-revprop-change"));

        let pre_lock_hook = repo.pre_lock_hook_path();
        assert!(pre_lock_hook.to_str().unwrap().contains("pre-lock"));

        let post_lock_hook = repo.post_lock_hook_path();
        assert!(post_lock_hook.to_str().unwrap().contains("post-lock"));

        let pre_unlock_hook = repo.pre_unlock_hook_path();
        assert!(pre_unlock_hook.to_str().unwrap().contains("pre-unlock"));

        let post_unlock_hook = repo.post_unlock_hook_path();
        assert!(post_unlock_hook.to_str().unwrap().contains("post-unlock"));
    }

    #[test]
    fn test_hooks_setenv() {
        // Create a temporary repository for testing
        let temp_dir = tempfile::tempdir().unwrap();
        let repo_path = temp_dir.path().join("test_repo");

        // Create repository
        let mut repo = Repos::create(&repo_path).unwrap();

        // Create a hooks env file
        let hooks_env_path = temp_dir.path().join("hooks.env");
        std::fs::write(&hooks_env_path, "TEST_VAR=test_value\n").unwrap();

        // Test setting the hooks environment
        repo.hooks_setenv(hooks_env_path.to_str().unwrap()).unwrap();
    }

    #[test]
    fn test_dir_delta2() {
        // Create a temporary repository for testing
        let temp_dir = tempfile::tempdir().unwrap();
        let repo_path = temp_dir.path().join("test_repo");

        // Create repository and filesystem
        let repo = Repos::create(&repo_path).unwrap();
        let fs = repo.fs().unwrap();

        // Create initial content
        let mut txn = fs.begin_txn(crate::Revnum::from(0u32), 0).unwrap();
        let mut txn_root = txn.root().unwrap();

        // Create a test file
        txn_root.make_dir("/trunk").unwrap();
        txn_root.make_file("/trunk/test.txt").unwrap();

        let mut stream = txn_root.apply_text("/trunk/test.txt", None).unwrap();
        stream.write(b"Initial content\n").unwrap();
        stream.close().unwrap();

        txn.commit().unwrap();

        // Create a second revision with changes
        let mut txn2 = fs.begin_txn(crate::Revnum::from(1u32), 0).unwrap();
        let mut txn_root2 = txn2.root().unwrap();

        let mut stream2 = txn_root2.apply_text("/trunk/test.txt", None).unwrap();
        stream2.write(b"Modified content\n").unwrap();
        stream2.close().unwrap();

        txn_root2.make_file("/trunk/new.txt").unwrap();
        let mut stream3 = txn_root2.apply_text("/trunk/new.txt", None).unwrap();
        stream3.write(b"New file\n").unwrap();
        stream3.close().unwrap();

        txn2.commit().unwrap();

        // Get roots for comparison
        let root1 = fs.revision_root(crate::Revnum::from(1u32)).unwrap();
        let root2 = fs.revision_root(crate::Revnum::from(2u32)).unwrap();

        // Create a default editor for testing
        let pool = apr::Pool::new();
        let editor = crate::delta::default_editor(pool);

        // Test dir_delta2 with a proper no-op editor
        let result = dir_delta2(
            &root1,
            "/trunk",
            "",
            &root2,
            "/trunk",
            &editor,
            true, // text_deltas
            crate::Depth::Infinity,
            false, // entry_props
            false, // ignore_ancestry
        );

        // Should succeed with a proper editor
        result.unwrap();
    }

    #[test]
    fn test_get_commit_editor_with_authz_callback() {
        // Test get_commit_editor with an authz callback
        let temp_dir = tempfile::tempdir().unwrap();
        let repo_path = temp_dir.path().join("test_repo");
        let repo = Repos::create(&repo_path).unwrap();
        let repo_url = format!("file://{}", repo_path.to_string_lossy());

        // Track callback invocations
        let authz_called = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false));
        let authz_called_clone = authz_called.clone();

        // Test with authz callback that always allows access
        let result = repo.get_commit_editor(
            &repo_url,
            "",
            "Test commit with authz",
            Some("test_author"),
            None,
            None,
            Some(Box::new(move |_access, _root, _path| {
                authz_called_clone.store(true, std::sync::atomic::Ordering::SeqCst);
                Ok(true) // Allow access
            })),
            None, // no txn
        );

        assert!(
            result.is_ok(),
            "Failed to get commit editor with authz callback: {:?}",
            result.err()
        );

        // Note: The authz callback is only called when the editor is actually used to commit something,
        // not when it's created. So we don't expect it to be called here.
    }

    #[test]
    fn test_begin_report_with_authz_callback() {
        // Test begin_report with an authz callback
        let temp_dir = tempfile::tempdir().unwrap();
        let repo_path = temp_dir.path().join("test_repo");
        let repo = Repos::create(&repo_path).unwrap();
        let repo_url = format!("file://{}", repo_path.to_string_lossy());

        // Get a commit editor first
        let editor_result = repo.get_commit_editor(
            &repo_url,
            "",
            "Test commit",
            Some("test_author"),
            None,
            None,
            None,
            None, // no txn
        );

        let editor_box = editor_result.unwrap();
        {
            let (editor_ptr, baton_ptr) = editor_box.as_raw_parts();

            // Track callback invocations
            let authz_called = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false));
            let authz_called_clone = authz_called.clone();

            // Test begin_report with authz callback
            let report_result = unsafe {
                repo.begin_report(
                    crate::Revnum::from_raw(0).unwrap(),
                    "/",
                    "",
                    None,
                    false,
                    crate::Depth::Infinity,
                    false,
                    false,
                    editor_ptr,
                    baton_ptr,
                    Some(Box::new(move |_root, _path| {
                        authz_called_clone.store(true, std::sync::atomic::Ordering::SeqCst);
                        Ok(true) // Allow access
                    })),
                )
            };

            let report = report_result.unwrap();
            report.abort().unwrap();

            // Note: Like get_commit_editor, the authz callback is only called when the report is used,
            // not when it's created.
        }
    }

    #[test]
    fn test_callback_cleanup() {
        // Test that callback batons are properly cleaned up when editors are dropped
        use std::sync::atomic::{AtomicBool, Ordering};
        use std::sync::Arc;

        let temp_dir = tempfile::tempdir().unwrap();
        let repo_path = temp_dir.path().join("test_repo");
        let repo = Repos::create(&repo_path).unwrap();

        // Create a test file in the repository
        let fs = repo.fs().unwrap();
        let mut txn = fs.begin_txn(crate::Revnum(0), 0).unwrap();
        let mut txn_root = txn.root().unwrap();
        txn_root.make_file("/test.txt").unwrap();
        let mut stream = txn_root.apply_text("/test.txt", None).unwrap();
        stream.write(b"test content").unwrap();
        stream.close().unwrap();
        txn.commit().unwrap();

        // Track if the callback was called and if it gets dropped
        let callback_called = Arc::new(AtomicBool::new(false));
        let callback_called_clone = callback_called.clone();

        // Create a scope where the editor will be created and dropped
        {
            let mut buffer = Vec::new();
            let mut stream = crate::io::wrap_write(&mut buffer).unwrap();

            // This should create an editor with a callback, then drop it
            let mut options = DumpOptions {
                start_rev: Some(crate::Revnum(1)),
                end_rev: Some(crate::Revnum(1)),
                include_revprops: true,
                include_changes: true,
                filter_func: Some(Box::new(move |_root, _path| {
                    callback_called_clone.store(true, Ordering::SeqCst);
                    Ok(true)
                })),
                ..Default::default()
            };
            repo.dump(&mut stream, &mut options).unwrap();
            // Editor is dropped here when result goes out of scope
        }

        // If we get here without segfaulting, the cleanup worked!
        // The callback baton was properly freed in the editor's Drop implementation.
    }

    #[test]
    fn test_get_commit_editor_with_txn_none() {
        // Test that get_commit_editor works with txn=None (creates new transaction)
        let temp_dir = tempfile::tempdir().unwrap();
        let repo_path = temp_dir.path().join("test_repo");
        let repo = Repos::create(&repo_path).unwrap();
        let repo_url = format!("file://{}", repo_path.to_string_lossy());

        // Test with txn=None - should create a new transaction internally
        let result = repo.get_commit_editor(
            &repo_url,
            "",
            "Test commit with txn=None",
            Some("test_author"),
            None,
            None,
            None,
            None, // txn = None means create new transaction
        );

        assert!(
            result.is_ok(),
            "get_commit_editor with txn=None should succeed: {:?}",
            result.err()
        );
    }

    #[test]
    fn test_commit_txn_creates_revision() {
        // Create a fresh repository
        let temp_dir = tempfile::tempdir().unwrap();
        let repo_path = temp_dir.path().join("test_repo");
        let repo = Repos::create(&repo_path).unwrap();

        // Begin a transaction at revision 0 with author + log message
        let txn = repo
            .begin_txn_for_commit(
                crate::Revnum(0),
                "test_author",
                "Test commit via commit_txn",
            )
            .unwrap();

        // Commit the (empty) transaction through the repos layer (runs hooks)
        let (new_rev, conflict) = repo.commit_txn(txn).unwrap();

        // An empty transaction still creates revision 1
        assert_eq!(new_rev, crate::Revnum(1));
        // No conflicts expected
        assert_eq!(conflict, None);
    }

    #[test]
    fn test_commit_txn_with_changes() {
        // Create a fresh repository
        let temp_dir = tempfile::tempdir().unwrap();
        let repo_path = temp_dir.path().join("test_repo");
        let repo = Repos::create(&repo_path).unwrap();

        // Begin a transaction
        let mut txn = repo
            .begin_txn_for_commit(crate::Revnum(0), "test_author", "Add trunk directory")
            .unwrap();

        // Make a change: create /trunk in the transaction root
        {
            let mut root = txn.root().unwrap();
            root.make_dir("/trunk").unwrap();
        }

        // Commit via the repos layer
        let (new_rev, conflict) = repo.commit_txn(txn).unwrap();

        assert_eq!(new_rev, crate::Revnum(1));
        assert_eq!(conflict, None);

        // Verify the change made it into the repository by reading the committed revision
        let fs = repo.fs().unwrap();
        let rev_root = fs.revision_root(crate::Revnum(1)).unwrap();
        let kind = rev_root.check_path("/trunk").unwrap();
        assert_eq!(kind, crate::NodeKind::Dir);
    }

    #[test]
    fn test_dated_revision_returns_rev0() {
        let temp_dir = tempfile::tempdir().unwrap();
        let repo_path = temp_dir.path().join("test_repo");
        let repo = Repos::create(&repo_path).unwrap();

        // Any timestamp before the repository was created should return rev 0.
        // Use 1 microsecond after the Unix epoch.
        let rev = repo.dated_revision(1).unwrap();
        assert_eq!(rev, crate::Revnum(0));
    }

    #[test]
    fn test_deleted_rev_finds_deletion() {
        let temp_dir = tempfile::tempdir().unwrap();
        let repo_path = temp_dir.path().join("test_repo");
        let repo = Repos::create(&repo_path).unwrap();

        // Rev 1: add /foo
        let mut txn = repo
            .begin_txn_for_commit(crate::Revnum(0), "author", "add foo")
            .unwrap();
        txn.root().unwrap().make_file("/foo").unwrap();
        repo.commit_txn(txn).unwrap();

        // Rev 2: delete /foo
        let mut txn = repo
            .begin_txn_for_commit(crate::Revnum(1), "author", "delete foo")
            .unwrap();
        txn.root().unwrap().delete("/foo").unwrap();
        repo.commit_txn(txn).unwrap();

        let deleted = repo
            .deleted_rev("/foo", crate::Revnum(1), crate::Revnum(2))
            .unwrap();
        assert_eq!(deleted, Some(crate::Revnum(2)));
    }

    #[test]
    fn test_deleted_rev_returns_none_when_not_deleted() {
        let temp_dir = tempfile::tempdir().unwrap();
        let repo_path = temp_dir.path().join("test_repo");
        let repo = Repos::create(&repo_path).unwrap();

        // Rev 1: add /foo
        let mut txn = repo
            .begin_txn_for_commit(crate::Revnum(0), "author", "add foo")
            .unwrap();
        txn.root().unwrap().make_file("/foo").unwrap();
        repo.commit_txn(txn).unwrap();

        // /foo is not deleted in range 1..=1
        let deleted = repo
            .deleted_rev("/foo", crate::Revnum(1), crate::Revnum(1))
            .unwrap();
        assert_eq!(deleted, None);
    }

    #[test]
    fn test_history_collects_revisions() {
        let temp_dir = tempfile::tempdir().unwrap();
        let repo_path = temp_dir.path().join("test_repo");
        let repo = Repos::create(&repo_path).unwrap();

        // Rev 1: add /bar
        let mut txn = repo
            .begin_txn_for_commit(crate::Revnum(0), "author", "add bar")
            .unwrap();
        txn.root().unwrap().make_file("/bar").unwrap();
        repo.commit_txn(txn).unwrap();

        // Rev 2: add a property on /bar to create a second revision of the node
        let mut txn = repo
            .begin_txn_for_commit(crate::Revnum(1), "author", "tag bar")
            .unwrap();
        txn.root()
            .unwrap()
            .change_node_prop("/bar", "test:prop", b"value")
            .unwrap();
        repo.commit_txn(txn).unwrap();

        let mut entries: Vec<(String, crate::Revnum)> = Vec::new();
        repo.history(
            "/bar",
            crate::Revnum(1),
            crate::Revnum(2),
            false,
            &mut |path, rev| {
                entries.push((path.to_string(), rev));
                Ok(())
            },
        )
        .unwrap();

        // Expect rev 2 and rev 1 (newest first)
        assert_eq!(entries.len(), 2);
        assert_eq!(entries[0], ("/bar".to_string(), crate::Revnum(2)));
        assert_eq!(entries[1], ("/bar".to_string(), crate::Revnum(1)));
    }

    #[test]
    fn test_history_callback_error_propagates() {
        let temp_dir = tempfile::tempdir().unwrap();
        let repo_path = temp_dir.path().join("test_repo");
        let repo = Repos::create(&repo_path).unwrap();

        // Rev 1: add /baz
        let mut txn = repo
            .begin_txn_for_commit(crate::Revnum(0), "author", "add baz")
            .unwrap();
        txn.root().unwrap().make_file("/baz").unwrap();
        repo.commit_txn(txn).unwrap();

        let result = repo.history(
            "/baz",
            crate::Revnum(0),
            crate::Revnum(1),
            false,
            &mut |_path, _rev| Err(crate::Error::from_message("test error")),
        );

        assert!(result.is_err());
    }

    #[test]
    fn test_get_logs_retrieves_entries() {
        let temp_dir = tempfile::tempdir().unwrap();
        let repo_path = temp_dir.path().join("test_repo");
        let repo = Repos::create(&repo_path).unwrap();

        // Rev 1: add /file.txt
        let mut txn = repo
            .begin_txn_for_commit(crate::Revnum(0), "alice", "initial commit")
            .unwrap();
        txn.root().unwrap().make_file("/file.txt").unwrap();
        repo.commit_txn(txn).unwrap();

        // Rev 2: add /other.txt
        let mut txn = repo
            .begin_txn_for_commit(crate::Revnum(1), "bob", "add other")
            .unwrap();
        txn.root().unwrap().make_file("/other.txt").unwrap();
        repo.commit_txn(txn).unwrap();

        let mut revisions: Vec<crate::Revnum> = Vec::new();
        repo.get_logs(
            &[],
            crate::Revnum(1),
            crate::Revnum(2),
            0,     // limit = unlimited
            false, // discover_changed_paths
            false, // strict_node_history
            false, // include_merged_revisions
            &["svn:log", "svn:author"],
            &mut |entry| {
                if let Some(rev) = entry.revision() {
                    revisions.push(rev);
                }
                Ok(())
            },
        )
        .unwrap();

        assert_eq!(revisions.len(), 2);
        assert_eq!(revisions[0], crate::Revnum(1));
        assert_eq!(revisions[1], crate::Revnum(2));
    }

    #[test]
    fn test_get_logs_with_limit() {
        let temp_dir = tempfile::tempdir().unwrap();
        let repo_path = temp_dir.path().join("test_repo");
        let repo = Repos::create(&repo_path).unwrap();

        // Create 3 revisions
        let files = ["/file0.txt", "/file1.txt", "/file2.txt"];
        for (i, path) in files.iter().enumerate() {
            let mut txn = repo
                .begin_txn_for_commit(crate::Revnum(i as i64), "author", "commit")
                .unwrap();
            txn.root().unwrap().make_file(*path).unwrap();
            repo.commit_txn(txn).unwrap();
        }

        let mut count = 0usize;
        repo.get_logs(
            &[],
            crate::Revnum(1),
            crate::Revnum(3),
            2, // limit = 2
            false,
            false,
            false,
            &[],
            &mut |_entry| {
                count += 1;
                Ok(())
            },
        )
        .unwrap();

        assert_eq!(count, 2);
    }

    #[test]
    fn test_trace_node_locations_tracks_file() {
        let temp_dir = tempfile::tempdir().unwrap();
        let repo_path = temp_dir.path().join("test_repo");
        let repo = Repos::create(&repo_path).unwrap();

        // Rev 1: add /file.txt
        let mut txn = repo
            .begin_txn_for_commit(crate::Revnum(0), "alice", "add file")
            .unwrap();
        txn.root().unwrap().make_file("/file.txt").unwrap();
        repo.commit_txn(txn).unwrap();

        // Rev 2: add another file (file.txt unchanged)
        let mut txn = repo
            .begin_txn_for_commit(crate::Revnum(1), "bob", "add other")
            .unwrap();
        txn.root().unwrap().make_file("/other.txt").unwrap();
        repo.commit_txn(txn).unwrap();

        let mut fs = repo.fs().expect("repos must have a filesystem");
        let locations = repo
            .trace_node_locations(
                &mut fs,
                "/file.txt",
                crate::Revnum(1),
                &[crate::Revnum(1), crate::Revnum(2)],
            )
            .unwrap();

        // /file.txt should be at /file.txt at both revisions.
        assert_eq!(
            locations.get(&crate::Revnum(1)).map(|s| s.as_str()),
            Some("/file.txt")
        );
        assert_eq!(
            locations.get(&crate::Revnum(2)).map(|s| s.as_str()),
            Some("/file.txt")
        );
    }

    #[test]
    fn test_fs_get_inherited_props_returns_empty_for_root() {
        let temp_dir = tempfile::tempdir().unwrap();
        let repo_path = temp_dir.path().join("test_repo");
        let repo = Repos::create(&repo_path).unwrap();

        // Rev 1: add /file.txt with a property
        let mut txn = repo
            .begin_txn_for_commit(crate::Revnum(0), "alice", "add file")
            .unwrap();
        let mut txn_root = txn.root().unwrap();
        txn_root.make_file("/file.txt").unwrap();
        drop(txn_root);
        repo.commit_txn(txn).unwrap();

        // Get revision root at rev 1
        let fs = repo.fs().expect("repos must have a filesystem");
        let mut root = fs.revision_root(crate::Revnum(1)).unwrap();

        // /file.txt has no explicit properties set on ancestors
        let inherited = repo
            .fs_get_inherited_props(&mut root, "/file.txt", None)
            .unwrap();

        // The result should be a list (possibly empty, possibly with root entry).
        // Since we set no properties on any ancestors, all entries should have
        // empty property maps.
        for (_path, props) in &inherited {
            assert!(
                props.is_empty(),
                "no props expected on ancestors, got {props:?}"
            );
        }
    }

    #[test]
    fn test_rev_prop_returns_value() {
        let temp_dir = tempfile::tempdir().unwrap();
        let repo_path = temp_dir.path().join("test_repo");
        let repo = Repos::create(&repo_path).unwrap();

        // Rev 0 always has svn:date set by SVN itself
        let date_val = repo.rev_prop(crate::Revnum(0), "svn:date").unwrap();

        // svn:date should be present (a non-empty ISO 8601 timestamp)
        assert!(date_val.is_some(), "svn:date should be present on rev 0");
        let date_bytes = date_val.unwrap();
        assert!(
            !date_bytes.is_empty(),
            "svn:date should be a non-empty value"
        );
        // Sanity check: should look like an SVN timestamp "YYYY-MM-DDThh:mm:ss.uuuuuuZ"
        let date_str = std::str::from_utf8(&date_bytes).unwrap();
        assert!(
            date_str.contains('T'),
            "svn:date should contain 'T': {date_str}"
        );
    }

    #[test]
    fn test_rev_prop_returns_none_for_missing() {
        let temp_dir = tempfile::tempdir().unwrap();
        let repo_path = temp_dir.path().join("test_repo");
        let repo = Repos::create(&repo_path).unwrap();

        // Rev 0 has no svn:log property
        let val = repo.rev_prop(crate::Revnum(0), "svn:log").unwrap();
        assert_eq!(val, None);
    }

    #[test]
    fn test_stat_returns_dirent_for_existing_path() {
        let temp_dir = tempfile::tempdir().unwrap();
        let repo_path = temp_dir.path().join("test_repo");
        let repo = Repos::create(&repo_path).unwrap();

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

        let root = fs.revision_root(rev1).unwrap();
        let entry = repo.stat(&root, "/readme.txt").unwrap();
        assert!(entry.is_some(), "stat should return Some for existing path");
        let entry = entry.unwrap();
        assert_eq!(entry.kind(), crate::NodeKind::File);
        assert_eq!(entry.created_rev(), rev1);
    }

    #[test]
    fn test_stat_returns_none_for_missing_path() {
        let temp_dir = tempfile::tempdir().unwrap();
        let repo_path = temp_dir.path().join("test_repo");
        let repo = Repos::create(&repo_path).unwrap();

        let fs = repo.fs().unwrap();
        let root = fs.revision_root(crate::Revnum(0)).unwrap();
        let entry = repo.stat(&root, "/no-such-path").unwrap();
        assert_eq!(
            entry.is_none(),
            true,
            "stat should return None for missing path"
        );
    }

    #[test]
    fn test_get_file_revs_collects_revisions() {
        let temp_dir = tempfile::tempdir().unwrap();
        let repo_path = temp_dir.path().join("test_repo");
        let repo = Repos::create(&repo_path).unwrap();

        let fs = repo.fs().unwrap();
        // Create /hello.txt in rev 1
        let mut txn = fs.begin_txn(crate::Revnum(0), 0).unwrap();
        {
            let mut root = txn.root().unwrap();
            root.make_file("/hello.txt").unwrap();
            root.set_file_contents("/hello.txt", b"hello\n").unwrap();
        }
        let rev1 = txn.commit().unwrap();

        // Modify /hello.txt in rev 2
        let mut txn2 = fs.begin_txn(rev1, 0).unwrap();
        {
            let mut root = txn2.root().unwrap();
            root.set_file_contents("/hello.txt", b"hello world\n")
                .unwrap();
        }
        let rev2 = txn2.commit().unwrap();

        let mut seen_revs: Vec<crate::Revnum> = Vec::new();
        repo.get_file_revs(
            "/hello.txt",
            crate::Revnum(1),
            rev2,
            false,
            |_path, rev, _rev_props, _merged, _prop_diffs| {
                seen_revs.push(rev);
                Ok(())
            },
        )
        .unwrap();

        assert_eq!(seen_revs, vec![rev1, rev2]);
    }

    #[test]
    fn test_get_file_revs_empty_range() {
        let temp_dir = tempfile::tempdir().unwrap();
        let repo_path = temp_dir.path().join("test_repo");
        let repo = Repos::create(&repo_path).unwrap();

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

        // Ask for revisions before the file existed (rev 0 to rev 0) — should yield nothing
        let mut count = 0usize;
        repo.get_file_revs(
            "/file.txt",
            rev1,
            rev1,
            false,
            |_path, _rev, _rev_props, _merged, _prop_diffs| {
                count += 1;
                Ok(())
            },
        )
        .unwrap();

        assert_eq!(count, 1, "file created in rev1 should appear once");
    }

    #[test]
    fn test_repos_info_format() {
        let temp_dir = tempfile::tempdir().unwrap();
        let repo_path = temp_dir.path().join("test_repo");
        let repo = Repos::create(&repo_path).unwrap();

        let (format, version) = repo.info_format().unwrap();
        assert!(format > 0, "repos format should be positive, got {format}");
        // supports_version is the minimum SVN version that can read this format
        let (major, minor, patch) = version;
        assert!(
            major >= 0 && minor >= 0 && patch >= 0,
            "version fields should be non-negative: ({major}, {minor}, {patch})"
        );
    }

    #[test]
    fn test_node_location_segments_simple() {
        let temp_dir = tempfile::tempdir().unwrap();
        let repo_path = temp_dir.path().join("test_repo");
        let repo = Repos::create(&repo_path).unwrap();

        // Use repos-level commit so the repos layer tracks the revision
        let mut txn = repo
            .begin_txn_for_commit(crate::Revnum(0), "author", "create file")
            .unwrap();
        txn.root().unwrap().make_file("/hello.txt").unwrap();
        let rev1 = repo.commit_txn(txn).unwrap().0;

        let mut segments: Vec<(Option<String>, crate::Revnum, crate::Revnum)> = Vec::new();
        repo.node_location_segments("/hello.txt", rev1, rev1, rev1, |path, start, end| {
            segments.push((path.map(str::to_owned), start, end));
            Ok(())
        })
        .unwrap();

        // Should have one segment: /hello.txt at rev1
        assert_eq!(segments.len(), 1);
        // The path returned is without leading slash
        assert_eq!(segments[0].0.as_deref(), Some("hello.txt"));
        assert_eq!(segments[0].1, rev1);
        assert_eq!(segments[0].2, rev1);
    }

    #[test]
    fn test_list_root_directory() {
        let temp_dir = tempfile::tempdir().unwrap();
        let repo_path = temp_dir.path().join("test_repo");
        let repo = Repos::create(&repo_path).unwrap();

        let fs = repo.fs().unwrap();
        // Create a directory and two files in rev 1
        let mut txn = fs.begin_txn(crate::Revnum(0), 0).unwrap();
        {
            let mut root = txn.root().unwrap();
            root.make_dir("/trunk").unwrap();
            root.make_file("/trunk/foo.txt").unwrap();
            root.make_file("/trunk/bar.txt").unwrap();
        }
        let rev1 = txn.commit().unwrap();

        let root = fs.revision_root(rev1).unwrap();
        let mut entries: Vec<String> = Vec::new();
        list(
            &root,
            "/trunk",
            &[],
            crate::Depth::Immediates,
            false,
            |path, _entry| {
                entries.push(path.to_owned());
                Ok(())
            },
        )
        .unwrap();

        entries.sort();
        assert_eq!(entries, vec!["/trunk", "/trunk/bar.txt", "/trunk/foo.txt"]);
    }

    #[test]
    fn test_list_with_pattern() {
        let temp_dir = tempfile::tempdir().unwrap();
        let repo_path = temp_dir.path().join("test_repo");
        let repo = Repos::create(&repo_path).unwrap();

        let fs = repo.fs().unwrap();
        let mut txn = fs.begin_txn(crate::Revnum(0), 0).unwrap();
        {
            let mut root = txn.root().unwrap();
            root.make_dir("/src").unwrap();
            root.make_file("/src/main.rs").unwrap();
            root.make_file("/src/lib.rs").unwrap();
            root.make_file("/src/README.md").unwrap();
        }
        let rev1 = txn.commit().unwrap();

        let root = fs.revision_root(rev1).unwrap();
        let mut entries: Vec<String> = Vec::new();
        list(
            &root,
            "/src",
            &["*.rs"],
            crate::Depth::Immediates,
            false,
            |path, _entry| {
                entries.push(path.to_owned());
                Ok(())
            },
        )
        .unwrap();

        entries.sort();
        // Only .rs files should match the pattern (the /src dir itself doesn't match *.rs)
        assert_eq!(entries, vec!["/src/lib.rs", "/src/main.rs"]);
    }

    #[test]
    fn test_list_dirent_fields() {
        let temp_dir = tempfile::tempdir().unwrap();
        let repo_path = temp_dir.path().join("test_repo");
        let repo = Repos::create(&repo_path).unwrap();

        let fs = repo.fs().unwrap();
        let mut txn = fs.begin_txn(crate::Revnum(0), 0).unwrap();
        {
            let mut root = txn.root().unwrap();
            root.make_dir("/docs").unwrap();
            root.make_file("/docs/guide.txt").unwrap();
        }
        let rev1 = txn.commit().unwrap();

        let root = fs.revision_root(rev1).unwrap();
        let mut entries: Vec<(String, RepoDirEntry)> = Vec::new();
        list(
            &root,
            "/docs",
            &[],
            crate::Depth::Immediates,
            false,
            |path, entry| {
                entries.push((path.to_owned(), entry));
                Ok(())
            },
        )
        .unwrap();

        // Should have /docs (dir) and /docs/guide.txt (file)
        let file_entry = entries
            .iter()
            .find(|(p, _)| p == "/docs/guide.txt")
            .expect("should have found /docs/guide.txt");
        assert_eq!(file_entry.1.kind(), crate::NodeKind::File);
        assert_eq!(file_entry.1.created_rev(), rev1);

        let dir_entry = entries
            .iter()
            .find(|(p, _)| p == "/docs")
            .expect("should have found /docs directory entry");
        assert_eq!(dir_entry.1.kind(), crate::NodeKind::Dir);
    }

    #[test]
    #[cfg(feature = "delta")]
    fn test_replay_runs_without_error() {
        let temp_dir = tempfile::tempdir().unwrap();
        let repo_path = temp_dir.path().join("test_repo");
        let repo = Repos::create(&repo_path).unwrap();

        let fs = repo.fs().unwrap();
        // Create a file in rev 1
        let mut txn = fs.begin_txn(crate::Revnum(0), 0).unwrap();
        {
            let mut root = txn.root().unwrap();
            root.make_file("/hello.txt").unwrap();
        }
        let rev1 = txn.commit().unwrap();

        let root = fs.revision_root(rev1).unwrap();

        // Use a default no-op editor; just verify replay doesn't error
        let pool = apr::Pool::new();
        let editor = crate::delta::default_editor(pool);
        replay(&root, "", crate::Revnum(-1), false, &editor).unwrap();
    }

    #[test]
    fn test_fs_get_mergeinfo_empty_when_not_set() {
        let temp_dir = tempfile::tempdir().unwrap();
        let repo_path = temp_dir.path().join("test_repo");
        let repo = Repos::create(&repo_path).unwrap();

        let fs = repo.fs().unwrap();
        let mut txn = fs.begin_txn(crate::Revnum(0), 0).unwrap();
        txn.root().unwrap().make_file("/hello.txt").unwrap();
        let rev1 = txn.commit().unwrap();

        let mut received: Vec<String> = Vec::new();
        repo.fs_get_mergeinfo(
            &["/hello.txt"],
            rev1,
            crate::mergeinfo::MergeinfoInheritance::Explicit,
            false,
            |path, _mi| {
                received.push(path.to_owned());
                Ok(())
            },
        )
        .unwrap();

        // No mergeinfo was set, so receiver should not be called
        assert_eq!(received, Vec::<String>::new());
    }

    #[test]
    fn test_fs_get_mergeinfo_returns_value_when_set() {
        let temp_dir = tempfile::tempdir().unwrap();
        let repo_path = temp_dir.path().join("test_repo");
        let repo = Repos::create(&repo_path).unwrap();

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

        // Create /trunk in rev 1
        let mut txn = fs.begin_txn(crate::Revnum(0), 0).unwrap();
        txn.root().unwrap().make_dir("/trunk").unwrap();
        let rev1 = txn.commit().unwrap();

        // Set svn:mergeinfo on /trunk in rev 2
        let mut txn2 = fs.begin_txn(rev1, 0).unwrap();
        txn2.root()
            .unwrap()
            .change_node_prop("/trunk", "svn:mergeinfo", b"/branches/dev:1")
            .unwrap();
        let rev2 = txn2.commit().unwrap();

        let mut received: Vec<(String, String)> = Vec::new();
        repo.fs_get_mergeinfo(
            &["/trunk"],
            rev2,
            crate::mergeinfo::MergeinfoInheritance::Explicit,
            false,
            |path, mi| {
                received.push((path.to_owned(), mi.to_string().unwrap()));
                Ok(())
            },
        )
        .unwrap();

        assert_eq!(received.len(), 1);
        assert_eq!(received[0].0, "/trunk");
        assert!(
            received[0].1.contains("/branches/dev"),
            "mergeinfo should contain source: {}",
            received[0].1
        );
    }

    #[test]
    fn test_repos_fs_lock_and_unlock() {
        let temp_dir = tempfile::tempdir().unwrap();
        let repo_path = temp_dir.path().join("test_repo");
        let mut repo = Repos::create(&repo_path).unwrap();

        // Create a file in rev 1 so we can lock it
        let mut txn = repo
            .begin_txn_for_commit(crate::Revnum(0), "author", "add file")
            .unwrap();
        txn.root().unwrap().make_file("/file.txt").unwrap();
        repo.commit_txn(txn).unwrap();

        // Set a username on the underlying filesystem (required for locking).
        // Keep the Fs alive so the access context pool stays valid.
        let mut fs_for_access = repo.fs().unwrap();
        fs_for_access.set_access("testuser").unwrap();

        // Lock the file using the repos layer (runs pre/post hooks)
        let lock = repo
            .fs_lock(
                "/file.txt",
                None,
                Some("test lock"),
                false,
                None,
                crate::Revnum::invalid(),
                false,
            )
            .expect("fs_lock should succeed");
        let token = lock.token().to_string();
        assert_eq!(lock.path(), "/file.txt");
        assert_eq!(lock.comment(), "test lock");

        // Unlock using the repos layer
        repo.fs_unlock("/file.txt", &token, false)
            .expect("fs_unlock should succeed");

        // Verify the lock is gone
        let fs = repo.fs().unwrap();
        let retrieved = fs.get_lock("/file.txt").unwrap();
        assert!(retrieved.is_none(), "Lock should have been removed");
    }

    #[test]
    fn test_repos_fs_lock_many_and_unlock_many() {
        let temp_dir = tempfile::tempdir().unwrap();
        let repo_path = temp_dir.path().join("test_repo");
        let mut repo = Repos::create(&repo_path).unwrap();

        // Create two files
        let mut txn = repo
            .begin_txn_for_commit(crate::Revnum(0), "author", "add files")
            .unwrap();
        {
            let mut root = txn.root().unwrap();
            root.make_file("/a.txt").unwrap();
            root.make_file("/b.txt").unwrap();
        }
        repo.commit_txn(txn).unwrap();

        // Set a username on the underlying filesystem (required for locking).
        // Keep the Fs alive so the access context pool stays valid.
        let mut fs = repo.fs().unwrap();
        fs.set_access("testuser").unwrap();

        // Lock both files using lock_many
        let targets = [
            ("/a.txt", None, crate::Revnum::invalid()),
            ("/b.txt", None, crate::Revnum::invalid()),
        ];
        let mut locked_paths = Vec::new();
        repo.fs_lock_many(&targets, None, false, None, false, |path, err| {
            assert!(err.is_none(), "Lock error: {:?}", err);
            locked_paths.push(path.to_string());
        })
        .expect("fs_lock_many should succeed");
        assert_eq!(locked_paths.len(), 2);

        // Retrieve tokens via fs layer
        let fs = repo.fs().unwrap();
        let token_a = fs.get_lock("/a.txt").unwrap().unwrap().token().to_string();
        let token_b = fs.get_lock("/b.txt").unwrap().unwrap().token().to_string();

        // Unlock both using unlock_many
        let unlock_targets = [("/a.txt", token_a.as_str()), ("/b.txt", token_b.as_str())];
        let mut unlocked_paths = Vec::new();
        repo.fs_unlock_many(&unlock_targets, false, |path, err| {
            assert!(err.is_none(), "Unlock error: {:?}", err);
            unlocked_paths.push(path.to_string());
        })
        .expect("fs_unlock_many should succeed");
        assert_eq!(unlocked_paths.len(), 2);
    }

    #[test]
    fn test_repos_fs_get_locks() {
        let temp_dir = tempfile::tempdir().unwrap();
        let repo_path = temp_dir.path().join("test_repo");
        let mut repo = Repos::create(&repo_path).unwrap();

        // Create a file and lock it
        let mut txn = repo
            .begin_txn_for_commit(crate::Revnum(0), "author", "add file")
            .unwrap();
        txn.root().unwrap().make_file("/locked.txt").unwrap();
        repo.commit_txn(txn).unwrap();

        // Set a username on the underlying filesystem (required for locking).
        // Keep the Fs alive so the access context pool stays valid.
        let mut fs_for_access = repo.fs().unwrap();
        fs_for_access.set_access("testuser").unwrap();

        repo.fs_lock(
            "/locked.txt",
            None,
            None,
            false,
            None,
            crate::Revnum::invalid(),
            false,
        )
        .expect("Lock should succeed");

        // Get all locks via repos layer
        let locks = repo
            .fs_get_locks("/", crate::Depth::Infinity, None)
            .expect("fs_get_locks should succeed");

        assert_eq!(locks.len(), 1, "Should have exactly one lock");
        assert_eq!(locks[0].path(), "/locked.txt");
    }
}
