use crate::{svn_result, Error, Revision, Revnum};
use std::collections::HashMap;
use std::marker::PhantomData;
use subversion_sys::svn_mergeinfo_t;

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

impl Mergeinfo {
    pub(crate) unsafe fn from_ptr_and_pool(ptr: svn_mergeinfo_t, pool: apr::Pool<'static>) -> Self {
        Self {
            ptr,
            pool,
            _phantom: PhantomData,
        }
    }

    /// Parse a mergeinfo string into a Mergeinfo struct.
    ///
    /// The input string should be in the standard mergeinfo format:
    /// `/path:rev1,rev2-rev3\n/other/path:rev4`
    pub fn parse(input: &str) -> Result<Self, Error<'static>> {
        let pool = apr::Pool::new();
        let input_cstr = std::ffi::CString::new(input)
            .map_err(|_| Error::from_message("Invalid mergeinfo string"))?;

        unsafe {
            let mut mergeinfo: svn_mergeinfo_t = std::ptr::null_mut();
            let err = subversion_sys::svn_mergeinfo_parse(
                &mut mergeinfo,
                input_cstr.as_ptr(),
                pool.as_mut_ptr(),
            );
            svn_result(err)?;
            Ok(Self::from_ptr_and_pool(mergeinfo, pool))
        }
    }

    /// Convert this mergeinfo to a string representation.
    pub fn to_string(&self) -> Result<String, Error<'static>> {
        unsafe {
            let mut output: *mut subversion_sys::svn_string_t = std::ptr::null_mut();
            let err = subversion_sys::svn_mergeinfo_to_string(
                &mut output,
                self.ptr,
                self.pool.as_mut_ptr(),
            );
            svn_result(err)?;

            if output.is_null() || (*output).data.is_null() {
                return Ok(String::new());
            }

            let data_slice = std::slice::from_raw_parts((*output).data as *const u8, (*output).len);
            Ok(std::str::from_utf8(data_slice)
                .map_err(|_| Error::from_message("Mergeinfo string is not valid UTF-8"))?
                .to_string())
        }
    }

    /// Merge another mergeinfo into this one.
    pub fn merge(&mut self, other: &Mergeinfo) -> Result<(), Error<'static>> {
        unsafe {
            let err = subversion_sys::svn_mergeinfo_merge2(
                self.ptr,
                other.ptr,
                self.pool.as_mut_ptr(),
                self.pool.as_mut_ptr(),
            );
            svn_result(err)
        }
    }

    /// Remove revisions in `eraser` from this mergeinfo.
    pub fn remove(&self, eraser: &Mergeinfo) -> Result<Mergeinfo, Error<'static>> {
        let pool = apr::Pool::new();
        unsafe {
            let mut result: svn_mergeinfo_t = std::ptr::null_mut();
            let err = subversion_sys::svn_mergeinfo_remove2(
                &mut result,
                eraser.ptr,
                self.ptr,
                1, // consider_inheritance
                pool.as_mut_ptr(),
                pool.as_mut_ptr(),
            );
            svn_result(err)?;
            Ok(Mergeinfo::from_ptr_and_pool(result, pool))
        }
    }

    /// Compute the intersection of this mergeinfo with another.
    pub fn intersect(&self, other: &Mergeinfo) -> Result<Mergeinfo, Error<'static>> {
        let pool = apr::Pool::new();
        unsafe {
            let mut result: svn_mergeinfo_t = std::ptr::null_mut();
            let err = subversion_sys::svn_mergeinfo_intersect2(
                &mut result,
                self.ptr,
                other.ptr,
                1, // consider_inheritance
                pool.as_mut_ptr(),
                pool.as_mut_ptr(),
            );
            svn_result(err)?;
            Ok(Mergeinfo::from_ptr_and_pool(result, pool))
        }
    }

    /// Compute the difference between this mergeinfo and another.
    ///
    /// Returns (deleted, added) where:
    /// - deleted: mergeinfo in `self` but not in `other`
    /// - added: mergeinfo in `other` but not in `self`
    pub fn diff(&self, other: &Mergeinfo) -> Result<(Mergeinfo, Mergeinfo), Error<'static>> {
        let pool1 = apr::Pool::new();
        let pool2 = apr::Pool::new();
        unsafe {
            let mut deleted: svn_mergeinfo_t = std::ptr::null_mut();
            let mut added: svn_mergeinfo_t = std::ptr::null_mut();
            let err = subversion_sys::svn_mergeinfo_diff2(
                &mut deleted,
                &mut added,
                self.ptr,
                other.ptr,
                1, // consider_inheritance
                pool1.as_mut_ptr(),
                pool1.as_mut_ptr(),
            );
            svn_result(err)?;
            Ok((
                Mergeinfo::from_ptr_and_pool(deleted, pool1),
                Mergeinfo::from_ptr_and_pool(added, pool2),
            ))
        }
    }

    /// Duplicate this mergeinfo.
    pub fn dup(&self) -> Mergeinfo {
        let pool = apr::Pool::new();
        unsafe {
            let result = subversion_sys::svn_mergeinfo_dup(self.ptr, pool.as_mut_ptr());
            Mergeinfo::from_ptr_and_pool(result, pool)
        }
    }

    /// Sort the mergeinfo.
    pub fn sort(&mut self) -> Result<(), Error<'static>> {
        unsafe {
            let err = subversion_sys::svn_mergeinfo_sort(self.ptr, self.pool.as_mut_ptr());
            svn_result(err)
        }
    }

    /// Get the inheritable or non-inheritable portions of mergeinfo.
    ///
    /// If `path` is Some, only examine the mergeinfo for that path.
    /// If `inheritable` is true, returns inheritable mergeinfo.
    /// If `inheritable` is false, returns non-inheritable mergeinfo.
    pub fn inheritable(
        &self,
        path: Option<&str>,
        start: Revnum,
        end: Revnum,
        inheritable: bool,
    ) -> Result<Mergeinfo, Error<'static>> {
        let pool = apr::Pool::new();
        unsafe {
            let path_cstr = path
                .map(|p| std::ffi::CString::new(p).expect("path contains null byte"))
                .map(|c| c.as_ptr())
                .unwrap_or(std::ptr::null());
            let mut result: svn_mergeinfo_t = std::ptr::null_mut();
            let err = subversion_sys::svn_mergeinfo_inheritable2(
                &mut result,
                self.ptr,
                path_cstr,
                start.0,
                end.0,
                inheritable as i32,
                pool.as_mut_ptr(),
                pool.as_mut_ptr(),
            );
            svn_result(err)?;
            Ok(Mergeinfo::from_ptr_and_pool(result, pool))
        }
    }

    /// Get the paths in this mergeinfo as a HashMap of path -> revision ranges.
    pub fn paths(&self) -> HashMap<String, Vec<crate::RevisionRange>> {
        let mut result = HashMap::new();

        if self.ptr.is_null() {
            return result;
        }

        unsafe {
            let hash =
                apr::hash::TypedHash::<subversion_sys::svn_rangelist_t>::from_ptr(self.ptr as _);

            for (key, rangelist) in hash.iter() {
                let path = std::str::from_utf8(key)
                    .expect("mergeinfo path is not valid UTF-8")
                    .to_string();

                let mut ranges = Vec::new();
                // The rangelist holds `svn_merge_range_t *` pointers.
                let array =
                    apr::tables::TypedArray::<*mut subversion_sys::svn_merge_range_t>::from_ptr(
                        rangelist as *const _ as *mut _,
                    );

                for range_ptr in array.iter() {
                    if range_ptr.is_null() {
                        continue;
                    }
                    let range = &*range_ptr;
                    ranges.push(crate::RevisionRange {
                        start: Revision::Number(Revnum(range.start)),
                        end: Revision::Number(Revnum(range.end)),
                    });
                }

                result.insert(path, ranges);
            }
        }

        result
    }

    /// Return the mergeinfo as a map of path to merge ranges, preserving the
    /// inheritable flag for each range.
    pub fn paths_with_ranges(&self) -> HashMap<String, Vec<MergeRange>> {
        let mut result = HashMap::new();

        if self.ptr.is_null() {
            return result;
        }

        unsafe {
            let hash =
                apr::hash::TypedHash::<subversion_sys::svn_rangelist_t>::from_ptr(self.ptr as _);

            for (key, rangelist) in hash.iter() {
                let path = std::str::from_utf8(key)
                    .expect("mergeinfo path is not valid UTF-8")
                    .to_string();

                let mut ranges = Vec::new();
                let array =
                    apr::tables::TypedArray::<*mut subversion_sys::svn_merge_range_t>::from_ptr(
                        rangelist as *const _ as *mut _,
                    );

                for range_ptr in array.iter() {
                    if range_ptr.is_null() {
                        continue;
                    }
                    ranges.push(MergeRange::from(&*range_ptr));
                }

                result.insert(path, ranges);
            }
        }

        result
    }
}

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

/// Specifies how merge information is inherited.
#[derive(Debug, PartialEq, Clone, Copy)]
pub enum MergeinfoInheritance {
    /// Merge information is explicit.
    Explicit,
    /// Merge information is inherited.
    Inherited,
    /// Merge information comes from the nearest ancestor.
    NearestAncestor,
}

impl From<subversion_sys::svn_mergeinfo_inheritance_t> for MergeinfoInheritance {
    fn from(value: subversion_sys::svn_mergeinfo_inheritance_t) -> Self {
        match value {
            subversion_sys::svn_mergeinfo_inheritance_t_svn_mergeinfo_nearest_ancestor => {
                MergeinfoInheritance::NearestAncestor
            }
            subversion_sys::svn_mergeinfo_inheritance_t_svn_mergeinfo_explicit => {
                MergeinfoInheritance::Explicit
            }
            subversion_sys::svn_mergeinfo_inheritance_t_svn_mergeinfo_inherited => {
                MergeinfoInheritance::Inherited
            }
            _ => unreachable!(),
        }
    }
}

impl From<MergeinfoInheritance> for subversion_sys::svn_mergeinfo_inheritance_t {
    fn from(value: MergeinfoInheritance) -> Self {
        match value {
            MergeinfoInheritance::NearestAncestor => {
                subversion_sys::svn_mergeinfo_inheritance_t_svn_mergeinfo_nearest_ancestor
            }
            MergeinfoInheritance::Explicit => {
                subversion_sys::svn_mergeinfo_inheritance_t_svn_mergeinfo_explicit
            }
            MergeinfoInheritance::Inherited => {
                subversion_sys::svn_mergeinfo_inheritance_t_svn_mergeinfo_inherited
            }
        }
    }
}

impl MergeinfoInheritance {
    /// Convert to the canonical string representation.
    pub fn to_word(self) -> &'static str {
        unsafe {
            let ptr = subversion_sys::svn_inheritance_to_word(self.into());
            std::ffi::CStr::from_ptr(ptr)
                .to_str()
                .expect("inheritance word is not valid UTF-8")
        }
    }

    /// Parse from a string representation.
    pub fn from_word(word: &str) -> Self {
        let word_cstr = std::ffi::CString::new(word).expect("inheritance word contains null byte");
        unsafe { subversion_sys::svn_inheritance_from_word(word_cstr.as_ptr()).into() }
    }
}

/// A merge range with start/end revisions and inheritability.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct MergeRange {
    /// Start revision (exclusive)
    pub start: Revnum,
    /// End revision (inclusive)
    pub end: Revnum,
    /// Whether this range is inheritable
    pub inheritable: bool,
}

impl MergeRange {
    /// Create a new merge range.
    pub fn new(start: Revnum, end: Revnum, inheritable: bool) -> Self {
        Self {
            start,
            end,
            inheritable,
        }
    }
}

impl From<&subversion_sys::svn_merge_range_t> for MergeRange {
    fn from(r: &subversion_sys::svn_merge_range_t) -> Self {
        Self {
            start: Revnum(r.start),
            end: Revnum(r.end),
            inheritable: r.inheritable != 0,
        }
    }
}

impl From<subversion_sys::svn_merge_range_t> for MergeRange {
    fn from(r: subversion_sys::svn_merge_range_t) -> Self {
        Self {
            start: Revnum(r.start),
            end: Revnum(r.end),
            inheritable: r.inheritable != 0,
        }
    }
}

/// A list of merge ranges.
pub struct Rangelist {
    ptr: *mut subversion_sys::svn_rangelist_t,
    pool: apr::Pool<'static>,
}

impl Rangelist {
    /// Create a new empty rangelist.
    pub fn new() -> Self {
        let pool = apr::Pool::new();
        // An svn_rangelist_t is an apr_array_header_t of `svn_merge_range_t *`
        // pointers, so each element is pointer-sized, not the struct itself.
        let ptr = unsafe {
            apr_sys::apr_array_make(
                pool.as_mut_ptr(),
                0,
                std::mem::size_of::<*mut subversion_sys::svn_merge_range_t>() as i32,
            )
        };
        Self { ptr, pool }
    }

    /// Create a rangelist from a raw pointer and pool.
    ///
    /// # Safety
    /// The pointer must be valid and the pool must own the memory.
    pub(crate) unsafe fn from_ptr_and_pool(
        ptr: *mut subversion_sys::svn_rangelist_t,
        pool: apr::Pool<'static>,
    ) -> Self {
        Self { ptr, pool }
    }

    /// Convert the rangelist to a string representation.
    pub fn to_string(&self) -> Result<String, Error<'static>> {
        unsafe {
            let mut output: *mut subversion_sys::svn_string_t = std::ptr::null_mut();
            let err = subversion_sys::svn_rangelist_to_string(
                &mut output,
                self.ptr,
                self.pool.as_mut_ptr(),
            );
            svn_result(err)?;

            if output.is_null() || (*output).data.is_null() {
                return Ok(String::new());
            }

            let data_slice = std::slice::from_raw_parts((*output).data as *const u8, (*output).len);
            Ok(std::str::from_utf8(data_slice)
                .map_err(|_| Error::from_message("Rangelist string is not valid UTF-8"))?
                .to_string())
        }
    }

    /// Compute the difference between two rangelists.
    ///
    /// Returns (deleted, added) where:
    /// - deleted: ranges in `from` but not in `to`
    /// - added: ranges in `to` but not in `from`
    pub fn diff(
        from: &Rangelist,
        to: &Rangelist,
        consider_inheritance: bool,
    ) -> Result<(Rangelist, Rangelist), Error<'static>> {
        let pool1 = apr::Pool::new();
        let pool2 = apr::Pool::new();
        unsafe {
            let mut deleted: *mut subversion_sys::svn_rangelist_t = std::ptr::null_mut();
            let mut added: *mut subversion_sys::svn_rangelist_t = std::ptr::null_mut();
            let err = subversion_sys::svn_rangelist_diff(
                &mut deleted,
                &mut added,
                from.ptr,
                to.ptr,
                consider_inheritance.into(),
                pool1.as_mut_ptr(),
            );
            svn_result(err)?;
            Ok((
                Rangelist::from_ptr_and_pool(deleted, pool1),
                Rangelist::from_ptr_and_pool(added, pool2),
            ))
        }
    }

    /// Merge changes into this rangelist.
    pub fn merge(&mut self, changes: &Rangelist) -> Result<(), Error<'static>> {
        unsafe {
            let err = subversion_sys::svn_rangelist_merge2(
                self.ptr,
                changes.ptr,
                self.pool.as_mut_ptr(),
                self.pool.as_mut_ptr(),
            );
            svn_result(err)
        }
    }

    /// Remove ranges in `eraser` from `whiteboard`.
    pub fn remove(
        eraser: &Rangelist,
        whiteboard: &Rangelist,
        consider_inheritance: bool,
    ) -> Result<Rangelist, Error<'static>> {
        let pool = apr::Pool::new();
        unsafe {
            let mut output: *mut subversion_sys::svn_rangelist_t = std::ptr::null_mut();
            let err = subversion_sys::svn_rangelist_remove(
                &mut output,
                eraser.ptr,
                whiteboard.ptr,
                consider_inheritance.into(),
                pool.as_mut_ptr(),
            );
            svn_result(err)?;
            Ok(Rangelist::from_ptr_and_pool(output, pool))
        }
    }

    /// Compute the intersection of two rangelists.
    pub fn intersect(
        rangelist1: &Rangelist,
        rangelist2: &Rangelist,
        consider_inheritance: bool,
    ) -> Result<Rangelist, Error<'static>> {
        let pool = apr::Pool::new();
        unsafe {
            let mut output: *mut subversion_sys::svn_rangelist_t = std::ptr::null_mut();
            let err = subversion_sys::svn_rangelist_intersect(
                &mut output,
                rangelist1.ptr,
                rangelist2.ptr,
                consider_inheritance.into(),
                pool.as_mut_ptr(),
            );
            svn_result(err)?;
            Ok(Rangelist::from_ptr_and_pool(output, pool))
        }
    }

    /// Reverse this rangelist in place.
    pub fn reverse(&mut self) -> Result<(), Error<'static>> {
        unsafe {
            let err = subversion_sys::svn_rangelist_reverse(self.ptr, self.pool.as_mut_ptr());
            svn_result(err)
        }
    }

    /// Duplicate this rangelist.
    pub fn dup(&self) -> Rangelist {
        let pool = apr::Pool::new();
        unsafe {
            let result = subversion_sys::svn_rangelist_dup(self.ptr, pool.as_mut_ptr());
            Rangelist::from_ptr_and_pool(result, pool)
        }
    }

    /// Get the ranges as a Vec.
    pub fn ranges(&self) -> Vec<MergeRange> {
        if self.ptr.is_null() {
            return Vec::new();
        }

        unsafe {
            // The array holds `svn_merge_range_t *` pointers, so read each
            // element as a pointer and dereference it.
            let array = apr::tables::TypedArray::<*mut subversion_sys::svn_merge_range_t>::from_ptr(
                self.ptr,
            );
            array
                .iter()
                .filter(|p| !p.is_null())
                .map(|p| MergeRange::from(&*p))
                .collect()
        }
    }

    /// Check if the rangelist is empty.
    pub fn is_empty(&self) -> bool {
        if self.ptr.is_null() {
            return true;
        }
        unsafe { (*self.ptr).nelts == 0 }
    }

    /// Get the number of ranges.
    pub fn len(&self) -> usize {
        if self.ptr.is_null() {
            return 0;
        }
        unsafe { (*self.ptr).nelts as usize }
    }

    /// Get the inheritable or non-inheritable ranges within a revision range.
    ///
    /// If `inheritable` is true, returns ranges that are inheritable.
    /// If `inheritable` is false, returns ranges that are non-inheritable.
    pub fn inheritable(
        &self,
        start: Revnum,
        end: Revnum,
        inheritable: bool,
    ) -> Result<Rangelist, Error<'static>> {
        let pool = apr::Pool::new();
        unsafe {
            let mut result: *mut subversion_sys::svn_rangelist_t = std::ptr::null_mut();
            let err = subversion_sys::svn_rangelist_inheritable2(
                &mut result,
                self.ptr,
                start.0,
                end.0,
                inheritable as i32,
                pool.as_mut_ptr(),
                pool.as_mut_ptr(),
            );
            svn_result(err)?;
            Ok(Rangelist::from_ptr_and_pool(result, pool))
        }
    }
}

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

/// A catalog of mergeinfo, mapping paths to their mergeinfo.
///
/// This is essentially a hash table where keys are repository paths
/// and values are Mergeinfo objects.
pub struct MergeinfoCatalog {
    ptr: subversion_sys::svn_mergeinfo_catalog_t,
    pool: apr::Pool<'static>,
}

impl MergeinfoCatalog {
    /// Create a new empty mergeinfo catalog.
    pub fn new() -> Self {
        let pool = apr::Pool::new();
        let ptr = unsafe { apr_sys::apr_hash_make(pool.as_mut_ptr()) };
        Self { ptr, pool }
    }

    /// Create a catalog from a raw pointer and pool.
    ///
    /// # Safety
    /// The pointer must be valid and the pool must own the memory.
    pub(crate) unsafe fn from_ptr_and_pool(
        ptr: subversion_sys::svn_mergeinfo_catalog_t,
        pool: apr::Pool<'static>,
    ) -> Self {
        Self { ptr, pool }
    }

    /// Merge changes from another catalog into this one.
    pub fn merge(&mut self, changes: &MergeinfoCatalog) -> Result<(), Error<'static>> {
        unsafe {
            let err = subversion_sys::svn_mergeinfo_catalog_merge(
                self.ptr,
                changes.ptr,
                self.pool.as_mut_ptr(),
                self.pool.as_mut_ptr(),
            );
            svn_result(err)
        }
    }

    /// Duplicate this catalog.
    pub fn dup(&self) -> MergeinfoCatalog {
        let pool = apr::Pool::new();
        unsafe {
            let result = subversion_sys::svn_mergeinfo_catalog_dup(self.ptr, pool.as_mut_ptr());
            MergeinfoCatalog::from_ptr_and_pool(result, pool)
        }
    }

    /// Check if the catalog is empty.
    pub fn is_empty(&self) -> bool {
        if self.ptr.is_null() {
            return true;
        }
        unsafe { apr_sys::apr_hash_count(self.ptr) == 0 }
    }

    /// Get the number of entries in the catalog.
    pub fn len(&self) -> usize {
        if self.ptr.is_null() {
            return 0;
        }
        unsafe { apr_sys::apr_hash_count(self.ptr) as usize }
    }

    /// Get the paths in this catalog.
    pub fn paths(&self) -> Vec<String> {
        if self.ptr.is_null() {
            return Vec::new();
        }

        unsafe {
            let hash = apr::hash::TypedHash::<svn_mergeinfo_t>::from_ptr(self.ptr);
            hash.iter()
                .map(|(key, _)| {
                    std::str::from_utf8(key)
                        .expect("catalog path is not valid UTF-8")
                        .to_string()
                })
                .collect()
        }
    }
}

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

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

    #[test]
    fn test_mergeinfo_parse() {
        let mergeinfo = Mergeinfo::parse("/trunk:1-10").unwrap();
        let s = mergeinfo.to_string().unwrap();
        assert_eq!(s, "/trunk:1-10");
    }

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

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

    #[test]
    fn test_mergeinfo_roundtrip_conversion() {
        // Test that converting back and forth preserves values
        let variants = vec![
            MergeinfoInheritance::Explicit,
            MergeinfoInheritance::Inherited,
            MergeinfoInheritance::NearestAncestor,
        ];

        for variant in variants {
            let svn_val = subversion_sys::svn_mergeinfo_inheritance_t::from(variant);
            let back = MergeinfoInheritance::from(svn_val);
            assert_eq!(variant, back);
        }
    }

    #[test]
    fn test_mergeinfo_drop() {
        // Test that Mergeinfo can be dropped without panic
        {
            let _mergeinfo = Mergeinfo::parse("/trunk:1-5").unwrap();
            // mergeinfo is dropped here
        }
        // No panic should occur
    }

    #[test]
    fn test_mergeinfo_dup() {
        let mergeinfo = Mergeinfo::parse("/trunk:1-10").unwrap();
        let dup = mergeinfo.dup();
        assert_eq!(dup.to_string().unwrap(), "/trunk:1-10");
    }

    #[test]
    fn test_rangelist_new() {
        let rangelist = Rangelist::new();
        assert!(rangelist.is_empty());
        assert_eq!(rangelist.len(), 0);
    }

    #[test]
    fn test_merge_range_from() {
        let range = MergeRange {
            start: Revnum(1),
            end: Revnum(10),
            inheritable: true,
        };
        assert_eq!(range.start.0, 1);
        assert_eq!(range.end.0, 10);
        assert!(range.inheritable);
    }

    #[test]
    fn test_rangelist_dup() {
        let rangelist = Rangelist::new();
        let dup = rangelist.dup();
        assert!(dup.is_empty());
    }

    #[test]
    fn test_rangelist_to_string_empty() {
        let rangelist = Rangelist::new();
        let s = rangelist.to_string().unwrap();
        assert_eq!(s, "");
    }

    #[test]
    fn test_rangelist_merge() {
        let mut r1 = Rangelist::new();
        let r2 = Rangelist::new();
        r1.merge(&r2).unwrap();
        assert!(r1.is_empty());
    }

    #[test]
    fn test_rangelist_intersect() {
        let r1 = Rangelist::new();
        let r2 = Rangelist::new();
        let intersected = Rangelist::intersect(&r1, &r2, true).unwrap();
        assert!(intersected.is_empty());
    }

    #[test]
    fn test_rangelist_reverse() {
        let mut rangelist = Rangelist::new();
        rangelist.reverse().unwrap();
        assert!(rangelist.is_empty());
    }

    #[test]
    fn test_rangelist_ranges_empty() {
        let rangelist = Rangelist::new();
        let ranges = rangelist.ranges();
        assert!(ranges.is_empty());
    }

    #[test]
    fn test_mergeinfo_catalog_new() {
        let catalog = MergeinfoCatalog::new();
        assert!(catalog.is_empty());
        assert_eq!(catalog.len(), 0);
    }

    #[test]
    fn test_mergeinfo_catalog_dup() {
        let catalog = MergeinfoCatalog::new();
        let dup = catalog.dup();
        assert!(dup.is_empty());
    }

    #[test]
    fn test_mergeinfo_catalog_merge() {
        let mut c1 = MergeinfoCatalog::new();
        let c2 = MergeinfoCatalog::new();
        c1.merge(&c2).unwrap();
        assert!(c1.is_empty());
    }

    #[test]
    fn test_mergeinfo_catalog_paths() {
        let catalog = MergeinfoCatalog::new();
        let paths = catalog.paths();
        assert!(paths.is_empty());
    }

    /// Build a catalog containing one entry keyed by `path`, holding the
    /// mergeinfo parsed from `mergeinfo`. The mergeinfo's pool is leaked into
    /// the catalog's pool ownership via insertion, which is fine for tests.
    fn populated_catalog(path: &str, mergeinfo: &str) -> MergeinfoCatalog {
        let catalog = MergeinfoCatalog::new();
        let mi = Mergeinfo::parse(mergeinfo).unwrap();
        unsafe {
            let key = std::ffi::CString::new(path).unwrap();
            let key_ptr = apr_sys::apr_pstrdup(catalog.pool.as_mut_ptr(), key.as_ptr());
            // Duplicate the mergeinfo into the catalog pool so it outlives `mi`.
            let mi_dup = subversion_sys::svn_mergeinfo_dup(mi.ptr, catalog.pool.as_mut_ptr());
            apr_sys::apr_hash_set(
                catalog.ptr,
                key_ptr as *const std::ffi::c_void,
                apr_sys::APR_HASH_KEY_STRING as apr_sys::apr_ssize_t,
                mi_dup as *const std::ffi::c_void,
            );
        }
        catalog
    }

    #[test]
    fn test_mergeinfo_catalog_populated_len_and_paths() {
        let catalog = populated_catalog("/trunk", "/branch:1-3");
        assert!(!catalog.is_empty());
        assert_eq!(catalog.len(), 1);
        assert_eq!(catalog.paths(), vec!["/trunk".to_string()]);
    }

    #[test]
    fn test_mergeinfo_catalog_dup_populated() {
        let catalog = populated_catalog("/trunk", "/branch:1-3");
        let dup = catalog.dup();
        assert_eq!(dup.len(), 1);
        assert_eq!(dup.paths(), vec!["/trunk".to_string()]);
    }

    #[test]
    fn test_inheritance_to_word() {
        assert_eq!(MergeinfoInheritance::Explicit.to_word(), "explicit");
        assert_eq!(MergeinfoInheritance::Inherited.to_word(), "inherited");
        assert_eq!(
            MergeinfoInheritance::NearestAncestor.to_word(),
            "nearest-ancestor"
        );
    }

    #[test]
    fn test_inheritance_from_word() {
        assert_eq!(
            MergeinfoInheritance::from_word("explicit"),
            MergeinfoInheritance::Explicit
        );
        assert_eq!(
            MergeinfoInheritance::from_word("inherited"),
            MergeinfoInheritance::Inherited
        );
    }

    #[test]
    fn test_merge_range_from_ref_inheritable() {
        let raw = subversion_sys::svn_merge_range_t {
            start: 1,
            end: 10,
            inheritable: 1,
        };
        let range = MergeRange::from(&raw);
        assert_eq!(range.start, Revnum(1));
        assert_eq!(range.end, Revnum(10));
        assert!(range.inheritable);

        let raw_non = subversion_sys::svn_merge_range_t {
            start: 2,
            end: 5,
            inheritable: 0,
        };
        assert!(!MergeRange::from(&raw_non).inheritable);
    }

    #[test]
    fn test_merge_range_from_owned_inheritable() {
        let raw = subversion_sys::svn_merge_range_t {
            start: 1,
            end: 10,
            inheritable: 1,
        };
        assert!(MergeRange::from(raw).inheritable);

        let raw_non = subversion_sys::svn_merge_range_t {
            start: 1,
            end: 10,
            inheritable: 0,
        };
        assert!(!MergeRange::from(raw_non).inheritable);
    }

    #[test]
    fn test_mergeinfo_merge_combines() {
        let mut a = Mergeinfo::parse("/trunk:1-3").unwrap();
        let b = Mergeinfo::parse("/trunk:5-7\n/branch:2").unwrap();
        a.merge(&b).unwrap();
        assert_eq!(a.to_string().unwrap(), "/branch:2\n/trunk:1-3,5-7");
    }

    #[test]
    fn test_mergeinfo_paths_content() {
        let mergeinfo = Mergeinfo::parse("/trunk:1-3\n/branch:5").unwrap();
        let paths = mergeinfo.paths();
        assert_eq!(paths.len(), 2);
        assert!(paths.contains_key("/trunk"));
        assert!(paths.contains_key("/branch"));
    }

    #[test]
    fn test_mergeinfo_paths_ranges() {
        // Regression test: an svn_rangelist_t is an array of svn_merge_range_t
        // POINTERS, not inline structs. Reading it inline returned garbage
        // revision numbers; these assertions pin the real values. SVN stores
        // the range start as exclusive, so "1-3" is the half-open range 0..3.
        let mergeinfo = Mergeinfo::parse("/trunk:1-3\n/branch:5,7-9").unwrap();
        let paths = mergeinfo.paths();

        // Extract (start, end) revision numbers from a path's ranges.
        let nums = |ranges: &[crate::RevisionRange]| -> Vec<(i64, i64)> {
            ranges
                .iter()
                .map(|r| match (r.start, r.end) {
                    // svn_revnum_t is a C long (i32 on Windows, i64 elsewhere),
                    // so cast to i64 to keep these assertions portable.
                    (Revision::Number(s), Revision::Number(e)) => (s.0 as i64, e.0 as i64),
                    other => panic!("expected numeric ranges, got {:?}", other),
                })
                .collect()
        };

        assert_eq!(nums(&paths["/trunk"]), vec![(0, 3)]);
        assert_eq!(nums(&paths["/branch"]), vec![(4, 5), (6, 9)]);
    }

    /// Build a Rangelist populated with the given (start, end, inheritable)
    /// ranges. An svn_rangelist_t is an array of `svn_merge_range_t *`
    /// pointers, so each range is allocated in the pool and a pointer to it is
    /// pushed onto the array.
    fn populated_rangelist(ranges: &[(i64, i64, bool)]) -> Rangelist {
        let pool = apr::Pool::new();
        unsafe {
            let ptr = apr_sys::apr_array_make(
                pool.as_mut_ptr(),
                ranges.len() as i32,
                std::mem::size_of::<*mut subversion_sys::svn_merge_range_t>() as i32,
            );
            for &(start, end, inheritable) in ranges {
                let range: *mut subversion_sys::svn_merge_range_t = pool.calloc();
                (*range).start = start as subversion_sys::svn_revnum_t;
                (*range).end = end as subversion_sys::svn_revnum_t;
                (*range).inheritable = inheritable as subversion_sys::svn_boolean_t;
                let slot =
                    apr_sys::apr_array_push(ptr) as *mut *mut subversion_sys::svn_merge_range_t;
                *slot = range;
            }
            Rangelist::from_ptr_and_pool(ptr as *mut subversion_sys::svn_rangelist_t, pool)
        }
    }

    #[test]
    fn test_rangelist_populated_ranges() {
        let rl = populated_rangelist(&[(0, 5, true), (10, 15, false)]);
        assert!(!rl.is_empty());
        assert_eq!(rl.len(), 2);

        let ranges = rl.ranges();
        assert_eq!(ranges.len(), 2);
        assert_eq!(ranges[0], MergeRange::new(Revnum(0), Revnum(5), true));
        assert_eq!(ranges[1], MergeRange::new(Revnum(10), Revnum(15), false));
    }

    #[test]
    fn test_rangelist_to_string_populated() {
        // Range 0..5 renders as "1-5" in mergeinfo notation (exclusive start).
        let rl = populated_rangelist(&[(0, 5, true)]);
        assert_eq!(rl.to_string().unwrap(), "1-5");
    }

    #[test]
    fn test_rangelist_dup_populated() {
        let rl = populated_rangelist(&[(0, 5, true), (10, 15, true)]);
        let dup = rl.dup();
        assert_eq!(dup.len(), 2);
        assert_eq!(dup.to_string().unwrap(), rl.to_string().unwrap());
    }

    #[test]
    fn test_rangelist_merge_populated() {
        let mut a = populated_rangelist(&[(0, 5, true)]);
        let b = populated_rangelist(&[(5, 10, true)]);
        a.merge(&b).unwrap();
        // Adjacent ranges combine into 1-10.
        assert_eq!(a.to_string().unwrap(), "1-10");
    }

    #[test]
    fn test_rangelist_remove_populated() {
        let whiteboard = populated_rangelist(&[(0, 10, true)]);
        let eraser = populated_rangelist(&[(2, 5, true)]);
        let result = Rangelist::remove(&eraser, &whiteboard, true).unwrap();
        // Removing revs (2,5] from (0,10] leaves (0,2] and (5,10].
        assert_eq!(result.to_string().unwrap(), "1-2,6-10");
    }

    #[test]
    fn test_rangelist_intersect_populated() {
        let a = populated_rangelist(&[(0, 10, true)]);
        let b = populated_rangelist(&[(5, 15, true)]);
        let result = Rangelist::intersect(&a, &b, true).unwrap();
        assert_eq!(result.to_string().unwrap(), "6-10");
    }

    #[test]
    fn test_rangelist_diff_populated() {
        let from = populated_rangelist(&[(0, 5, true)]);
        let to = populated_rangelist(&[(0, 10, true)]);
        let (deleted, added) = Rangelist::diff(&from, &to, true).unwrap();
        assert!(deleted.is_empty());
        assert_eq!(added.to_string().unwrap(), "6-10");
    }

    #[test]
    fn test_rangelist_reverse_populated() {
        let mut rl = populated_rangelist(&[(0, 5, true), (10, 15, true)]);
        rl.reverse().unwrap();
        // reverse() flips the array order and swaps each range's start/end.
        let ranges = rl.ranges();
        assert_eq!(ranges[0], MergeRange::new(Revnum(15), Revnum(10), true));
        assert_eq!(ranges[1], MergeRange::new(Revnum(5), Revnum(0), true));
    }
}
