//! Keyword substitution and EOL translation
//!
//! This module provides safe Rust wrappers around Subversion's keyword and
//! end-of-line substitution functionality.

use crate::{with_tmp_pool, Error};
use std::collections::HashMap;

/// EOL style for text files
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum EolStyle {
    /// Unrecognized style
    Unknown,
    /// No EOL translation
    None,
    /// Use the native EOL style for the platform
    Native,
    /// Use a fixed EOL style (LF, CR, or CRLF)
    Fixed,
}

impl From<subversion_sys::svn_subst_eol_style_t> for EolStyle {
    fn from(style: subversion_sys::svn_subst_eol_style_t) -> Self {
        match style {
            subversion_sys::svn_subst_eol_style_svn_subst_eol_style_unknown => EolStyle::Unknown,
            subversion_sys::svn_subst_eol_style_svn_subst_eol_style_none => EolStyle::None,
            subversion_sys::svn_subst_eol_style_svn_subst_eol_style_native => EolStyle::Native,
            subversion_sys::svn_subst_eol_style_svn_subst_eol_style_fixed => EolStyle::Fixed,
            _ => unreachable!("unknown svn_subst_eol_style_t value: {}", style),
        }
    }
}

impl From<EolStyle> for subversion_sys::svn_subst_eol_style_t {
    fn from(style: EolStyle) -> Self {
        match style {
            EolStyle::Unknown => subversion_sys::svn_subst_eol_style_svn_subst_eol_style_unknown,
            EolStyle::None => subversion_sys::svn_subst_eol_style_svn_subst_eol_style_none,
            EolStyle::Native => subversion_sys::svn_subst_eol_style_svn_subst_eol_style_native,
            EolStyle::Fixed => subversion_sys::svn_subst_eol_style_svn_subst_eol_style_fixed,
        }
    }
}

/// Parse an EOL style value from a property string
///
/// Returns the EOL style and the corresponding EOL string.
/// The EOL string is None for EolStyle::None, the platform native
/// EOL for EolStyle::Native, or the specific EOL for EolStyle::Fixed.
pub fn eol_style_from_value(value: &str) -> (EolStyle, Option<String>) {
    let value_cstr = std::ffi::CString::new(value).unwrap();

    unsafe {
        let mut style: subversion_sys::svn_subst_eol_style_t =
            subversion_sys::svn_subst_eol_style_svn_subst_eol_style_unknown;
        let mut eol_ptr: *const std::ffi::c_char = std::ptr::null();

        subversion_sys::svn_subst_eol_style_from_value(
            &mut style,
            &mut eol_ptr,
            value_cstr.as_ptr(),
        );

        let eol_str = if eol_ptr.is_null() {
            None
        } else {
            Some(
                std::ffi::CStr::from_ptr(eol_ptr)
                    .to_str()
                    .unwrap()
                    .to_string(),
            )
        };

        (EolStyle::from(style), eol_str)
    }
}

/// Check if translation is required for the given parameters
///
/// Returns true if the working copy and normalized versions of a file
/// with the given parameters differ.
pub fn translation_required(
    style: EolStyle,
    eol: Option<&str>,
    keywords: Option<&HashMap<String, String>>,
    special: bool,
    force_eol_check: bool,
) -> bool {
    with_tmp_pool(|pool| {
        let eol_cstr = eol.map(|s| std::ffi::CString::new(s).unwrap());
        let eol_ptr = eol_cstr
            .as_ref()
            .map(|c| c.as_ptr())
            .unwrap_or(std::ptr::null());

        let keywords_hash = match keywords {
            Some(kw) => {
                let mut hash = apr::hash::Hash::new(pool);
                for (k, v) in kw.iter() {
                    unsafe {
                        // Copy key into pool to ensure it outlives the hash
                        let k_pooled = apr::strings::pstrdup_raw(k, pool).unwrap();
                        // Create svn_string_t for the value (SVN expects svn_string_t*, not const char*)
                        let v_cstr = std::ffi::CString::new(v.as_str()).unwrap();
                        let svn_str =
                            subversion_sys::svn_string_create(v_cstr.as_ptr(), pool.as_mut_ptr());
                        // Use the pooled key which is null-terminated
                        let key_slice =
                            std::slice::from_raw_parts(k_pooled as *const u8, k.len() + 1);
                        hash.insert(key_slice, svn_str as *mut std::ffi::c_void);
                    }
                }
                unsafe { hash.as_mut_ptr() }
            }
            None => std::ptr::null_mut(),
        };

        unsafe {
            subversion_sys::svn_subst_translation_required(
                style.into(),
                eol_ptr,
                keywords_hash,
                if special { 1 } else { 0 },
                if force_eol_check { 1 } else { 0 },
            ) != 0
        }
    })
}

/// Build keyword hash from keyword string and file information
///
/// Creates a keyword hash suitable for use with translation functions.
/// The keywords_string should be the value of the svn:keywords property.
pub fn build_keywords(
    keywords_string: &str,
    rev: Option<&str>,
    url: Option<&str>,
    date: Option<apr::time::Time>,
    author: Option<&str>,
    repos_root_url: Option<&str>,
) -> Result<HashMap<String, String>, Error<'static>> {
    with_tmp_pool(|pool| {
        let keywords_cstr = std::ffi::CString::new(keywords_string).unwrap();
        let rev_cstr = rev.map(|s| std::ffi::CString::new(s).unwrap());
        let url_cstr = url.map(|s| std::ffi::CString::new(s).unwrap());
        let author_cstr = author.map(|s| std::ffi::CString::new(s).unwrap());
        let repos_root_cstr = repos_root_url.map(|s| std::ffi::CString::new(s).unwrap());

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

        unsafe {
            let err = subversion_sys::svn_subst_build_keywords3(
                &mut keywords_hash,
                keywords_cstr.as_ptr(),
                rev_cstr
                    .as_ref()
                    .map(|c| c.as_ptr())
                    .unwrap_or(std::ptr::null()),
                url_cstr
                    .as_ref()
                    .map(|c| c.as_ptr())
                    .unwrap_or(std::ptr::null()),
                repos_root_cstr
                    .as_ref()
                    .map(|c| c.as_ptr())
                    .unwrap_or(std::ptr::null()),
                date.map(|d| d.into()).unwrap_or(0),
                author_cstr
                    .as_ref()
                    .map(|c| c.as_ptr())
                    .unwrap_or(std::ptr::null()),
                pool.as_mut_ptr(),
            );
            Error::from_raw(err)?;
        }

        if keywords_hash.is_null() {
            return Ok(HashMap::new());
        }

        let prop_hash = unsafe { crate::props::PropHash::from_ptr(keywords_hash) };
        Ok(prop_hash.to_string_hashmap())
    })
}

/// Check if two keyword hashes differ
///
/// If compare_values is true, both the keys and values must match.
/// If false, only the keys need to match (values can differ).
pub fn keywords_differ(
    a: Option<&HashMap<String, String>>,
    b: Option<&HashMap<String, String>>,
    compare_values: bool,
) -> Result<bool, Error<'static>> {
    with_tmp_pool(|pool| {
        // Keep c_strings alive until after the FFI call
        // Store key AND CString value together to ensure iteration order matches
        // Sort by key to ensure deterministic order regardless of HashMap iteration
        let mut c_strings_a: Vec<_> = match a {
            Some(kw) => kw
                .iter()
                .map(|(k, v)| (k.as_str(), std::ffi::CString::new(v.as_str()).unwrap()))
                .collect(),
            None => Vec::new(),
        };
        c_strings_a.sort_by(|a, b| a.0.cmp(b.0));

        let hash_a = match a {
            Some(_kw) => {
                let mut hash = apr::hash::Hash::new(pool);
                // Use c_strings_a directly instead of re-iterating over HashMap
                // to ensure consistent iteration order
                for (k, v_cstr) in c_strings_a.iter() {
                    unsafe {
                        // Create svn_string_t for the value (SVN expects svn_string_t*, not const char*)
                        let svn_str =
                            subversion_sys::svn_string_create(v_cstr.as_ptr(), pool.as_mut_ptr());
                        hash.insert(k.as_bytes(), svn_str as *mut std::ffi::c_void);
                    }
                }
                unsafe { hash.as_mut_ptr() }
            }
            None => std::ptr::null_mut(),
        };

        // Keep c_strings alive until after the FFI call
        // Store key AND CString value together to ensure iteration order matches
        // Sort by key to ensure deterministic order regardless of HashMap iteration
        let mut c_strings_b: Vec<_> = match b {
            Some(kw) => kw
                .iter()
                .map(|(k, v)| (k.as_str(), std::ffi::CString::new(v.as_str()).unwrap()))
                .collect(),
            None => Vec::new(),
        };
        c_strings_b.sort_by(|a, b| a.0.cmp(b.0));

        let hash_b = match b {
            Some(_kw) => {
                let mut hash = apr::hash::Hash::new(pool);
                // Use c_strings_b directly instead of re-iterating over HashMap
                // to ensure consistent iteration order
                for (k, v_cstr) in c_strings_b.iter() {
                    unsafe {
                        // Create svn_string_t for the value (SVN expects svn_string_t*, not const char*)
                        let svn_str =
                            subversion_sys::svn_string_create(v_cstr.as_ptr(), pool.as_mut_ptr());
                        hash.insert(k.as_bytes(), svn_str as *mut std::ffi::c_void);
                    }
                }
                unsafe { hash.as_mut_ptr() }
            }
            None => std::ptr::null_mut(),
        };

        unsafe {
            let result = subversion_sys::svn_subst_keywords_differ2(
                hash_a,
                hash_b,
                if compare_values { 1 } else { 0 },
                pool.as_mut_ptr(),
            );
            Ok(result != 0)
        }
    })
}

/// Create a translated stream for keyword and EOL substitution
///
/// Returns a stream that performs EOL translation and keyword expansion/contraction
/// when data is read from or written to it.
pub fn stream_translated(
    mut source_stream: crate::io::Stream,
    eol_str: Option<&str>,
    repair: bool,
    keywords: Option<&HashMap<String, String>>,
    expand: bool,
) -> Result<crate::io::Stream, Error<'static>> {
    // Create result_pool OUTSIDE with_tmp_pool so it survives
    let result_pool = apr::Pool::new();

    // Copy eol_str into result_pool so it survives
    let eol_ptr = match eol_str {
        Some(s) => apr::strings::pstrdup_raw(s, &result_pool)?,
        None => std::ptr::null(),
    };

    // Allocate keywords_hash in result_pool, not temporary pool
    let keywords_hash = match keywords {
        Some(kw) => {
            // Create C strings and copy them into the result_pool using apr::strings::pstrdup
            let mut hash = apr::hash::Hash::new(&result_pool);
            for (k, v) in kw.iter() {
                unsafe {
                    // Copy both key and value into result_pool
                    let key_ptr = apr::strings::pstrdup_raw(k, &result_pool)?;
                    let val_ptr = apr::strings::pstrdup_raw(v, &result_pool)?;
                    hash.insert(
                        std::slice::from_raw_parts(key_ptr as *const u8, k.len()),
                        val_ptr as *mut std::ffi::c_void,
                    );
                }
            }
            unsafe { hash.as_mut_ptr() }
        }
        None => std::ptr::null_mut(),
    };

    unsafe {
        let translated_stream = subversion_sys::svn_subst_stream_translated(
            source_stream.as_mut_ptr(),
            eol_ptr,
            if repair { 1 } else { 0 },
            keywords_hash,
            if expand { 1 } else { 0 },
            result_pool.as_mut_ptr(),
        );

        Ok(crate::io::Stream::from_ptr(translated_stream, result_pool))
    }
}

/// Create a translated stream for converting to normal form
///
/// This creates a stream that translates content to the "normal form"
/// used internally by Subversion (LF line endings, expanded keywords).
pub fn stream_translated_to_normal_form(
    mut source_stream: crate::io::Stream,
    eol_style: EolStyle,
    eol_str: Option<&str>,
    always_repair_eols: bool,
    keywords: Option<&HashMap<String, String>>,
) -> Result<crate::io::Stream, Error<'static>> {
    // Create result_pool OUTSIDE with_tmp_pool so it survives
    let result_pool = apr::Pool::new();

    // Copy eol_str into result_pool so it survives
    let eol_ptr = match eol_str {
        Some(s) => apr::strings::pstrdup_raw(s, &result_pool)?,
        None => std::ptr::null(),
    };

    // Allocate keywords_hash in result_pool, not temporary pool
    let keywords_hash = match keywords {
        Some(kw) => {
            // Create C strings and copy them into the result_pool using apr::strings::pstrdup
            let mut hash = apr::hash::Hash::new(&result_pool);
            for (k, v) in kw.iter() {
                unsafe {
                    // Copy both key and value into result_pool
                    let key_ptr = apr::strings::pstrdup_raw(k, &result_pool)?;
                    let val_ptr = apr::strings::pstrdup_raw(v, &result_pool)?;
                    hash.insert(
                        std::slice::from_raw_parts(key_ptr as *const u8, k.len()),
                        val_ptr as *mut std::ffi::c_void,
                    );
                }
            }
            unsafe { hash.as_mut_ptr() }
        }
        None => std::ptr::null_mut(),
    };

    unsafe {
        let mut translated_stream: *mut subversion_sys::svn_stream_t = std::ptr::null_mut();
        let err = subversion_sys::svn_subst_stream_translated_to_normal_form(
            &mut translated_stream,
            source_stream.as_mut_ptr(),
            eol_style.into(),
            eol_ptr,
            if always_repair_eols { 1 } else { 0 },
            keywords_hash,
            result_pool.as_mut_ptr(),
        );
        Error::from_raw(err)?;

        Ok(crate::io::Stream::from_ptr(translated_stream, result_pool))
    }
}

/// Copy and translate a file with keyword and EOL substitution
///
/// This is a convenience function that copies from source to destination
/// while performing translation. Either eol_str or keywords (or both) must
/// be provided.
pub fn translate_stream(
    src_stream: &mut crate::io::Stream,
    dst_stream: &mut crate::io::Stream,
    eol_str: Option<&str>,
    repair: bool,
    keywords: Option<&HashMap<String, String>>,
    expand: bool,
) -> Result<(), Error<'static>> {
    with_tmp_pool(|pool| {
        let eol_cstr = eol_str.map(|s| std::ffi::CString::new(s).unwrap());
        let eol_ptr = eol_cstr
            .as_ref()
            .map(|c| c.as_ptr())
            .unwrap_or(std::ptr::null());

        let keywords_hash = match keywords {
            Some(kw) => {
                let mut hash = apr::hash::Hash::new(pool);
                for (k, v) in kw.iter() {
                    unsafe {
                        // Copy key into pool to ensure it outlives the hash
                        let k_pooled = apr::strings::pstrdup_raw(k, pool).unwrap();
                        // Create svn_string_t for the value (SVN expects svn_string_t*, not const char*)
                        let v_cstr = std::ffi::CString::new(v.as_str()).unwrap();
                        let svn_str =
                            subversion_sys::svn_string_create(v_cstr.as_ptr(), pool.as_mut_ptr());
                        // Use the pooled key which is null-terminated
                        let key_slice =
                            std::slice::from_raw_parts(k_pooled as *const u8, k.len() + 1);
                        hash.insert(key_slice, svn_str as *mut std::ffi::c_void);
                    }
                }
                unsafe { hash.as_mut_ptr() }
            }
            None => std::ptr::null_mut(),
        };

        unsafe {
            let err = subversion_sys::svn_subst_translate_stream3(
                src_stream.as_mut_ptr(),
                dst_stream.as_mut_ptr(),
                eol_ptr,
                if repair { 1 } else { 0 },
                keywords_hash,
                if expand { 1 } else { 0 },
                pool.as_mut_ptr(),
            );
            Error::from_raw(err)
        }
    })
}

/// Copy and translate a file with keyword and EOL substitution.
///
/// Copies from `src` to `dst` while performing EOL and keyword translation.
/// If `special` is true, the file is treated as a special file (symlink, etc.).
pub fn copy_and_translate(
    src: &std::path::Path,
    dst: &std::path::Path,
    eol_str: Option<&str>,
    repair: bool,
    keywords: Option<&HashMap<String, String>>,
    expand: bool,
    special: bool,
) -> Result<(), Error<'static>> {
    with_tmp_pool(|pool| {
        let src_cstr = std::ffi::CString::new(
            src.to_str()
                .ok_or_else(|| Error::from_message("Invalid source path encoding"))?,
        )?;
        let dst_cstr = std::ffi::CString::new(
            dst.to_str()
                .ok_or_else(|| Error::from_message("Invalid destination path encoding"))?,
        )?;

        let eol_cstr = eol_str.map(|s| std::ffi::CString::new(s).unwrap());
        let eol_ptr = eol_cstr
            .as_ref()
            .map(|c| c.as_ptr())
            .unwrap_or(std::ptr::null());

        let keywords_hash = match keywords {
            Some(kw) => {
                let mut hash = apr::hash::Hash::new(pool);
                for (k, v) in kw.iter() {
                    unsafe {
                        let k_pooled = apr::strings::pstrdup_raw(k, pool).unwrap();
                        let v_cstr = std::ffi::CString::new(v.as_str()).unwrap();
                        let svn_str =
                            subversion_sys::svn_string_create(v_cstr.as_ptr(), pool.as_mut_ptr());
                        let key_slice =
                            std::slice::from_raw_parts(k_pooled as *const u8, k.len() + 1);
                        hash.insert(key_slice, svn_str as *mut std::ffi::c_void);
                    }
                }
                unsafe { hash.as_mut_ptr() }
            }
            None => std::ptr::null_mut(),
        };

        unsafe {
            let err = subversion_sys::svn_subst_copy_and_translate4(
                src_cstr.as_ptr(),
                dst_cstr.as_ptr(),
                eol_ptr,
                if repair { 1 } else { 0 },
                keywords_hash,
                if expand { 1 } else { 0 },
                if special { 1 } else { 0 },
                None,
                std::ptr::null_mut(),
                pool.as_mut_ptr(),
            );
            Error::from_raw(err)
        }
    })
}

/// Translate a string with keyword and EOL substitution.
///
/// Returns the translated string.
pub fn translate_cstring(
    src: &str,
    eol_str: Option<&str>,
    repair: bool,
    keywords: Option<&HashMap<String, String>>,
    expand: bool,
) -> Result<String, Error<'static>> {
    with_tmp_pool(|pool| {
        let src_cstr = std::ffi::CString::new(src)?;
        let eol_cstr = eol_str.map(|s| std::ffi::CString::new(s).unwrap());
        let eol_ptr = eol_cstr
            .as_ref()
            .map(|c| c.as_ptr())
            .unwrap_or(std::ptr::null());

        let keywords_hash = match keywords {
            Some(kw) => {
                let mut hash = apr::hash::Hash::new(pool);
                for (k, v) in kw.iter() {
                    unsafe {
                        let k_pooled = apr::strings::pstrdup_raw(k, pool).unwrap();
                        let v_cstr = std::ffi::CString::new(v.as_str()).unwrap();
                        let svn_str =
                            subversion_sys::svn_string_create(v_cstr.as_ptr(), pool.as_mut_ptr());
                        let key_slice =
                            std::slice::from_raw_parts(k_pooled as *const u8, k.len() + 1);
                        hash.insert(key_slice, svn_str as *mut std::ffi::c_void);
                    }
                }
                unsafe { hash.as_mut_ptr() }
            }
            None => std::ptr::null_mut(),
        };

        let mut dst_ptr: *const std::ffi::c_char = std::ptr::null();

        unsafe {
            let err = subversion_sys::svn_subst_translate_cstring2(
                src_cstr.as_ptr(),
                &mut dst_ptr,
                eol_ptr,
                if repair { 1 } else { 0 },
                keywords_hash,
                if expand { 1 } else { 0 },
                pool.as_mut_ptr(),
            );
            Error::from_raw(err)?;

            if dst_ptr.is_null() {
                return Ok(String::new());
            }

            Ok(std::ffi::CStr::from_ptr(dst_ptr)
                .to_str()
                .map_err(|_| Error::from_message("Translated string is not valid UTF-8"))?
                .to_string())
        }
    })
}

/// Translate a string to UTF-8 and LF line endings.
///
/// Returns the translated string and flags indicating what translations were performed.
pub fn translate_string(
    value: &[u8],
    encoding: Option<&str>,
    repair: bool,
) -> Result<(Vec<u8>, bool, bool), Error<'static>> {
    with_tmp_pool(|pool| {
        let encoding_cstr = encoding.map(|s| std::ffi::CString::new(s).unwrap());
        let encoding_ptr = encoding_cstr
            .as_ref()
            .map(|c| c.as_ptr())
            .unwrap_or(std::ptr::null());

        // Create svn_string_t for input
        let input_str = unsafe {
            subversion_sys::svn_string_ncreate(
                value.as_ptr() as *const std::ffi::c_char,
                value.len(),
                pool.as_mut_ptr(),
            )
        };

        let mut new_value: *mut subversion_sys::svn_string_t = std::ptr::null_mut();
        let mut translated_to_utf8: i32 = 0;
        let mut translated_line_endings: i32 = 0;

        unsafe {
            let err = subversion_sys::svn_subst_translate_string2(
                &mut new_value,
                &mut translated_to_utf8,
                &mut translated_line_endings,
                input_str,
                encoding_ptr,
                if repair { 1 } else { 0 },
                pool.as_mut_ptr(),
                pool.as_mut_ptr(),
            );
            Error::from_raw(err)?;

            if new_value.is_null() || (*new_value).data.is_null() {
                return Ok((Vec::new(), false, false));
            }

            let data = std::slice::from_raw_parts((*new_value).data as *const u8, (*new_value).len);
            Ok((
                data.to_vec(),
                translated_to_utf8 != 0,
                translated_line_endings != 0,
            ))
        }
    })
}

/// Open a stream for reading a special file (symlink, etc.).
///
/// Returns a stream that reads the "normal form" content of the special file.
pub fn read_specialfile(path: &std::path::Path) -> Result<crate::io::Stream, Error<'static>> {
    let pool = apr::Pool::new();
    let path_cstr = std::ffi::CString::new(
        path.to_str()
            .ok_or_else(|| Error::from_message("Invalid path encoding"))?,
    )?;

    let mut stream: *mut subversion_sys::svn_stream_t = std::ptr::null_mut();

    unsafe {
        let err = subversion_sys::svn_subst_read_specialfile(
            &mut stream,
            path_cstr.as_ptr(),
            pool.as_mut_ptr(),
            pool.as_mut_ptr(),
        );
        Error::from_raw(err)?;

        Ok(crate::io::Stream::from_ptr(stream, pool))
    }
}

/// Create a stream for writing a special file (symlink, etc.).
///
/// Returns a stream that accepts content in "normal form" and creates
/// the special file when closed.
pub fn create_specialfile(path: &std::path::Path) -> Result<crate::io::Stream, Error<'static>> {
    let pool = apr::Pool::new();
    let path_cstr = std::ffi::CString::new(
        path.to_str()
            .ok_or_else(|| Error::from_message("Invalid path encoding"))?,
    )?;

    let mut stream: *mut subversion_sys::svn_stream_t = std::ptr::null_mut();

    unsafe {
        let err = subversion_sys::svn_subst_create_specialfile(
            &mut stream,
            path_cstr.as_ptr(),
            pool.as_mut_ptr(),
            pool.as_mut_ptr(),
        );
        Error::from_raw(err)?;

        Ok(crate::io::Stream::from_ptr(stream, pool))
    }
}

/// Create a detranslated stream (from working copy to repository form).
///
/// Returns a stream that translates content from the working copy format
/// to the repository format.
pub fn stream_detranslated(
    src_path: &std::path::Path,
    eol_style: EolStyle,
    eol_str: Option<&str>,
    always_repair_eols: bool,
    keywords: Option<&HashMap<String, String>>,
    special: bool,
) -> Result<crate::io::Stream, Error<'static>> {
    let pool = apr::Pool::new();
    let path_cstr = std::ffi::CString::new(
        src_path
            .to_str()
            .ok_or_else(|| Error::from_message("Invalid path encoding"))?,
    )?;

    let eol_ptr = match eol_str {
        Some(s) => apr::strings::pstrdup_raw(s, &pool)?,
        None => std::ptr::null(),
    };

    let keywords_hash = match keywords {
        Some(kw) => {
            let mut hash = apr::hash::Hash::new(&pool);
            for (k, v) in kw.iter() {
                unsafe {
                    let key_ptr = apr::strings::pstrdup_raw(k, &pool)?;
                    let val_ptr = apr::strings::pstrdup_raw(v, &pool)?;
                    hash.insert(
                        std::slice::from_raw_parts(key_ptr as *const u8, k.len()),
                        val_ptr as *mut std::ffi::c_void,
                    );
                }
            }
            unsafe { hash.as_mut_ptr() }
        }
        None => std::ptr::null_mut(),
    };

    let mut stream: *mut subversion_sys::svn_stream_t = std::ptr::null_mut();

    unsafe {
        let err = subversion_sys::svn_subst_stream_detranslated(
            &mut stream,
            path_cstr.as_ptr(),
            eol_style.into(),
            eol_ptr,
            if always_repair_eols { 1 } else { 0 },
            keywords_hash,
            if special { 1 } else { 0 },
            pool.as_mut_ptr(),
        );
        Error::from_raw(err)?;

        Ok(crate::io::Stream::from_ptr(stream, pool))
    }
}

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

    #[test]
    fn test_eol_style_from_value() {
        let (style, eol) = eol_style_from_value("native");
        assert_eq!(style, EolStyle::Native);
        assert!(eol.is_some());

        let (style, eol) = eol_style_from_value("LF");
        assert_eq!(style, EolStyle::Fixed);
        assert_eq!(eol.as_deref(), Some("\n"));

        let (style, eol) = eol_style_from_value("CRLF");
        assert_eq!(style, EolStyle::Fixed);
        assert_eq!(eol.as_deref(), Some("\r\n"));

        let (style, eol) = eol_style_from_value("CR");
        assert_eq!(style, EolStyle::Fixed);
        assert_eq!(eol.as_deref(), Some("\r"));

        // Test invalid value returns Unknown
        let (style, eol) = eol_style_from_value("invalid");
        assert_eq!(style, EolStyle::Unknown);
        assert!(eol.is_none());
    }

    #[test]
    fn test_translation_required() {
        // No translation needed
        assert!(!translation_required(
            EolStyle::None,
            None,
            None,
            false,
            false
        ));

        // EOL translation may or may not be needed depending on platform
        // On Unix systems, native is \n, so \n -> native requires no translation
        let _result = translation_required(EolStyle::Native, Some("\n"), None, false, false);
        // Don't assert since this is platform-dependent

        // Keyword translation needed
        let mut keywords = HashMap::new();
        keywords.insert("Id".to_string(), "$Id$".to_string());
        assert!(translation_required(
            EolStyle::None,
            None,
            Some(&keywords),
            false,
            false
        ));
    }

    #[test]
    fn test_build_keywords() {
        let keywords = build_keywords(
            "Id Rev Author Date",
            Some("123"),
            Some("http://example.com/repo/file.txt"),
            Some(apr::time::Time::now()),
            Some("testuser"),
            Some("http://example.com/repo"),
        )
        .unwrap();

        assert!(keywords.contains_key("Id"));
        assert!(keywords.contains_key("Rev"));
        assert!(keywords.contains_key("Author"));
        assert!(keywords.contains_key("Date"));

        // Test with empty keyword string
        let empty_keywords = build_keywords("", None, None, None, None, None).unwrap();
        assert!(empty_keywords.is_empty());
    }

    #[test]
    fn test_keywords_differ() {
        let mut kw1 = HashMap::new();
        kw1.insert("Id".to_string(), "$Id$".to_string());
        kw1.insert("Rev".to_string(), "$Rev$".to_string());

        let mut kw2 = HashMap::new();
        kw2.insert("Id".to_string(), "$Id$".to_string());
        kw2.insert("Rev".to_string(), "$Rev$".to_string());

        // Same keywords
        assert!(!keywords_differ(Some(&kw1), Some(&kw2), true).unwrap());
        assert!(!keywords_differ(Some(&kw1), Some(&kw2), false).unwrap());

        // Different values
        kw2.insert("Rev".to_string(), "$Rev: 456$".to_string());
        assert!(keywords_differ(Some(&kw1), Some(&kw2), true).unwrap());
        assert!(!keywords_differ(Some(&kw1), Some(&kw2), false).unwrap());

        // Different keys
        kw2.insert("Date".to_string(), "$Date$".to_string());
        assert!(keywords_differ(Some(&kw1), Some(&kw2), true).unwrap());
        assert!(keywords_differ(Some(&kw1), Some(&kw2), false).unwrap());

        // One empty, one not
        assert!(keywords_differ(Some(&kw1), None, false).unwrap());
        assert!(keywords_differ(None, Some(&kw2), false).unwrap());

        // Both empty
        assert!(!keywords_differ(None, None, false).unwrap());
    }

    #[test]
    fn test_eol_style_conversions() {
        // Test From<svn_subst_eol_style_t> for EolStyle
        let unknown: EolStyle =
            subversion_sys::svn_subst_eol_style_svn_subst_eol_style_unknown.into();
        assert_eq!(unknown, EolStyle::Unknown);

        let none: EolStyle = subversion_sys::svn_subst_eol_style_svn_subst_eol_style_none.into();
        assert_eq!(none, EolStyle::None);

        let native: EolStyle =
            subversion_sys::svn_subst_eol_style_svn_subst_eol_style_native.into();
        assert_eq!(native, EolStyle::Native);

        let fixed: EolStyle = subversion_sys::svn_subst_eol_style_svn_subst_eol_style_fixed.into();
        assert_eq!(fixed, EolStyle::Fixed);

        // Test From<EolStyle> for svn_subst_eol_style_t
        let unknown_raw: subversion_sys::svn_subst_eol_style_t = EolStyle::Unknown.into();
        assert_eq!(
            unknown_raw,
            subversion_sys::svn_subst_eol_style_svn_subst_eol_style_unknown
        );

        let none_raw: subversion_sys::svn_subst_eol_style_t = EolStyle::None.into();
        assert_eq!(
            none_raw,
            subversion_sys::svn_subst_eol_style_svn_subst_eol_style_none
        );

        let native_raw: subversion_sys::svn_subst_eol_style_t = EolStyle::Native.into();
        assert_eq!(
            native_raw,
            subversion_sys::svn_subst_eol_style_svn_subst_eol_style_native
        );

        let fixed_raw: subversion_sys::svn_subst_eol_style_t = EolStyle::Fixed.into();
        assert_eq!(
            fixed_raw,
            subversion_sys::svn_subst_eol_style_svn_subst_eol_style_fixed
        );
    }

    #[test]
    fn test_build_keywords_with_nulls() {
        // Test with all None values
        let keywords = build_keywords("Id", None, None, None, None, None).unwrap();
        assert!(keywords.contains_key("Id"));

        // Test with some None values
        let keywords = build_keywords("Rev", Some("100"), None, None, None, None).unwrap();
        assert!(keywords.contains_key("Rev"));
    }

    #[test]
    fn test_stream_translated() {
        // Create a simple memory stream with test content
        let content = b"Test content with $Id$ keyword\n";
        let source = crate::io::Stream::from(&content[..]);

        // Test basic stream translation without keywords
        stream_translated(source, Some("\n"), false, None, true).unwrap();
    }

    #[test]
    fn test_stream_translated_with_keywords() {
        let content = b"Test content with $Id$ keyword\n";
        let source = crate::io::Stream::from(&content[..]);

        let mut keywords = HashMap::new();
        keywords.insert("Id".to_string(), "test-id".to_string());

        stream_translated(source, Some("\n"), false, Some(&keywords), true).unwrap();
    }

    #[test]
    fn test_stream_translated_to_normal_form() {
        let content = b"Test content\r\n";
        let source = crate::io::Stream::from(&content[..]);

        stream_translated_to_normal_form(source, EolStyle::Native, Some("\n"), false, None)
            .unwrap();
    }

    #[test]
    fn test_stream_translated_to_normal_form_with_keywords() {
        let content = b"Test $Rev$ content\n";
        let source = crate::io::Stream::from(&content[..]);

        let mut keywords = HashMap::new();
        keywords.insert("Rev".to_string(), "123".to_string());

        stream_translated_to_normal_form(
            source,
            EolStyle::Fixed,
            Some("\n"),
            false,
            Some(&keywords),
        )
        .unwrap();
    }

    #[test]
    fn test_translate_stream() {
        let content = b"Test content\n";
        let mut source = crate::io::Stream::from(&content[..]);
        let mut dest = crate::io::Stream::empty();

        translate_stream(&mut source, &mut dest, Some("\n"), false, None, false).unwrap();
    }

    #[test]
    fn test_translate_stream_with_keywords() {
        let content = b"Test $Id$ content\n";
        let mut source = crate::io::Stream::from(&content[..]);
        let mut dest = crate::io::Stream::empty();

        let mut keywords = HashMap::new();
        keywords.insert("Id".to_string(), "test-id".to_string());

        translate_stream(
            &mut source,
            &mut dest,
            Some("\n"),
            false,
            Some(&keywords),
            true,
        )
        .unwrap();
    }

    #[test]
    fn test_translate_cstring() {
        let src = "Hello\nWorld\n";
        let result = translate_cstring(src, Some("\n"), false, None, false).unwrap();
        assert_eq!(result, "Hello\nWorld\n");
    }

    #[test]
    fn test_translate_cstring_with_eol() {
        let src = "Hello\nWorld\n";
        let result = translate_cstring(src, Some("\r\n"), false, None, false).unwrap();
        assert_eq!(result, "Hello\r\nWorld\r\n");
    }

    #[test]
    fn test_translate_string() {
        let value = b"Hello\nWorld\n";
        let (result, to_utf8, line_endings) = translate_string(value, None, false).unwrap();
        assert_eq!(result, b"Hello\nWorld\n");
        // Input is already UTF-8 with LF endings, so no translation needed
        assert!(!to_utf8);
        assert!(!line_endings);
    }

    #[test]
    fn test_copy_and_translate() {
        let td = tempfile::tempdir().unwrap();
        let src = td.path().join("src.txt");
        let dst = td.path().join("dst.txt");

        std::fs::write(&src, "Hello\nWorld\n").unwrap();

        copy_and_translate(&src, &dst, Some("\n"), false, None, false, false).unwrap();

        let content = std::fs::read_to_string(&dst).unwrap();
        assert_eq!(content, "Hello\nWorld\n");
    }

    #[test]
    fn test_translate_cstring_expands_keyword() {
        let mut keywords = HashMap::new();
        keywords.insert("Author".to_string(), "alice".to_string());

        let expanded =
            translate_cstring("$Author$\n", Some("\n"), false, Some(&keywords), true).unwrap();
        assert_eq!(expanded, "$Author: alice $\n");

        // Without expansion the keyword is left collapsed.
        let collapsed =
            translate_cstring("$Author$\n", Some("\n"), false, Some(&keywords), false).unwrap();
        assert_eq!(collapsed, "$Author$\n");
    }

    #[test]
    fn test_translate_stream_expands_keyword() {
        let content = b"$Author$\n";
        let mut source = crate::io::Stream::from(&content[..]);
        let mut stringbuf = crate::io::StringBuf::new();

        let mut keywords = HashMap::new();
        keywords.insert("Author".to_string(), "alice".to_string());

        {
            let mut dest = crate::io::Stream::from_stringbuf(&mut stringbuf);
            translate_stream(
                &mut source,
                &mut dest,
                Some("\n"),
                false,
                Some(&keywords),
                true,
            )
            .unwrap();
        }
        assert_eq!(stringbuf.to_string(), "$Author: alice $\n");
    }

    #[test]
    fn test_copy_and_translate_expands_keyword() {
        let td = tempfile::tempdir().unwrap();
        let src = td.path().join("src.txt");
        let dst = td.path().join("dst.txt");

        std::fs::write(&src, "$Author$\n").unwrap();

        let mut keywords = HashMap::new();
        keywords.insert("Author".to_string(), "alice".to_string());

        copy_and_translate(&src, &dst, Some("\n"), false, Some(&keywords), true, false).unwrap();

        let content = std::fs::read_to_string(&dst).unwrap();
        assert_eq!(content, "$Author: alice $\n");
    }
}
